mirror of
https://github.com/FOSSBilling/FOSSBilling.git
synced 2026-10-23 03:37:01 +09:00
Harden release cleanup and updater integrity (#4166)
This commit is contained in:
1 parent
bbc69f9d9d
commit
b364588f30
148 files changed
+2640
-1805
No files matched your search
@@ -13,6 +13,7 @@ node_modules
|
|||||||
output.log
|
output.log
|
||||||
|
|
||||||
src/config.php
|
src/config.php
|
||||||
|
src/config.old.php
|
||||||
src/vendor
|
src/vendor
|
||||||
src/**/node_modules
|
src/**/node_modules
|
||||||
src/public/assets
|
src/public/assets
|
||||||
|
|||||||
+6
-2
@@ -61,6 +61,8 @@ COPY src/library ./src/library
|
|||||||
COPY src/modules ./src/modules
|
COPY src/modules ./src/modules
|
||||||
|
|
||||||
RUN --mount=type=cache,target=/tmp/composer-cache,id=composer-prod \
|
RUN --mount=type=cache,target=/tmp/composer-cache,id=composer-prod \
|
||||||
|
set -eux; \
|
||||||
|
find ./src/modules -type d -name tests -prune -exec rm -rf {} +; \
|
||||||
COMPOSER_CACHE_DIR=/tmp/composer-cache \
|
COMPOSER_CACHE_DIR=/tmp/composer-cache \
|
||||||
composer install --prefer-dist --no-dev --optimize-autoloader --no-interaction --no-progress
|
composer install --prefer-dist --no-dev --optimize-autoloader --no-interaction --no-progress
|
||||||
|
|
||||||
@@ -112,6 +114,7 @@ COPY --from=frontend-assets /app/src/themes/admin_default/assets/build ./src/the
|
|||||||
COPY --from=frontend-assets /app/src/themes/huraga/assets/build ./src/themes/huraga/assets/build
|
COPY --from=frontend-assets /app/src/themes/huraga/assets/build ./src/themes/huraga/assets/build
|
||||||
|
|
||||||
RUN set -eux; \
|
RUN set -eux; \
|
||||||
|
find ./src/modules -type d -name tests -prune -exec rm -rf {} +; \
|
||||||
mkdir -p ./src/locale; \
|
mkdir -p ./src/locale; \
|
||||||
if [ "${INSTALL_TRANSLATIONS}" = "true" ]; then \
|
if [ "${INSTALL_TRANSLATIONS}" = "true" ]; then \
|
||||||
curl -fsSL "${TRANSLATIONS_URL}" -o /tmp/translations.zip; \
|
curl -fsSL "${TRANSLATIONS_URL}" -o /tmp/translations.zip; \
|
||||||
@@ -125,7 +128,7 @@ RUN set -eux; \
|
|||||||
FOSSBILLING_VERSION_TRUNCATE="${FOSSBILLING_VERSION_TRUNCATE}" \
|
FOSSBILLING_VERSION_TRUNCATE="${FOSSBILLING_VERSION_TRUNCATE}" \
|
||||||
SENTRY_DSN="${SENTRY_DSN}" \
|
SENTRY_DSN="${SENTRY_DSN}" \
|
||||||
php -r '$version = getenv("FOSSBILLING_VERSION") ?: "0.0.1"; $truncate = (int) (getenv("FOSSBILLING_VERSION_TRUNCATE") ?: 0); if ($truncate > 0) { $version = substr($version, 0, $truncate); } $versionFile = "./src/library/FOSSBilling/Version.php"; $contents = file_get_contents($versionFile); $quote = chr(39); $pattern = "/public const string VERSION = " . $quote . "[^" . $quote . "]+" . $quote . ";/"; $replacement = "public const string VERSION = " . var_export($version, true) . ";"; $contents = preg_replace($pattern, $replacement, $contents, 1, $count); if ($contents === null || $count !== 1) { fwrite(STDERR, "Failed to replace FOSSBilling version.\n"); exit(1); } file_put_contents($versionFile, $contents); $dsn = getenv("SENTRY_DSN"); if ($dsn !== false && $dsn !== "") { $sentryFile = "./src/library/FOSSBilling/SentryHelper.php"; file_put_contents($sentryFile, str_replace("--replace--this--during--release--process--", $dsn, file_get_contents($sentryFile))); }'; \
|
php -r '$version = getenv("FOSSBILLING_VERSION") ?: "0.0.1"; $truncate = (int) (getenv("FOSSBILLING_VERSION_TRUNCATE") ?: 0); if ($truncate > 0) { $version = substr($version, 0, $truncate); } $versionFile = "./src/library/FOSSBilling/Version.php"; $contents = file_get_contents($versionFile); $quote = chr(39); $pattern = "/public const string VERSION = " . $quote . "[^" . $quote . "]+" . $quote . ";/"; $replacement = "public const string VERSION = " . var_export($version, true) . ";"; $contents = preg_replace($pattern, $replacement, $contents, 1, $count); if ($contents === null || $count !== 1) { fwrite(STDERR, "Failed to replace FOSSBilling version.\n"); exit(1); } file_put_contents($versionFile, $contents); $dsn = getenv("SENTRY_DSN"); if ($dsn !== false && $dsn !== "") { $sentryFile = "./src/library/FOSSBilling/SentryHelper.php"; file_put_contents($sentryFile, str_replace("--replace--this--during--release--process--", $dsn, file_get_contents($sentryFile))); }'; \
|
||||||
php -r '$composer = json_decode(file_get_contents("./composer.json"), true, 512, JSON_THROW_ON_ERROR); $composer["autoload"]["psr-4"]["Box\\Mod\\"] = "modules/"; $composer["autoload"]["classmap"] = ["library/"]; $composer["config"]["vendor-dir"] = "vendor"; file_put_contents("./src/composer.json", json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR) . PHP_EOL);'; \
|
php -r '$composer = json_decode(file_get_contents("./composer.json"), true, 512, JSON_THROW_ON_ERROR); $composer["autoload"]["psr-4"]["FOSSBilling\\"] = "library/FOSSBilling/"; $composer["autoload"]["psr-4"]["Box\\Mod\\"] = "modules/"; $composer["autoload"]["classmap"] = ["library/"]; $composer["config"]["vendor-dir"] = "vendor"; file_put_contents("./src/composer.json", json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR) . PHP_EOL);'; \
|
||||||
composer --no-plugins dump-autoload --working-dir=./src --no-dev --optimize --no-interaction; \
|
composer --no-plugins dump-autoload --working-dir=./src --no-dev --optimize --no-interaction; \
|
||||||
rm ./composer.json ./src/composer.json; \
|
rm ./composer.json ./src/composer.json; \
|
||||||
chmod -R u=rwX,go=rX ./src
|
chmod -R u=rwX,go=rX ./src
|
||||||
@@ -155,8 +158,9 @@ FROM runtime AS test
|
|||||||
WORKDIR /workspace
|
WORKDIR /workspace
|
||||||
|
|
||||||
COPY --from=release-tree /app/src ./src
|
COPY --from=release-tree /app/src ./src
|
||||||
|
COPY src/modules ./src/modules
|
||||||
COPY --from=php-dev-vendor /app/src/vendor ./src/vendor
|
COPY --from=php-dev-vendor /app/src/vendor ./src/vendor
|
||||||
COPY composer.json composer.lock phpstan.neon phpstan-baseline.neon phpunit.xml.dist ./
|
COPY composer.json composer.lock phpstan.neon phpunit.xml.dist ./
|
||||||
COPY tests ./tests
|
COPY tests ./tests
|
||||||
|
|
||||||
RUN set -eux; \
|
RUN set -eux; \
|
||||||
|
|||||||
@@ -101,6 +101,7 @@
|
|||||||
"Server_": "src/library/"
|
"Server_": "src/library/"
|
||||||
},
|
},
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
|
"FOSSBilling\\": "src/library/FOSSBilling/",
|
||||||
"Box\\Mod\\": "src/modules/"
|
"Box\\Mod\\": "src/modules/"
|
||||||
},
|
},
|
||||||
"classmap": [
|
"classmap": [
|
||||||
|
|||||||
+13
-3
@@ -62,7 +62,8 @@
|
|||||||
"falsey",
|
"falsey",
|
||||||
"unindexed",
|
"unindexed",
|
||||||
"catchable",
|
"catchable",
|
||||||
"nelexa"
|
"nelexa",
|
||||||
|
"apisecret"
|
||||||
],
|
],
|
||||||
|
|
||||||
"ignoreWords": [
|
"ignoreWords": [
|
||||||
@@ -546,7 +547,17 @@
|
|||||||
"htype",
|
"htype",
|
||||||
"webspace",
|
"webspace",
|
||||||
"webspaces",
|
"webspaces",
|
||||||
"serviceexternal"
|
"serviceexternal",
|
||||||
|
"aftp",
|
||||||
|
"anonftp",
|
||||||
|
"dnscontrol",
|
||||||
|
"maillists",
|
||||||
|
"nemailf",
|
||||||
|
"nemailml",
|
||||||
|
"nemailr",
|
||||||
|
"spamfilter",
|
||||||
|
"sysinfo",
|
||||||
|
"unemailml"
|
||||||
],
|
],
|
||||||
"ignorePaths": [
|
"ignorePaths": [
|
||||||
"node_modules/**",
|
"node_modules/**",
|
||||||
@@ -585,7 +596,6 @@
|
|||||||
".github/**",
|
".github/**",
|
||||||
".gitignore",
|
".gitignore",
|
||||||
"src/.htaccess",
|
"src/.htaccess",
|
||||||
"phpstan-baseline.neon",
|
|
||||||
"Dockerfile",
|
"Dockerfile",
|
||||||
".ddev/**"
|
".ddev/**"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,127 +0,0 @@
|
|||||||
parameters:
|
|
||||||
ignoreErrors:
|
|
||||||
-
|
|
||||||
message: '#^Call to an undefined method Closure\:\:getExistingModelById\(\)\.$#'
|
|
||||||
identifier: method.notFound
|
|
||||||
count: 2
|
|
||||||
path: src/di.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: '#^Call to an undefined method Monolog\\Handler\\HandlerInterface\:\:setFormatter\(\)\.$#'
|
|
||||||
identifier: method.notFound
|
|
||||||
count: 1
|
|
||||||
path: src/library/FOSSBilling/Monolog.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: '#^Method Registrar_Adapter_Namecheap\:\:getPrivacyInfo\(\) should return array\<string\> but returns array\<string, bool\|string\>\.$#'
|
|
||||||
identifier: return.type
|
|
||||||
count: 1
|
|
||||||
path: src/library/Registrar/Adapter/Namecheap.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: '#^Method Registrar_Adapter_Resellerclub\:\:addSubReseller\(\) should return stdClass but returns array\|string\.$#'
|
|
||||||
identifier: return.type
|
|
||||||
count: 1
|
|
||||||
path: src/library/Registrar/Adapter/Resellerclub.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: '#^Call to an undefined method Server_Package\:\:getHasAnonymousFtp\(\)\.$#'
|
|
||||||
identifier: method.notFound
|
|
||||||
count: 1
|
|
||||||
path: src/library/Server/Manager/Directadmin.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: '#^Call to an undefined method Server_Package\:\:getHasCatchAll\(\)\.$#'
|
|
||||||
identifier: method.notFound
|
|
||||||
count: 1
|
|
||||||
path: src/library/Server/Manager/Directadmin.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: '#^Call to an undefined method Server_Package\:\:getHasCgi\(\)\.$#'
|
|
||||||
identifier: method.notFound
|
|
||||||
count: 1
|
|
||||||
path: src/library/Server/Manager/Directadmin.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: '#^Call to an undefined method Server_Package\:\:getHasCron\(\)\.$#'
|
|
||||||
identifier: method.notFound
|
|
||||||
count: 1
|
|
||||||
path: src/library/Server/Manager/Directadmin.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: '#^Call to an undefined method Server_Package\:\:getHasPhp\(\)\.$#'
|
|
||||||
identifier: method.notFound
|
|
||||||
count: 1
|
|
||||||
path: src/library/Server/Manager/Directadmin.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: '#^Call to an undefined method Server_Package\:\:getHasShell\(\)\.$#'
|
|
||||||
identifier: method.notFound
|
|
||||||
count: 1
|
|
||||||
path: src/library/Server/Manager/Directadmin.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: '#^Call to an undefined method Server_Package\:\:getHasSsl\(\)\.$#'
|
|
||||||
identifier: method.notFound
|
|
||||||
count: 1
|
|
||||||
path: src/library/Server/Manager/Directadmin.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: '#^Call to an undefined method Server_Package\:\:getHasSpamFilter\(\)\.$#'
|
|
||||||
identifier: method.notFound
|
|
||||||
count: 1
|
|
||||||
path: src/library/Server/Manager/Directadmin.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: '#^Call to an undefined method Server_Package\:\:getMaxEmailAutoresponders\(\)\.$#'
|
|
||||||
identifier: method.notFound
|
|
||||||
count: 4
|
|
||||||
path: src/library/Server/Manager/Directadmin.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: '#^Call to an undefined method Server_Package\:\:getMaxEmailForwarders\(\)\.$#'
|
|
||||||
identifier: method.notFound
|
|
||||||
count: 4
|
|
||||||
path: src/library/Server/Manager/Directadmin.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: '#^Call to an undefined method Server_Package\:\:getMaxEmailLists\(\)\.$#'
|
|
||||||
identifier: method.notFound
|
|
||||||
count: 4
|
|
||||||
path: src/library/Server/Manager/Directadmin.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: '#^Access to an undefined property PleskX\\Api\\Struct\\Dns\\Info\:\:\$dns\.$#'
|
|
||||||
identifier: property.notFound
|
|
||||||
count: 1
|
|
||||||
path: src/library/Server/Manager/Plesk.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: '#^Call to an undefined method Server_Package\:\:getHasAnonymousFtp\(\)\.$#'
|
|
||||||
identifier: method.notFound
|
|
||||||
count: 1
|
|
||||||
path: src/library/Server/Manager/Plesk.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: '#^Call to an undefined method Server_Package\:\:getHasCron\(\)\.$#'
|
|
||||||
identifier: method.notFound
|
|
||||||
count: 1
|
|
||||||
path: src/library/Server/Manager/Plesk.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: '#^Call to an undefined method Server_Package\:\:getHasShell\(\)\.$#'
|
|
||||||
identifier: method.notFound
|
|
||||||
count: 2
|
|
||||||
path: src/library/Server/Manager/Plesk.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: '#^Call to an undefined method Server_Package\:\:getHasSpamFilter\(\)\.$#'
|
|
||||||
identifier: method.notFound
|
|
||||||
count: 1
|
|
||||||
path: src/library/Server/Manager/Plesk.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: '#^Call to an undefined method Server_Package\:\:getMaxEmailLists\(\)\.$#'
|
|
||||||
identifier: method.notFound
|
|
||||||
count: 2
|
|
||||||
path: src/library/Server/Manager/Plesk.php
|
|
||||||
@@ -1,6 +1,3 @@
|
|||||||
includes:
|
|
||||||
- phpstan-baseline.neon
|
|
||||||
|
|
||||||
parameters:
|
parameters:
|
||||||
level: 5
|
level: 5
|
||||||
reportUnmatchedIgnoredErrors: false
|
reportUnmatchedIgnoredErrors: false
|
||||||
|
|||||||
+23
-20
@@ -39,27 +39,32 @@ if (!$request instanceof Request) {
|
|||||||
*
|
*
|
||||||
* @param void
|
* @param void
|
||||||
*
|
*
|
||||||
* @return Box_Log A new logger instance
|
* @return FOSSBilling\Logger A new logger instance
|
||||||
*/
|
*/
|
||||||
$di['logger'] = function () use ($di) {
|
$di['logger'] = function () use ($di) {
|
||||||
$log = new Box_Log();
|
$log = new FOSSBilling\Logger();
|
||||||
$log->setDi($di);
|
$log->setDi($di);
|
||||||
|
|
||||||
$activity_service = $di['mod_service']('activity');
|
$activity_service = $di['mod_service']('activity');
|
||||||
$dbWriter = new Box_LogDb($activity_service);
|
$dbWriter = new FOSSBilling\Logging\DatabaseWriter($activity_service);
|
||||||
$log->addWriter($dbWriter);
|
$log->addWriter($dbWriter);
|
||||||
|
|
||||||
|
$context = [];
|
||||||
if ($di['auth']->isAdminLoggedIn()) {
|
if ($di['auth']->isAdminLoggedIn()) {
|
||||||
$admin = $di['loggedin_admin'];
|
$admin = $di['loggedin_admin'];
|
||||||
$log->setEventItem('admin_id', $admin->getId());
|
$context['admin_id'] = $admin->getId();
|
||||||
} elseif ($di['auth']->isClientLoggedIn()) {
|
} elseif ($di['auth']->isClientLoggedIn()) {
|
||||||
$client = $di['loggedin_client'];
|
$client = $di['loggedin_client'];
|
||||||
$log->setEventItem('client_id', $client->getId());
|
$context['client_id'] = $client->getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
$monolog = new FOSSBilling\Monolog();
|
$monolog = new FOSSBilling\Monolog();
|
||||||
$log->addWriter($monolog);
|
$log->addWriter($monolog);
|
||||||
|
|
||||||
|
if ($context !== []) {
|
||||||
|
$log = $log->withContext($context);
|
||||||
|
}
|
||||||
|
|
||||||
return $log;
|
return $log;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -67,10 +72,10 @@ $di['logger'] = function () use ($di) {
|
|||||||
*
|
*
|
||||||
* @param void
|
* @param void
|
||||||
*
|
*
|
||||||
* @return \Box_Crypt
|
* @return FOSSBilling\Crypt
|
||||||
*/
|
*/
|
||||||
$di['crypt'] = function () use ($di) {
|
$di['crypt'] = function () use ($di) {
|
||||||
$crypt = new Box_Crypt();
|
$crypt = new FOSSBilling\Crypt();
|
||||||
$crypt->setDi($di);
|
$crypt->setDi($di);
|
||||||
|
|
||||||
return $crypt;
|
return $crypt;
|
||||||
@@ -105,18 +110,16 @@ $di['dbal'] = (fn (): Connection => DriverManagerFactory::getSharedConnection())
|
|||||||
* @return PDO The PDO object used for database connections
|
* @return PDO The PDO object used for database connections
|
||||||
*/
|
*/
|
||||||
$di['pdo'] = function () {
|
$di['pdo'] = function () {
|
||||||
$debugConfig = Config::getProperty('debug_and_monitoring', []);
|
$debug = (bool) Config::getProperty('debug_and_monitoring.debug', false);
|
||||||
|
|
||||||
$pdo = DriverManagerFactory::getSharedConnection()->getNativeConnection();
|
$pdo = DriverManagerFactory::getSharedConnection()->getNativeConnection();
|
||||||
if (!$pdo instanceof PDO) {
|
if (!$pdo instanceof PDO) {
|
||||||
throw new RuntimeException('PDO service must resolve to a PDO instance');
|
throw new RuntimeException('PDO service must resolve to a PDO instance');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($debugConfig['debug']) && $debugConfig['debug']) {
|
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
||||||
$pdo->setAttribute(PDO::ATTR_STATEMENT_CLASS, ['Box_DbLoggedPDOStatement']);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new DebugBar\DataCollector\PDO\TraceablePDO($pdo);
|
return $debug ? new DebugBar\DataCollector\PDO\TraceablePDO($pdo) : $pdo;
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -136,10 +139,10 @@ $di['pager'] = function () use ($di) {
|
|||||||
*
|
*
|
||||||
* @param void
|
* @param void
|
||||||
*
|
*
|
||||||
* @return Box_Url
|
* @return FOSSBilling\Url
|
||||||
*/
|
*/
|
||||||
$di['url'] = function () use ($di) {
|
$di['url'] = function () use ($di) {
|
||||||
$url = new Box_Url();
|
$url = new FOSSBilling\Url();
|
||||||
$url->setDi($di);
|
$url->setDi($di);
|
||||||
$url->setBaseUri(SYSTEM_URL);
|
$url->setBaseUri(SYSTEM_URL);
|
||||||
|
|
||||||
@@ -589,13 +592,13 @@ $di['server_manager'] = $di->protect(function ($manager, $config) use ($di) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Creates a new Box_Period object using the provided period code and returns it.
|
* Creates a new FOSSBilling\Period object using the provided period code and returns it.
|
||||||
*
|
*
|
||||||
* @param string $code The two character period code to create the period object with.
|
* @param string $code The two character period code to create the period object with.
|
||||||
*
|
*
|
||||||
* @return \Box_Period The new period object that was just created.
|
* @return FOSSBilling\Period The new period object that was just created.
|
||||||
*/
|
*/
|
||||||
$di['period'] = $di->protect(fn ($code): Box_Period => new Box_Period($code));
|
$di['period'] = $di->protect(fn (string $code): FOSSBilling\Period => new FOSSBilling\Period($code));
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Gets the current client area theme.
|
* Gets the current client area theme.
|
||||||
@@ -655,14 +658,14 @@ $di['geoip'] = function () use ($di) {
|
|||||||
$di['password'] = fn (): FOSSBilling\PasswordManager => new FOSSBilling\PasswordManager();
|
$di['password'] = fn (): FOSSBilling\PasswordManager => new FOSSBilling\PasswordManager();
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Creates a new Box_Translate object and sets the specified text domain, locale, and other options.
|
* Creates a new FOSSBilling\Translate object and sets the specified text domain, locale, and other options.
|
||||||
*
|
*
|
||||||
* @param string $textDomain The text domain to create the translation object with.
|
* @param string $textDomain The text domain to create the translation object with.
|
||||||
*
|
*
|
||||||
* @return \Box_Translate The new translation object that was just created.
|
* @return FOSSBilling\Translate The new translation object that was just created.
|
||||||
*/
|
*/
|
||||||
$di['translate'] = $di->protect(function ($textDomain = '') use ($di) {
|
$di['translate'] = $di->protect(function ($textDomain = '') use ($di) {
|
||||||
$tr = new Box_Translate();
|
$tr = new FOSSBilling\Translate();
|
||||||
|
|
||||||
if (!empty($textDomain)) {
|
if (!empty($textDomain)) {
|
||||||
$tr->setDomain($textDomain);
|
$tr->setDomain($textDomain);
|
||||||
|
|||||||
@@ -881,7 +881,7 @@ CREATE TABLE `product_payment` (
|
|||||||
CREATE TABLE `product_payment_period` (
|
CREATE TABLE `product_payment_period` (
|
||||||
`id` bigint(20) NOT NULL AUTO_INCREMENT,
|
`id` bigint(20) NOT NULL AUTO_INCREMENT,
|
||||||
`product_payment_id` bigint(20) NOT NULL,
|
`product_payment_id` bigint(20) NOT NULL,
|
||||||
`code` varchar(10) NOT NULL COMMENT 'Box_Period code, e.g. 1M, 3Y, 45D',
|
`code` varchar(10) NOT NULL COMMENT 'Billing period code, e.g. 1M, 3Y, 45D',
|
||||||
`price` decimal(18,2) NOT NULL DEFAULT '0.00',
|
`price` decimal(18,2) NOT NULL DEFAULT '0.00',
|
||||||
`setup_price` decimal(18,2) NOT NULL DEFAULT '0.00',
|
`setup_price` decimal(18,2) NOT NULL DEFAULT '0.00',
|
||||||
`enabled` tinyint(1) NOT NULL DEFAULT '1',
|
`enabled` tinyint(1) NOT NULL DEFAULT '1',
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ class Box_App
|
|||||||
|
|
||||||
public function show404(Exception $e): Response
|
public function show404(Exception $e): Response
|
||||||
{
|
{
|
||||||
$this->di['logger']->setChannel('routing')->info($e->getMessage());
|
$this->di['logger']->withChannel('routing')->info($e->getMessage());
|
||||||
|
|
||||||
return $this->errorResponse($e, 404);
|
return $this->errorResponse($e, 404);
|
||||||
}
|
}
|
||||||
@@ -399,7 +399,7 @@ class Box_App
|
|||||||
protected function checkAdminPrefix(): bool
|
protected function checkAdminPrefix(): bool
|
||||||
{
|
{
|
||||||
$requestPath = $this->getRequestPath();
|
$requestPath = $this->getRequestPath();
|
||||||
$realAdminUrl = rtrim(SYSTEM_URL, '/') . ADMIN_PREFIX;
|
$realAdminUrl = rtrim(SYSTEM_URL, '/') . '/' . ltrim(ADMIN_PREFIX, '/');
|
||||||
$realAdminPath = parse_url($realAdminUrl, PHP_URL_PATH);
|
$realAdminPath = parse_url($realAdminUrl, PHP_URL_PATH);
|
||||||
|
|
||||||
if ($this->pathStartsWith($requestPath, $realAdminPath)) {
|
if ($this->pathStartsWith($requestPath, $realAdminPath)) {
|
||||||
|
|||||||
@@ -76,18 +76,20 @@ class Box_AppClient extends Box_App
|
|||||||
} catch (FOSSBilling\InformationException $e) {
|
} catch (FOSSBilling\InformationException $e) {
|
||||||
// @phpstan-ignore if.alwaysFalse (DEBUG is a runtime constant that may be true during debugging)
|
// @phpstan-ignore if.alwaysFalse (DEBUG is a runtime constant that may be true during debugging)
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
error_log($e->getMessage());
|
$this->di['logger']->withChannel('routing')->debug($e->getMessage());
|
||||||
}
|
}
|
||||||
} catch (Twig\Error\LoaderError|Twig\Error\RuntimeError|Twig\Error\SyntaxError $e) {
|
} catch (Twig\Error\LoaderError|Twig\Error\RuntimeError|Twig\Error\SyntaxError $e) {
|
||||||
// A real template bug, not a missing page. Surface as a 500 so the
|
// A real template bug, not a missing page. Surface as a 500 so the
|
||||||
// next regression of this shape (issue #3818) cannot hide behind a
|
// next regression of this shape (issue #3818) cannot hide behind a
|
||||||
// generic 404.
|
// generic 404.
|
||||||
$this->di['logger']->setChannel('routing')->error(sprintf(
|
$this->di['logger']->withChannel('routing')->error(
|
||||||
'Template rendering failed for "%s" (page "%s"): %s',
|
'Template rendering failed for "{template}" (page "{page}").',
|
||||||
$tpl,
|
[
|
||||||
(string) $page,
|
'template' => $tpl,
|
||||||
$e->getMessage(),
|
'page' => (string) $page,
|
||||||
), ['exception' => $e]);
|
'exception_class' => $e::class,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
$internal = new FOSSBilling\InformationException('The requested page could not be rendered.', [], 500);
|
$internal = new FOSSBilling\InformationException('The requested page could not be rendered.', [], 500);
|
||||||
|
|
||||||
@@ -95,7 +97,7 @@ class Box_AppClient extends Box_App
|
|||||||
}
|
}
|
||||||
$e = new FOSSBilling\InformationException('Page :url not found', [':url' => $this->url], 404);
|
$e = new FOSSBilling\InformationException('Page :url not found', [':url' => $this->url], 404);
|
||||||
|
|
||||||
$this->di['logger']->setChannel('routing')->info($e->getMessage());
|
$this->di['logger']->withChannel('routing')->info($e->getMessage());
|
||||||
|
|
||||||
return $this->errorResponse($e, 404);
|
return $this->errorResponse($e, 404);
|
||||||
}
|
}
|
||||||
@@ -109,7 +111,7 @@ class Box_AppClient extends Box_App
|
|||||||
try {
|
try {
|
||||||
$template = $this->getTwig()->load(Path::changeExtension($fileName, $ext));
|
$template = $this->getTwig()->load(Path::changeExtension($fileName, $ext));
|
||||||
} catch (Twig\Error\LoaderError $e) {
|
} catch (Twig\Error\LoaderError $e) {
|
||||||
$this->di['logger']->setChannel('routing')->info($e->getMessage());
|
$this->di['logger']->withChannel('routing')->info($e->getMessage());
|
||||||
|
|
||||||
throw new FOSSBilling\InformationException('Page not found', null, 404);
|
throw new FOSSBilling\InformationException('Page not found', null, 404);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
/**
|
|
||||||
* Copyright 2022-2025 FOSSBilling
|
|
||||||
* SPDX-License-Identifier: Apache-2.0.
|
|
||||||
*
|
|
||||||
* @copyright FOSSBilling (https://www.fossbilling.org)
|
|
||||||
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache-2.0
|
|
||||||
*/
|
|
||||||
class Box_DbLoggedPDOStatement extends PDOStatement
|
|
||||||
{
|
|
||||||
public function execute(?array $input_parameters = null): bool
|
|
||||||
{
|
|
||||||
error_log($this->queryString);
|
|
||||||
|
|
||||||
return parent::execute($input_parameters);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -38,7 +38,11 @@ class Box_EventManager implements FOSSBilling\InjectionAwareInterface
|
|||||||
$subject = $data['subject'] ?? null;
|
$subject = $data['subject'] ?? null;
|
||||||
$params = $data['params'] ?? null;
|
$params = $data['params'] ?? null;
|
||||||
|
|
||||||
$this->di['logger']->setChannel('event')->debug('Fired event: ' . $event, $params);
|
$eventContext = ['event' => $event];
|
||||||
|
if (is_array($params)) {
|
||||||
|
$eventContext['parameters'] = $params;
|
||||||
|
}
|
||||||
|
$this->di['logger']->withChannel('event')->debug('Fired event: {event}', $eventContext);
|
||||||
|
|
||||||
$e = new Box_Event($subject, $event, $params);
|
$e = new Box_Event($subject, $event, $params);
|
||||||
$e->setDi($this->di);
|
$e->setDi($this->di);
|
||||||
@@ -86,7 +90,7 @@ class Box_EventManager implements FOSSBilling\InjectionAwareInterface
|
|||||||
$disp->connect($dispatchEvent, [$s::class, $event]);
|
$disp->connect($dispatchEvent, [$s::class, $event]);
|
||||||
}
|
}
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
error_log($e->getMessage());
|
$this->di['logger']->withChannel('event')->error($e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,238 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
/**
|
|
||||||
* Copyright 2022-2025 FOSSBilling
|
|
||||||
* SPDX-License-Identifier: Apache-2.0.
|
|
||||||
*
|
|
||||||
* @copyright FOSSBilling (https://www.fossbilling.org)
|
|
||||||
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache-2.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @method void emergency(string $message)
|
|
||||||
* @method void alert(string $message)
|
|
||||||
* @method void critical(string $message)
|
|
||||||
* @method void error(string $message)
|
|
||||||
* @method void warning(string $message)
|
|
||||||
* @method void notice(string $message)
|
|
||||||
* @method void info(string $message)
|
|
||||||
* @method void debug(string $message)
|
|
||||||
* @method void emerg(string $message) Legacy alias for emergency()
|
|
||||||
* @method void crit(string $message) Legacy alias for critical()
|
|
||||||
* @method void err(string $message) Legacy alias for error()
|
|
||||||
* @method void warn(string $message) Legacy alias for warning()
|
|
||||||
*/
|
|
||||||
class Box_Log implements FOSSBilling\InjectionAwareInterface
|
|
||||||
{
|
|
||||||
final public const int EMERG = 0;
|
|
||||||
final public const int ALERT = 1;
|
|
||||||
final public const int CRIT = 2;
|
|
||||||
final public const int ERR = 3;
|
|
||||||
final public const int WARN = 4;
|
|
||||||
final public const int NOTICE = 5;
|
|
||||||
final public const int INFO = 6;
|
|
||||||
final public const int DEBUG = 7;
|
|
||||||
|
|
||||||
protected array $_priorities = [
|
|
||||||
self::EMERG => 'EMERGENCY',
|
|
||||||
self::ALERT => 'ALERT',
|
|
||||||
self::CRIT => 'CRITICAL',
|
|
||||||
self::ERR => 'ERROR',
|
|
||||||
self::WARN => 'WARNING',
|
|
||||||
self::NOTICE => 'NOTICE',
|
|
||||||
self::INFO => 'INFO',
|
|
||||||
self::DEBUG => 'DEBUG',
|
|
||||||
];
|
|
||||||
|
|
||||||
private const array PRIORITY_ALIASES = [
|
|
||||||
'EMERG' => 'EMERGENCY',
|
|
||||||
'CRIT' => 'CRITICAL',
|
|
||||||
'ERR' => 'ERROR',
|
|
||||||
'WARN' => 'WARNING',
|
|
||||||
];
|
|
||||||
|
|
||||||
protected ?Pimple\Container $di = null;
|
|
||||||
protected $_min_priority;
|
|
||||||
|
|
||||||
protected array $_writers = [];
|
|
||||||
protected array $_extras = [];
|
|
||||||
protected string $_channel = 'application';
|
|
||||||
|
|
||||||
private array $_maskedKeys = ['password', 'pass', 'token', 'key', 'apisecret', 'secret', 'api_token'];
|
|
||||||
|
|
||||||
public function setDi(Pimple\Container $di): void
|
|
||||||
{
|
|
||||||
$this->di = $di;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getDi(): ?Pimple\Container
|
|
||||||
{
|
|
||||||
return $this->di;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function maskParams(mixed $params, int $depthLimit = 15): mixed
|
|
||||||
{
|
|
||||||
if (!is_array($params)) {
|
|
||||||
return $params;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($depthLimit <= 0) {
|
|
||||||
return 'Recursion limit reached while masking event log parameters';
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($params as $key => $value) {
|
|
||||||
if (in_array(strtolower((string) $key), $this->_maskedKeys)) {
|
|
||||||
$params[$key] = '********';
|
|
||||||
} elseif (is_array($value)) {
|
|
||||||
$params[$key] = $this->maskParams($value, $depthLimit - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $params;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws FOSSBilling\Exception
|
|
||||||
*/
|
|
||||||
public function __call($method, $params): void
|
|
||||||
{
|
|
||||||
$priority = strtoupper((string) $method);
|
|
||||||
$priority = self::PRIORITY_ALIASES[$priority] ?? $priority;
|
|
||||||
if (($priority = array_search($priority, $this->_priorities, true)) !== false) {
|
|
||||||
switch (FOSSBilling\Tools::safeCount($params)) {
|
|
||||||
case 0:
|
|
||||||
throw new FOSSBilling\Exception('Missing log message');
|
|
||||||
case 1:
|
|
||||||
$message = array_shift($params);
|
|
||||||
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
$message = array_shift($params);
|
|
||||||
$params = $this->maskParams($params);
|
|
||||||
|
|
||||||
$message = vsprintf($message, array_values($params));
|
|
||||||
if (!$message) {
|
|
||||||
throw new FOSSBilling\Exception('Number of placeholders does not match number of variables');
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
$this->log($message, $priority, $params);
|
|
||||||
} else {
|
|
||||||
throw new FOSSBilling\Exception('Bad log priority');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws FOSSBilling\Exception
|
|
||||||
*/
|
|
||||||
public function log($message, $priority, array|string|null $extras = null): void
|
|
||||||
{
|
|
||||||
// sanity checks
|
|
||||||
if (!isset($this->_priorities[$priority])) {
|
|
||||||
throw new FOSSBilling\Exception('Bad log priority');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($this->_writers)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->_min_priority && $priority > $this->_min_priority) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$event = $this->_packEvent($message, $priority);
|
|
||||||
$extras = $this->maskParams($extras);
|
|
||||||
|
|
||||||
// Check to see if any extra information was passed
|
|
||||||
if (!empty($extras)) {
|
|
||||||
$info = [];
|
|
||||||
if (is_array($extras)) {
|
|
||||||
foreach ($extras as $key => $value) {
|
|
||||||
if (is_string($key)) {
|
|
||||||
if (!array_key_exists($key, $event)) {
|
|
||||||
$event[$key] = $value;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$info[] = $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$info = $extras;
|
|
||||||
}
|
|
||||||
if (!empty($info)) {
|
|
||||||
$event['info'] = $info;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// do not log debug level messages if debug is OFF
|
|
||||||
// @phpstan-ignore identical.alwaysTrue (DEBUG is a runtime constant that may be true during debugging)
|
|
||||||
if ($event['priority'] > self::INFO && DEBUG === false) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// send to each writer
|
|
||||||
foreach ($this->_writers as $writer) {
|
|
||||||
try {
|
|
||||||
$writer->write($event, $this->_channel);
|
|
||||||
} catch (Throwable $e) {
|
|
||||||
error_log(sprintf('[Box_Log] writer failure: %s at %s:%d', $e->getMessage(), $e->getFile(), $e->getLine()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function _packEvent($message, $priority): array
|
|
||||||
{
|
|
||||||
return ['timestamp' => date('Y-m-d H:i:s'), 'message' => $message, 'priority' => $priority, 'priorityName' => $this->_priorities[$priority], ...$this->_extras];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param Box_LogDb|FOSSBilling\Monolog $writer
|
|
||||||
*
|
|
||||||
* @return $this The Box_Log instance
|
|
||||||
*/
|
|
||||||
public function addWriter($writer): static
|
|
||||||
{
|
|
||||||
$this->_writers[] = $writer;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param $name string
|
|
||||||
* @param $value mixed
|
|
||||||
*
|
|
||||||
* @return $this The Box_Log instance
|
|
||||||
*/
|
|
||||||
public function setEventItem(string $name, mixed $value): static
|
|
||||||
{
|
|
||||||
$this->_extras = array_merge($this->_extras, [$name => $value]);
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the channel name for the logger.
|
|
||||||
*
|
|
||||||
* @param string $channel Channel name
|
|
||||||
*
|
|
||||||
* @return $this The Box_Log instance
|
|
||||||
*/
|
|
||||||
public function setChannel(string $channel): static
|
|
||||||
{
|
|
||||||
$this->_channel = $channel;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return $this The Box_Log instance
|
|
||||||
*/
|
|
||||||
public function setMinPriority($priority): static
|
|
||||||
{
|
|
||||||
$this->_min_priority = $priority;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
/**
|
|
||||||
* Copyright 2022-2025 FOSSBilling
|
|
||||||
* SPDX-License-Identifier: Apache-2.0.
|
|
||||||
*
|
|
||||||
* @copyright FOSSBilling (https://www.fossbilling.org)
|
|
||||||
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache-2.0
|
|
||||||
*/
|
|
||||||
class Box_LogDb
|
|
||||||
{
|
|
||||||
private array $ignoredChannels = ['billing', 'routing', 'security', 'email'];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class constructor.
|
|
||||||
*
|
|
||||||
* @param object|string $service - module service class object or class name
|
|
||||||
*/
|
|
||||||
public function __construct(protected object|string $service)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write a message to the log.
|
|
||||||
*/
|
|
||||||
public function write(array $event, string $channel = 'application'): void
|
|
||||||
{
|
|
||||||
// TODO: Temporary! Redo logging stuff in more depth for a major release.
|
|
||||||
if (in_array($channel, $this->ignoredChannels)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (method_exists($this->service, 'logEvent')) {
|
|
||||||
$this->service->logEvent($event);
|
|
||||||
}
|
|
||||||
} catch (Exception $e) {
|
|
||||||
error_log($e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
/**
|
|
||||||
* Copyright 2022-2025 FOSSBilling
|
|
||||||
* SPDX-License-Identifier: Apache-2.0.
|
|
||||||
*
|
|
||||||
* @copyright FOSSBilling (https://www.fossbilling.org)
|
|
||||||
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache-2.0
|
|
||||||
*/
|
|
||||||
if (!function_exists('__trans')) {
|
|
||||||
/**
|
|
||||||
* Translate a message with optional value substitution.
|
|
||||||
*
|
|
||||||
* @param string $msgid The message to translate
|
|
||||||
* @param array|null $values Optional values to substitute in the translated message
|
|
||||||
*
|
|
||||||
* @return string The translated message
|
|
||||||
*/
|
|
||||||
function __trans(string $msgid, ?array $values = null): string
|
|
||||||
{
|
|
||||||
$translated = _gettext($msgid);
|
|
||||||
|
|
||||||
if (is_array($values)) {
|
|
||||||
$translated = strtr($translated, $values);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $translated;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!function_exists('__pluralTrans')) {
|
|
||||||
/**
|
|
||||||
* Translate a plural message with optional value substitution.
|
|
||||||
*
|
|
||||||
* @param string $msgid The singular message to translate
|
|
||||||
* @param string $msgidPlural The plural message to translate
|
|
||||||
* @param int $number The number to determine which form to use
|
|
||||||
* @param array|null $values Optional values to substitute in the translated message
|
|
||||||
*
|
|
||||||
* @return string The translated message
|
|
||||||
*/
|
|
||||||
function __pluralTrans(string $msgid, string $msgidPlural, int $number, ?array $values = null): string
|
|
||||||
{
|
|
||||||
$translated = _ngettext($msgid, $msgidPlural, $number);
|
|
||||||
|
|
||||||
if (is_array($values)) {
|
|
||||||
$translated = strtr($translated, $values);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $translated;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Box_Translate
|
|
||||||
{
|
|
||||||
protected $domain = 'messages';
|
|
||||||
|
|
||||||
protected $locale = 'en_US';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getLocale()
|
|
||||||
{
|
|
||||||
return $this->locale;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $locale
|
|
||||||
*
|
|
||||||
* @return Box_Translate
|
|
||||||
*/
|
|
||||||
public function setLocale($locale)
|
|
||||||
{
|
|
||||||
$this->locale = $locale;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setup(): void
|
|
||||||
{
|
|
||||||
PhpMyAdmin\MoTranslator\Loader::loadFunctions();
|
|
||||||
|
|
||||||
$locale = $this->getLocale();
|
|
||||||
if (empty($locale)) {
|
|
||||||
// We are using the standard PHP Exception here rather than our custom one as ours requires translations to be setup, which we cannot do without the locale being defined.
|
|
||||||
throw new Exception('Unable to set up FOSSBilling translation functionality, locale was undefined.');
|
|
||||||
}
|
|
||||||
|
|
||||||
Locale::setDefault($locale);
|
|
||||||
|
|
||||||
$codeset = 'UTF-8';
|
|
||||||
// set locale
|
|
||||||
if (!defined('LC_MESSAGES')) {
|
|
||||||
define('LC_MESSAGES', 5);
|
|
||||||
}
|
|
||||||
if (!defined('LC_TIME')) {
|
|
||||||
define('LC_TIME', 2);
|
|
||||||
}
|
|
||||||
_setlocale(LC_MESSAGES, $locale . '.' . $codeset);
|
|
||||||
_setlocale(LC_TIME, $locale . '.' . $codeset);
|
|
||||||
_bindtextdomain($this->domain, PATH_LANGS);
|
|
||||||
_bind_textdomain_codeset($this->domain, $codeset);
|
|
||||||
_textdomain($this->domain);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getDomain()
|
|
||||||
{
|
|
||||||
return $this->domain;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return Box_Translate
|
|
||||||
*/
|
|
||||||
public function setDomain($domain)
|
|
||||||
{
|
|
||||||
$this->domain = $domain;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -28,8 +28,7 @@ class AbstractApi implements InjectionAwareInterface
|
|||||||
*/
|
*/
|
||||||
protected $mod;
|
protected $mod;
|
||||||
|
|
||||||
// TODO: Find a way to correctly set the type. Maybe a module's service should extend a "Service" class?
|
protected ?object $service = null;
|
||||||
protected $service;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var \Box\Mod\Client\Entity\Client|\Box\Mod\Staff\Entity\Admin|\FOSSBilling\Identity\Guest
|
* @var \Box\Mod\Client\Entity\Client|\Box\Mod\Staff\Entity\Admin|\FOSSBilling\Identity\Guest
|
||||||
@@ -85,15 +84,17 @@ class AbstractApi implements InjectionAwareInterface
|
|||||||
return $this->identity;
|
return $this->identity;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Find a way to correctly set the type. Maybe a module's service should extend a "Service" class?
|
public function setService(object $service): void
|
||||||
public function setService($service): void
|
|
||||||
{
|
{
|
||||||
$this->service = $service;
|
$this->service = $service;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Find a way to correctly set the type. Maybe a module's service should extend a "Service" class?
|
public function getService(): object
|
||||||
public function getService()
|
|
||||||
{
|
{
|
||||||
|
if ($this->service === null) {
|
||||||
|
throw new Exception('Service object is not set for the API');
|
||||||
|
}
|
||||||
|
|
||||||
return $this->service;
|
return $this->service;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,4 +119,14 @@ class AbstractApi implements InjectionAwareInterface
|
|||||||
{
|
{
|
||||||
$this->getDi()['mod_service']('Staff')->checkPermissionsAndThrowException($module, $key, $constraint, $this->identity);
|
$this->getDi()['mod_service']('Staff')->checkPermissionsAndThrowException($module, $key, $constraint, $this->identity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function checkCaptchaIfEnabled(array $data): void
|
||||||
|
{
|
||||||
|
$extensionService = $this->getDi()['mod_service']('extension');
|
||||||
|
if (!$extensionService->isExtensionActive('mod', 'antispam')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->getDi()['mod_service']('Antispam')->checkCaptcha($data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -112,7 +112,7 @@ class CentralAlerts implements InjectionAwareInterface
|
|||||||
]);
|
]);
|
||||||
$json = $response->toArray();
|
$json = $response->toArray();
|
||||||
} catch (TransportExceptionInterface|HttpExceptionInterface $e) {
|
} catch (TransportExceptionInterface|HttpExceptionInterface $e) {
|
||||||
error_log($e->getMessage());
|
$this->di['logger']->error($e->getMessage());
|
||||||
|
|
||||||
throw new Exception('Unable to fetch alerts from Central Alerts System. See the error log for more information.', null);
|
throw new Exception('Unable to fetch alerts from Central Alerts System. See the error log for more information.', null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,11 +9,13 @@ declare(strict_types=1);
|
|||||||
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache-2.0
|
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use FOSSBilling\Config;
|
namespace FOSSBilling;
|
||||||
|
|
||||||
class Box_Crypt implements FOSSBilling\InjectionAwareInterface
|
use Pimple\Container;
|
||||||
|
|
||||||
|
class Crypt implements InjectionAwareInterface
|
||||||
{
|
{
|
||||||
protected ?Pimple\Container $di = null;
|
protected ?Container $di = null;
|
||||||
|
|
||||||
final public const string METHOD = 'aes-256-cbc';
|
final public const string METHOD = 'aes-256-cbc';
|
||||||
final public const string CURRENT_FORMAT_PREFIX = 'v2:';
|
final public const string CURRENT_FORMAT_PREFIX = 'v2:';
|
||||||
@@ -21,16 +23,16 @@ class Box_Crypt implements FOSSBilling\InjectionAwareInterface
|
|||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
if (!extension_loaded('openssl')) {
|
if (!extension_loaded('openssl')) {
|
||||||
throw new FOSSBilling\Exception('The PHP OpenSSL extension must be enabled on your server');
|
throw new Exception('The PHP OpenSSL extension must be enabled on your server');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setDi(Pimple\Container $di): void
|
public function setDi(Container $di): void
|
||||||
{
|
{
|
||||||
$this->di = $di;
|
$this->di = $di;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getDi(): ?Pimple\Container
|
public function getDi(): ?Container
|
||||||
{
|
{
|
||||||
return $this->di;
|
return $this->di;
|
||||||
}
|
}
|
||||||
@@ -40,9 +42,9 @@ class Box_Crypt implements FOSSBilling\InjectionAwareInterface
|
|||||||
return self::CURRENT_FORMAT_PREFIX . $this->encryptWithKey($text, $this->getCurrentKey($pass));
|
return self::CURRENT_FORMAT_PREFIX . $this->encryptWithKey($text, $this->getCurrentKey($pass));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function decrypt(?string $text, ?string $pass = null)
|
public function decrypt(?string $text, ?string $pass = null): string|false
|
||||||
{
|
{
|
||||||
if (is_null($text)) {
|
if ($text === null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,8 +64,8 @@ class Box_Crypt implements FOSSBilling\InjectionAwareInterface
|
|||||||
|
|
||||||
private function encryptWithKey(string $text, string $key): string
|
private function encryptWithKey(string $text, string $key): string
|
||||||
{
|
{
|
||||||
$ivsize = openssl_cipher_iv_length(self::METHOD);
|
$ivSize = openssl_cipher_iv_length(self::METHOD);
|
||||||
$iv = openssl_random_pseudo_bytes($ivsize);
|
$iv = random_bytes($ivSize);
|
||||||
|
|
||||||
$ciphertext = openssl_encrypt(
|
$ciphertext = openssl_encrypt(
|
||||||
$text,
|
$text,
|
||||||
@@ -73,6 +75,10 @@ class Box_Crypt implements FOSSBilling\InjectionAwareInterface
|
|||||||
$iv
|
$iv
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if ($ciphertext === false) {
|
||||||
|
throw new \RuntimeException('Unable to encrypt data.');
|
||||||
|
}
|
||||||
|
|
||||||
return base64_encode($iv . $ciphertext);
|
return base64_encode($iv . $ciphertext);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,9 +89,13 @@ class Box_Crypt implements FOSSBilling\InjectionAwareInterface
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$ivsize = openssl_cipher_iv_length(self::METHOD);
|
$ivSize = openssl_cipher_iv_length(self::METHOD);
|
||||||
$iv = mb_substr($decoded, 0, $ivsize, '8bit');
|
if (strlen($decoded) <= $ivSize) {
|
||||||
$ciphertext = mb_substr($decoded, $ivsize, null, '8bit');
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$iv = substr($decoded, 0, $ivSize);
|
||||||
|
$ciphertext = substr($decoded, $ivSize);
|
||||||
|
|
||||||
$result = openssl_decrypt(
|
$result = openssl_decrypt(
|
||||||
$ciphertext,
|
$ciphertext,
|
||||||
@@ -133,6 +143,6 @@ class Box_Crypt implements FOSSBilling\InjectionAwareInterface
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return !preg_match('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', $text);
|
return preg_match('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', $text) !== 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -37,9 +37,9 @@ class Exception extends \Exception
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (DEBUG && $logStack && !Environment::isTesting()) {
|
if (DEBUG && $logStack && !Environment::isTesting()) {
|
||||||
error_log("Exception: $message");
|
// Exceptions can be created before the DI container and its logger
|
||||||
error_log('Stack trace:');
|
// exist, so keep this diagnostic on PHP's fallback logger.
|
||||||
error_log($this->stackTrace($stackLength, $protected));
|
error_log(sprintf("Exception: %s\nStack trace:\n%s", $message, $this->stackTrace($stackLength, $protected)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass the message to the parent
|
// Pass the message to the parent
|
||||||
|
|||||||
@@ -178,12 +178,16 @@ class Fingerprint
|
|||||||
$percentageWrong = round($percentageWrong * 100, 3);
|
$percentageWrong = round($percentageWrong * 100, 3);
|
||||||
$failureThreshold = round($failureThreshold * 100, 3);
|
$failureThreshold = round($failureThreshold * 100, 3);
|
||||||
|
|
||||||
error_log("The session with the ID '$ID' failed its fingerprint check with a (weighted) difference of $percentageWrong% compared to the allowed $failureThreshold%. $itemCount properties were used in the check.");
|
// Fingerprint is intentionally dependency-free, so retain PHP's
|
||||||
$output = PHP_EOL;
|
// fallback logger. Never write the session token itself to logs.
|
||||||
foreach ($differing as $name) {
|
error_log(sprintf(
|
||||||
$output .= ' ' . $name . PHP_EOL;
|
'Fingerprint check failed for session %s: weighted difference %.3f%% (allowed %.3f%%), %d properties compared; differing properties: %s',
|
||||||
}
|
hash('sha256', $ID),
|
||||||
error_log('The following properties differed:' . $output);
|
$percentageWrong,
|
||||||
|
$failureThreshold,
|
||||||
|
$itemCount,
|
||||||
|
implode(', ', $differing),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
return $valid;
|
return $valid;
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ class Reader
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
error_log("There was an error while updating the IP address database: {$e->getMessage()}.");
|
$this->di['logger']->error("There was an error while updating the IP address database: {$e->getMessage()}.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,208 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copyright 2022-2026 FOSSBilling
|
||||||
|
* SPDX-License-Identifier: Apache-2.0.
|
||||||
|
*
|
||||||
|
* @copyright FOSSBilling (https://www.fossbilling.org)
|
||||||
|
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace FOSSBilling;
|
||||||
|
|
||||||
|
use Pimple\Container;
|
||||||
|
use Psr\Log\AbstractLogger;
|
||||||
|
use Psr\Log\InvalidArgumentException;
|
||||||
|
use Psr\Log\LogLevel;
|
||||||
|
|
||||||
|
class Logger extends AbstractLogger implements InjectionAwareInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Numeric priorities are retained because they are persisted in the
|
||||||
|
* activity_system table and consumed by the file writer.
|
||||||
|
*/
|
||||||
|
final public const int EMERG = 0;
|
||||||
|
final public const int ALERT = 1;
|
||||||
|
final public const int CRIT = 2;
|
||||||
|
final public const int ERR = 3;
|
||||||
|
final public const int WARN = 4;
|
||||||
|
final public const int NOTICE = 5;
|
||||||
|
final public const int INFO = 6;
|
||||||
|
final public const int DEBUG = 7;
|
||||||
|
|
||||||
|
private const array PRIORITY_NAMES = [
|
||||||
|
self::EMERG => 'EMERGENCY',
|
||||||
|
self::ALERT => 'ALERT',
|
||||||
|
self::CRIT => 'CRITICAL',
|
||||||
|
self::ERR => 'ERROR',
|
||||||
|
self::WARN => 'WARNING',
|
||||||
|
self::NOTICE => 'NOTICE',
|
||||||
|
self::INFO => 'INFO',
|
||||||
|
self::DEBUG => 'DEBUG',
|
||||||
|
];
|
||||||
|
|
||||||
|
private const array LEVEL_PRIORITIES = [
|
||||||
|
LogLevel::EMERGENCY => self::EMERG,
|
||||||
|
LogLevel::ALERT => self::ALERT,
|
||||||
|
LogLevel::CRITICAL => self::CRIT,
|
||||||
|
LogLevel::ERROR => self::ERR,
|
||||||
|
LogLevel::WARNING => self::WARN,
|
||||||
|
LogLevel::NOTICE => self::NOTICE,
|
||||||
|
LogLevel::INFO => self::INFO,
|
||||||
|
LogLevel::DEBUG => self::DEBUG,
|
||||||
|
];
|
||||||
|
|
||||||
|
private const array MASKED_KEYS = ['password', 'pass', 'token', 'key', 'apisecret', 'secret', 'api_token'];
|
||||||
|
|
||||||
|
private ?Container $di = null;
|
||||||
|
|
||||||
|
/** @var array<int, object> */
|
||||||
|
private array $writers = [];
|
||||||
|
|
||||||
|
/** @var array<string, mixed> */
|
||||||
|
private array $context = [];
|
||||||
|
|
||||||
|
private string $channel = 'application';
|
||||||
|
|
||||||
|
public function setDi(Container $di): void
|
||||||
|
{
|
||||||
|
$this->di = $di;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDi(): ?Container
|
||||||
|
{
|
||||||
|
return $this->di;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function log($level, string|\Stringable $message, array $context = []): void
|
||||||
|
{
|
||||||
|
$levelName = is_string($level)
|
||||||
|
? $level
|
||||||
|
: (is_scalar($level) ? (string) $level : get_debug_type($level));
|
||||||
|
$priority = self::LEVEL_PRIORITIES[$levelName] ?? null;
|
||||||
|
if ($priority === null) {
|
||||||
|
throw new InvalidArgumentException(sprintf('Unknown log level "%s"', $levelName));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->writePsrEvent($priority, $message, $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a writer to this logger.
|
||||||
|
*
|
||||||
|
* A writer must expose write(array $event, string $channel): void.
|
||||||
|
*/
|
||||||
|
public function addWriter(object $writer): static
|
||||||
|
{
|
||||||
|
$this->writers[] = $writer;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a logger with additional context scoped to that instance.
|
||||||
|
*
|
||||||
|
* The container logger is shared, so operation-specific metadata must not
|
||||||
|
* be added to it directly or it can leak into later operations.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $context
|
||||||
|
*/
|
||||||
|
public function withContext(array $context): static
|
||||||
|
{
|
||||||
|
$logger = clone $this;
|
||||||
|
$logger->context = [...$this->context, ...$context];
|
||||||
|
|
||||||
|
return $logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withChannel(string $channel): static
|
||||||
|
{
|
||||||
|
$logger = clone $this;
|
||||||
|
$logger->channel = $channel;
|
||||||
|
|
||||||
|
return $logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function writePsrEvent(int $priority, string|\Stringable $message, array $context): void
|
||||||
|
{
|
||||||
|
$context = $this->maskContext($context);
|
||||||
|
$message = $this->interpolate((string) $message, $context);
|
||||||
|
|
||||||
|
$this->writeEvent($message, $priority, $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string|int, mixed> $context
|
||||||
|
*
|
||||||
|
* @return array<string|int, mixed>
|
||||||
|
*/
|
||||||
|
private function maskContext(array $context, int $depthLimit = 15): array
|
||||||
|
{
|
||||||
|
if ($depthLimit <= 0) {
|
||||||
|
return ['error' => 'Recursion limit reached while masking event log parameters'];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($context as $key => $value) {
|
||||||
|
if (in_array(strtolower((string) $key), self::MASKED_KEYS, true)) {
|
||||||
|
$context[$key] = '********';
|
||||||
|
} elseif (is_array($value)) {
|
||||||
|
$context[$key] = $this->maskContext($value, $depthLimit - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $context;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function interpolate(string $message, array $context): string
|
||||||
|
{
|
||||||
|
$replacements = [];
|
||||||
|
foreach ($context as $key => $value) {
|
||||||
|
if ($value === null || is_scalar($value) || $value instanceof \Stringable) {
|
||||||
|
$replacements['{' . $key . '}'] = (string) $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strtr($message, $replacements);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string|int, mixed> $context
|
||||||
|
*/
|
||||||
|
private function writeEvent(string $message, int $priority, array $context): void
|
||||||
|
{
|
||||||
|
if ($this->writers === []) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scopedContext = $this->maskContext($this->context);
|
||||||
|
$event = [
|
||||||
|
'timestamp' => date('Y-m-d H:i:s'),
|
||||||
|
'message' => $message,
|
||||||
|
'priority' => $priority,
|
||||||
|
'priorityName' => self::PRIORITY_NAMES[$priority],
|
||||||
|
...$scopedContext,
|
||||||
|
];
|
||||||
|
|
||||||
|
$writerContext = $this->maskContext([...$this->context, ...$context]);
|
||||||
|
if ($writerContext !== []) {
|
||||||
|
$event['info'] = $writerContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not log debug level messages if debug is OFF.
|
||||||
|
// @phpstan-ignore identical.alwaysTrue (DEBUG is a runtime constant that may be true during debugging)
|
||||||
|
if ($priority > self::INFO && DEBUG === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->writers as $writer) {
|
||||||
|
try {
|
||||||
|
$writer->write($event, $this->channel);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// A writer failure cannot be routed through this logger without recursion.
|
||||||
|
error_log(sprintf('[FOSSBilling\\Logger] writer failure: %s at %s:%d', $e->getMessage(), $e->getFile(), $e->getLine()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copyright 2022-2026 FOSSBilling
|
||||||
|
* SPDX-License-Identifier: Apache-2.0.
|
||||||
|
*
|
||||||
|
* @copyright FOSSBilling (https://www.fossbilling.org)
|
||||||
|
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace FOSSBilling\Logging;
|
||||||
|
|
||||||
|
class DatabaseWriter
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* These channels are kept in file logs only. Email has its own activity
|
||||||
|
* table, while the remaining channels contain sensitive or operational
|
||||||
|
* diagnostics rather than user-facing activity entries.
|
||||||
|
*/
|
||||||
|
private const array IGNORED_CHANNELS = ['billing', 'routing', 'security', 'email'];
|
||||||
|
|
||||||
|
public function __construct(private object $service)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write a message to the activity log.
|
||||||
|
*
|
||||||
|
* @param array<string|int, mixed> $event
|
||||||
|
*/
|
||||||
|
public function write(array $event, string $channel = 'application'): void
|
||||||
|
{
|
||||||
|
if (in_array($channel, self::IGNORED_CHANNELS, true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (method_exists($this->service, 'logEvent')) {
|
||||||
|
$this->service->logEvent($event);
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// The database writer cannot use the application logger while it is
|
||||||
|
// writing that logger, so retain PHP's last-resort error log here.
|
||||||
|
error_log(sprintf('[FOSSBilling\\Logging\\DatabaseWriter] writer failure: %s at %s:%d', $e->getMessage(), $e->getFile(), $e->getLine()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@ class Monolog
|
|||||||
'activity',
|
'activity',
|
||||||
'application',
|
'application',
|
||||||
'cron',
|
'cron',
|
||||||
|
'update',
|
||||||
'database',
|
'database',
|
||||||
'license',
|
'license',
|
||||||
'mail',
|
'mail',
|
||||||
@@ -49,7 +50,7 @@ class Monolog
|
|||||||
$this->logger[$channel]->pushHandler($rotatingHandler);
|
$this->logger[$channel]->pushHandler($rotatingHandler);
|
||||||
|
|
||||||
$formatter = new LineFormatter($this->outputFormat, $this->dateFormat, true, true, true);
|
$formatter = new LineFormatter($this->outputFormat, $this->dateFormat, true, true, true);
|
||||||
$this->logger[$channel]->getHandlers()[0]->setFormatter($formatter);
|
$rotatingHandler->setFormatter($formatter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,14 +69,14 @@ class Monolog
|
|||||||
{
|
{
|
||||||
// Map numeric priority to Monolog Level
|
// Map numeric priority to Monolog Level
|
||||||
return match ($priority) {
|
return match ($priority) {
|
||||||
\Box_Log::EMERG => Level::Emergency,
|
\FOSSBilling\Logger::EMERG => Level::Emergency,
|
||||||
\Box_Log::ALERT => Level::Alert,
|
\FOSSBilling\Logger::ALERT => Level::Alert,
|
||||||
\Box_Log::CRIT => Level::Critical,
|
\FOSSBilling\Logger::CRIT => Level::Critical,
|
||||||
\Box_Log::ERR => Level::Error,
|
\FOSSBilling\Logger::ERR => Level::Error,
|
||||||
\Box_Log::WARN => Level::Warning,
|
\FOSSBilling\Logger::WARN => Level::Warning,
|
||||||
\Box_Log::NOTICE => Level::Notice,
|
\FOSSBilling\Logger::NOTICE => Level::Notice,
|
||||||
\Box_Log::INFO => Level::Info,
|
\FOSSBilling\Logger::INFO => Level::Info,
|
||||||
\Box_Log::DEBUG => Level::Debug,
|
\FOSSBilling\Logger::DEBUG => Level::Debug,
|
||||||
default => Level::Debug,
|
default => Level::Debug,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -88,8 +89,10 @@ class Monolog
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
$this->getChannel($channel)->log($priority, $message, $context);
|
$this->getChannel($channel)->log($priority, $message, $context);
|
||||||
} catch (\Exception $e) {
|
} catch (\Throwable $e) {
|
||||||
error_log($e->getMessage());
|
// This is the final fallback when a Monolog handler itself fails;
|
||||||
|
// routing it through the application logger would recurse.
|
||||||
|
error_log(sprintf('[FOSSBilling\\Monolog] writer failure: %s at %s:%d', $e->getMessage(), $e->getFile(), $e->getLine()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,10 @@ declare(strict_types=1);
|
|||||||
* @copyright FOSSBilling (https://www.fossbilling.org)
|
* @copyright FOSSBilling (https://www.fossbilling.org)
|
||||||
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache-2.0
|
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache-2.0
|
||||||
*/
|
*/
|
||||||
class Box_Period
|
|
||||||
|
namespace FOSSBilling;
|
||||||
|
|
||||||
|
class Period
|
||||||
{
|
{
|
||||||
final public const string UNIT_DAY = 'D';
|
final public const string UNIT_DAY = 'D';
|
||||||
final public const string UNIT_WEEK = 'W';
|
final public const string UNIT_WEEK = 'W';
|
||||||
@@ -25,18 +28,11 @@ class Box_Period
|
|||||||
final public const string PERIOD_QUADRENNIAL = '4Y';
|
final public const string PERIOD_QUADRENNIAL = '4Y';
|
||||||
final public const string PERIOD_QUINQUENNIAL = '5Y';
|
final public const string PERIOD_QUINQUENNIAL = '5Y';
|
||||||
|
|
||||||
/**
|
private const array UNIT_RANGES = [
|
||||||
* Predefined periods.
|
self::UNIT_DAY => [1, 90],
|
||||||
*/
|
self::UNIT_WEEK => [1, 52],
|
||||||
protected $_multiplier = [
|
self::UNIT_MONTH => [1, 24],
|
||||||
self::PERIOD_MONTH => 1,
|
self::UNIT_YEAR => [1, 5],
|
||||||
self::PERIOD_QUARTER => 3,
|
|
||||||
self::PERIOD_BIANNUAL => 6,
|
|
||||||
self::PERIOD_ANNUAL => 12,
|
|
||||||
self::PERIOD_BIENNIAL => 24,
|
|
||||||
self::PERIOD_TRIENNIAL => 36,
|
|
||||||
self::PERIOD_QUADRENNIAL => 48,
|
|
||||||
self::PERIOD_QUINQUENNIAL => 60,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
private readonly string $unit;
|
private readonly string $unit;
|
||||||
@@ -46,46 +42,29 @@ class Box_Period
|
|||||||
{
|
{
|
||||||
// A period code is a quantity followed by a single unit letter (e.g. "1M", "45D",
|
// A period code is a quantity followed by a single unit letter (e.g. "1M", "45D",
|
||||||
// "24M"). Quantities are not limited to a single digit, so the code as a whole is
|
// "24M"). Quantities are not limited to a single digit, so the code as a whole is
|
||||||
// not a fixed length; getUnits() below enforces the actual allowed ranges.
|
// not a fixed length; UNIT_RANGES enforces the actual allowed ranges.
|
||||||
if (!preg_match('/^(\d+)([A-Za-z])$/', $code, $matches)) {
|
if (!preg_match('/^(\d+)([A-Za-z])$/', $code, $matches)) {
|
||||||
throw new FOSSBilling\Exception('Invalid period code. Period definition must be a quantity followed by a unit letter');
|
throw new Exception('Invalid period code. Period definition must be a quantity followed by a unit letter');
|
||||||
}
|
}
|
||||||
|
|
||||||
[, $qty, $unit] = $matches;
|
[, $qty, $unit] = $matches;
|
||||||
|
|
||||||
$units = $this->getUnits();
|
|
||||||
$qty = (int) $qty;
|
$qty = (int) $qty;
|
||||||
$unit = strtoupper($unit);
|
$unit = strtoupper($unit);
|
||||||
if (!array_key_exists($unit, $units)) {
|
$range = self::UNIT_RANGES[$unit] ?? null;
|
||||||
throw new FOSSBilling\Exception('Period Error. Unit :unit is not defined', [':unit' => $unit]);
|
|
||||||
|
if ($range === null) {
|
||||||
|
throw new Exception('Period Error. Unit :unit is not defined', [':unit' => $unit]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($qty < $units[$unit][0] || $qty > $units[$unit][1]) {
|
if ($qty < $range[0] || $qty > $range[1]) {
|
||||||
$d = [
|
throw new Exception('Invalid period quantity :qty for unit :unit. Allowed range is from :from to :to', [':qty' => $qty, ':unit' => $unit, ':from' => $range[0], ':to' => $range[1]]);
|
||||||
':qty' => $qty,
|
|
||||||
':unit' => $unit,
|
|
||||||
':from' => $units[$unit][0],
|
|
||||||
':to' => $units[$unit][1],
|
|
||||||
];
|
|
||||||
|
|
||||||
throw new FOSSBilling\Exception('Invalid period quantity :qty for unit :unit. Allowed range is from :from to :to', $d);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->unit = $unit;
|
$this->unit = $unit;
|
||||||
$this->qty = $qty;
|
$this->qty = $qty;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getUnits(): array
|
public static function getPredefined(bool $simple = true): array
|
||||||
{
|
|
||||||
return [
|
|
||||||
self::UNIT_DAY => [1, 90],
|
|
||||||
self::UNIT_WEEK => [1, 52],
|
|
||||||
self::UNIT_MONTH => [1, 24],
|
|
||||||
self::UNIT_YEAR => [1, 5],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getPredefined($simple = true): array
|
|
||||||
{
|
{
|
||||||
$periods = [
|
$periods = [
|
||||||
self::PERIOD_WEEK => ['rec_qty' => 1, 'title' => __trans('Every Week'), 'code' => self::PERIOD_WEEK, 'rec_unit' => self::UNIT_WEEK],
|
self::PERIOD_WEEK => ['rec_qty' => 1, 'title' => __trans('Every Week'), 'code' => self::PERIOD_WEEK, 'rec_unit' => self::UNIT_WEEK],
|
||||||
@@ -99,15 +78,16 @@ class Box_Period
|
|||||||
self::PERIOD_QUINQUENNIAL => ['rec_qty' => 5, 'title' => __trans('Every 5 Years'), 'code' => self::PERIOD_QUINQUENNIAL, 'rec_unit' => self::UNIT_YEAR],
|
self::PERIOD_QUINQUENNIAL => ['rec_qty' => 5, 'title' => __trans('Every 5 Years'), 'code' => self::PERIOD_QUINQUENNIAL, 'rec_unit' => self::UNIT_YEAR],
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($simple) {
|
if (!$simple) {
|
||||||
$new = [];
|
return $periods;
|
||||||
foreach ($periods as $pp) {
|
|
||||||
$new[$pp['code']] = $pp['title'];
|
|
||||||
}
|
|
||||||
$periods = $new;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $periods;
|
$simplePeriods = [];
|
||||||
|
foreach ($periods as $period) {
|
||||||
|
$simplePeriods[$period['code']] = $period['title'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $simplePeriods;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getUnit(): string
|
public function getUnit(): string
|
||||||
@@ -127,51 +107,46 @@ class Box_Period
|
|||||||
|
|
||||||
public function getTitle(): string
|
public function getTitle(): string
|
||||||
{
|
{
|
||||||
$qty = $this->qty;
|
$placeholders = [':number' => $this->qty];
|
||||||
$placeholders = [':number' => $qty];
|
|
||||||
|
|
||||||
return match ($this->unit) {
|
return match ($this->unit) {
|
||||||
self::UNIT_DAY => __pluralTrans('Every :number day', 'Every :number days', $qty, $placeholders),
|
self::UNIT_DAY => __pluralTrans('Every :number day', 'Every :number days', $this->qty, $placeholders),
|
||||||
self::UNIT_WEEK => __pluralTrans('Every :number week', 'Every :number weeks', $qty, $placeholders),
|
self::UNIT_WEEK => __pluralTrans('Every :number week', 'Every :number weeks', $this->qty, $placeholders),
|
||||||
self::UNIT_MONTH => __pluralTrans('Every :number month', 'Every :number months', $qty, $placeholders),
|
self::UNIT_MONTH => __pluralTrans('Every :number month', 'Every :number months', $this->qty, $placeholders),
|
||||||
self::UNIT_YEAR => __pluralTrans('Every :number year', 'Every :number years', $qty, $placeholders),
|
self::UNIT_YEAR => __pluralTrans('Every :number year', 'Every :number years', $this->qty, $placeholders),
|
||||||
default => throw new FOSSBilling\Exception('Unit not defined'),
|
default => throw new Exception('Unit not defined'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getDays()
|
public function getDays(): float|int
|
||||||
{
|
{
|
||||||
return $this->getMonths() * 30;
|
return $this->getMonths() * 30;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How many months $this->unit consists of.
|
* How many months the period consists of.
|
||||||
*
|
|
||||||
* @return int
|
|
||||||
*/
|
*/
|
||||||
public function getMonths()
|
public function getMonths(): float|int
|
||||||
{
|
{
|
||||||
return match ($this->unit) {
|
return match ($this->unit) {
|
||||||
self::UNIT_DAY => $this->qty / 30,
|
self::UNIT_DAY => $this->qty / 30,
|
||||||
self::UNIT_WEEK => $this->qty / 4,
|
self::UNIT_WEEK => $this->qty / 4,
|
||||||
self::UNIT_MONTH => $this->qty,
|
self::UNIT_MONTH => $this->qty,
|
||||||
self::UNIT_YEAR => $this->qty * 12,
|
self::UNIT_YEAR => $this->qty * 12,
|
||||||
default => throw new FOSSBilling\Exception('Unable to get the number of months for :unit', [':unit' => $this->unit]),
|
default => throw new Exception('Unable to get the number of months for :unit', [':unit' => $this->unit]),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getExpirationTime(?int $now = null): int|false
|
public function getExpirationTime(?int $now = null): int|false
|
||||||
{
|
{
|
||||||
if ($now === null) {
|
$now ??= time();
|
||||||
$now = time();
|
|
||||||
}
|
|
||||||
|
|
||||||
$shift = match ($this->unit) {
|
$shift = match ($this->unit) {
|
||||||
self::UNIT_DAY => 'days',
|
self::UNIT_DAY => 'days',
|
||||||
self::UNIT_WEEK => 'weeks',
|
self::UNIT_WEEK => 'weeks',
|
||||||
self::UNIT_MONTH => 'months',
|
self::UNIT_MONTH => 'months',
|
||||||
self::UNIT_YEAR => 'years',
|
self::UNIT_YEAR => 'years',
|
||||||
default => throw new FOSSBilling\Exception('Unit not defined'),
|
default => throw new Exception('Unit not defined'),
|
||||||
};
|
};
|
||||||
|
|
||||||
return strtotime("+$this->qty $shift", $now);
|
return strtotime("+$this->qty $shift", $now);
|
||||||
@@ -170,7 +170,10 @@ class Session implements InjectionAwareInterface
|
|||||||
$storedFingerprint = json_decode((string) $session['fingerprint'], true);
|
$storedFingerprint = json_decode((string) $session['fingerprint'], true);
|
||||||
if (!is_array($storedFingerprint) || !$fingerprint->checkFingerprint($storedFingerprint)) {
|
if (!is_array($storedFingerprint) || !$fingerprint->checkFingerprint($storedFingerprint)) {
|
||||||
$invalid = true;
|
$invalid = true;
|
||||||
error_log("Session ID $sessionID has potentially been hijacked as it failed the fingerprint check. The session has automatically been destroyed.");
|
$this->di['logger']->withChannel('security')->warning(
|
||||||
|
'A session failed the fingerprint check and was automatically destroyed.',
|
||||||
|
['session_id_sha256' => hash('sha256', $sessionID)],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -355,12 +355,15 @@ class Tools
|
|||||||
return $ip;
|
return $ip;
|
||||||
}
|
}
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
error_log(sprintf(
|
$this->di['logger']->error(
|
||||||
'Error fetching external IP from "%s" (%s): %s',
|
'Error fetching external IP from "{service}" ({exception_class}): {exception_message}',
|
||||||
$service,
|
[
|
||||||
$e::class,
|
'service' => $service,
|
||||||
$e->getMessage()
|
'exception_class' => $e::class,
|
||||||
));
|
'exception_message' => $e->getMessage(),
|
||||||
|
'exception' => $e,
|
||||||
|
]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copyright 2022-2025 FOSSBilling
|
||||||
|
* SPDX-License-Identifier: Apache-2.0.
|
||||||
|
*
|
||||||
|
* @copyright FOSSBilling (https://www.fossbilling.org)
|
||||||
|
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace FOSSBilling;
|
||||||
|
|
||||||
|
class Translate
|
||||||
|
{
|
||||||
|
private string $domain = 'messages';
|
||||||
|
|
||||||
|
private string $locale = 'en_US';
|
||||||
|
|
||||||
|
public function getLocale(): string
|
||||||
|
{
|
||||||
|
return $this->locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setLocale(string $locale): self
|
||||||
|
{
|
||||||
|
$this->locale = $locale;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setup(): void
|
||||||
|
{
|
||||||
|
\PhpMyAdmin\MoTranslator\Loader::loadFunctions();
|
||||||
|
|
||||||
|
if ($this->locale === '') {
|
||||||
|
// Our translated exception cannot be used until the locale is configured.
|
||||||
|
throw new \Exception('Unable to set up FOSSBilling translation functionality, locale was undefined.');
|
||||||
|
}
|
||||||
|
|
||||||
|
\Locale::setDefault($this->locale);
|
||||||
|
|
||||||
|
$codeset = 'UTF-8';
|
||||||
|
if (!defined('LC_MESSAGES')) {
|
||||||
|
define('LC_MESSAGES', 5);
|
||||||
|
}
|
||||||
|
if (!defined('LC_TIME')) {
|
||||||
|
define('LC_TIME', 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
\_setlocale(LC_MESSAGES, $this->locale . '.' . $codeset);
|
||||||
|
\_setlocale(LC_TIME, $this->locale . '.' . $codeset);
|
||||||
|
\_bindtextdomain($this->domain, PATH_LANGS);
|
||||||
|
\_bind_textdomain_codeset($this->domain, $codeset);
|
||||||
|
\_textdomain($this->domain);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDomain(): string
|
||||||
|
{
|
||||||
|
return $this->domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDomain(string $domain): self
|
||||||
|
{
|
||||||
|
$this->domain = $domain;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ use FOSSBilling\Twig\Extension\DebugBarExtension;
|
|||||||
use FOSSBilling\Twig\Extension\FOSSBillingExtension;
|
use FOSSBilling\Twig\Extension\FOSSBillingExtension;
|
||||||
use FOSSBilling\Twig\Extension\LegacyExtension;
|
use FOSSBilling\Twig\Extension\LegacyExtension;
|
||||||
use FOSSBilling\Twig\Markdown\FOSSBillingMarkdown;
|
use FOSSBilling\Twig\Markdown\FOSSBillingMarkdown;
|
||||||
|
use FOSSBilling\Url;
|
||||||
use FOSSBilling\Version;
|
use FOSSBilling\Version;
|
||||||
use Symfony\Component\Filesystem\Path;
|
use Symfony\Component\Filesystem\Path;
|
||||||
use Symfony\Component\Intl\Currencies;
|
use Symfony\Component\Intl\Currencies;
|
||||||
@@ -412,7 +413,7 @@ class TwigFactory
|
|||||||
unset($requestData['_url']);
|
unset($requestData['_url']);
|
||||||
|
|
||||||
$requestQuery = $requestData;
|
$requestQuery = $requestData;
|
||||||
$requestPath = \Box_Url::normalizeLinkPath(RequestFactory::getRoutePath($request));
|
$requestPath = Url::normalizeLinkPath(RequestFactory::getRoutePath($request));
|
||||||
$requestHasFilters = count(array_diff_key($requestData, [
|
$requestHasFilters = count(array_diff_key($requestData, [
|
||||||
'page' => true,
|
'page' => true,
|
||||||
'search' => true,
|
'search' => true,
|
||||||
|
|||||||
@@ -136,22 +136,65 @@ class Update implements InjectionAwareInterface
|
|||||||
$branch = (in_array($branch, ['release', 'preview'])) ? $branch : 'release';
|
$branch = (in_array($branch, ['release', 'preview'])) ? $branch : 'release';
|
||||||
|
|
||||||
if ($branch === 'preview') {
|
if ($branch === 'preview') {
|
||||||
|
$key = 'Update.latest_preview_version_info_v1';
|
||||||
|
|
||||||
|
if ($refetch) {
|
||||||
|
$this->di['cache']->delete($key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->di['cache']->get($key, function (ItemInterface $item) {
|
||||||
|
$item->expiresAfter(300);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$response = $this->di['http_client']->request('GET', 'https://api.fossbilling.net/previews/v1/main');
|
||||||
|
$previewInfo = $response->toArray()['result'] ?? null;
|
||||||
|
} catch (TransportExceptionInterface|HttpExceptionInterface $e) {
|
||||||
|
$this->di['logger']->withChannel('update')->error($e->getMessage());
|
||||||
|
|
||||||
|
throw new Exception('Failed to download the latest preview information. Further details are available in the error log.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_array($previewInfo)) {
|
||||||
|
throw new Exception('The previews API returned invalid preview metadata.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$downloadUrl = $previewInfo['download_url'] ?? null;
|
||||||
|
if (!is_string($downloadUrl) || !str_starts_with($downloadUrl, 'https://download.fossbilling.org/')) {
|
||||||
|
throw new Exception('The previews API returned invalid preview metadata.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$shortSha = $previewInfo['short_sha'] ?? null;
|
||||||
|
if (!is_string($shortSha) || $shortSha === '') {
|
||||||
|
$commitSha = $previewInfo['commit_sha'] ?? null;
|
||||||
|
$shortSha = is_string($commitSha) && $commitSha !== '' ? substr($commitSha, 0, 7) : Version::VERSION;
|
||||||
|
}
|
||||||
|
$shortSha = strtolower(trim($shortSha));
|
||||||
|
if ($shortSha !== Version::VERSION && preg_match('/\A[0-9a-f]{7,40}\z/', $shortSha) !== 1) {
|
||||||
|
throw new Exception('The previews API returned an invalid preview commit identifier.');
|
||||||
|
}
|
||||||
|
|
||||||
$currentVersion = Version::VERSION;
|
$currentVersion = Version::VERSION;
|
||||||
$compareLink = "https://github.com/FOSSBilling/FOSSBilling/compare/{$currentVersion}...main";
|
$compareLink = "https://github.com/FOSSBilling/FOSSBilling/compare/{$currentVersion}...main";
|
||||||
$downloadUrl = 'https://fossbilling.org/downloads/preview/';
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'version' => Version::VERSION,
|
'version' => $shortSha,
|
||||||
'download_url' => $downloadUrl,
|
'download_url' => $downloadUrl,
|
||||||
'release_notes' => "Release notes are not available for the preview branch. You can check the latest changes on our [GitHub]({$compareLink}) repository.",
|
'release_notes' => "Release notes are not available for the preview branch. You can check the latest changes on our [GitHub]({$compareLink}) repository.",
|
||||||
'update_type' => 0,
|
'update_type' => 0,
|
||||||
'last_check' => time(),
|
'last_check' => date('Y-m-d H:i:s'),
|
||||||
'next_check' => time() + 3600,
|
'next_check' => date('Y-m-d H:i:s', time() + 300),
|
||||||
'branch' => 'preview',
|
'branch' => 'preview',
|
||||||
'minimum_php_version' => 'unknown',
|
'minimum_php_version' => 'unknown',
|
||||||
|
'digest' => $previewInfo['digest'] ?? null,
|
||||||
|
'commit_sha' => $previewInfo['commit_sha'] ?? null,
|
||||||
|
'short_sha' => $shortSha,
|
||||||
|
'release_date' => $previewInfo['last_modified'] ?? $previewInfo['created_at'] ?? null,
|
||||||
];
|
];
|
||||||
|
});
|
||||||
}
|
}
|
||||||
$key = "Update.latest_{$branch}_version_info";
|
// The response shape changed when the API digest became mandatory; do not reuse
|
||||||
|
// cached metadata created before that contract existed.
|
||||||
|
$key = "Update.latest_{$branch}_version_info_v2";
|
||||||
|
|
||||||
// Delete the cached result to force a refetch
|
// Delete the cached result to force a refetch
|
||||||
if ($refetch) {
|
if ($refetch) {
|
||||||
@@ -167,7 +210,7 @@ class Update implements InjectionAwareInterface
|
|||||||
$response = $httpClient->request('GET', $releaseInfoUrl);
|
$response = $httpClient->request('GET', $releaseInfoUrl);
|
||||||
$releaseInfo = $response->toArray()['result'];
|
$releaseInfo = $response->toArray()['result'];
|
||||||
} catch (TransportExceptionInterface|HttpExceptionInterface $e) {
|
} catch (TransportExceptionInterface|HttpExceptionInterface $e) {
|
||||||
error_log($e->getMessage());
|
$this->di['logger']->withChannel('update')->error($e->getMessage());
|
||||||
|
|
||||||
throw new Exception('Failed to download the latest version information. Further details are available in the error log.');
|
throw new Exception('Failed to download the latest version information. Further details are available in the error log.');
|
||||||
}
|
}
|
||||||
@@ -182,10 +225,66 @@ class Update implements InjectionAwareInterface
|
|||||||
'next_check' => date('Y-m-d H:i:s', time() + 3600),
|
'next_check' => date('Y-m-d H:i:s', time() + 3600),
|
||||||
'branch' => $branch,
|
'branch' => $branch,
|
||||||
'minimum_php_version' => $releaseInfo['minimum_php_version'],
|
'minimum_php_version' => $releaseInfo['minimum_php_version'],
|
||||||
|
'digest' => $releaseInfo['digest'] ?? null,
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the SHA-256 digest for the exact update archive being downloaded.
|
||||||
|
*/
|
||||||
|
private function getArchiveDigest(array $releaseInfo): string
|
||||||
|
{
|
||||||
|
if (!isset($releaseInfo['digest'])) {
|
||||||
|
throw new InformationException('The FOSSBilling update API did not provide a SHA-256 digest. Update canceled for security reasons.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->normalizeSha256Digest($releaseInfo['digest']);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeSha256Digest(mixed $digest): string
|
||||||
|
{
|
||||||
|
if (!is_string($digest)) {
|
||||||
|
throw new InformationException('The FOSSBilling update API provided an invalid SHA-256 digest. Update canceled for security reasons.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$digest = trim($digest);
|
||||||
|
if (str_starts_with(strtolower($digest), 'sha256:')) {
|
||||||
|
$digest = substr($digest, 7);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('/\A[0-9a-fA-F]{64}\z/', $digest) !== 1) {
|
||||||
|
throw new InformationException('The FOSSBilling update API provided an invalid SHA-256 digest. Update canceled for security reasons.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return strtolower($digest);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function validateDownloadedArchive(string $archiveFile, array $releaseInfo): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$expectedDigest = $this->getArchiveDigest($releaseInfo);
|
||||||
|
$actualDigest = hash_file('sha256', $archiveFile);
|
||||||
|
|
||||||
|
if ($actualDigest === false || !hash_equals($expectedDigest, $actualDigest)) {
|
||||||
|
throw new InformationException('The downloaded update archive failed integrity verification. Update canceled for security reasons.');
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->removeDownloadedArchive($archiveFile);
|
||||||
|
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function removeDownloadedArchive(string $archiveFile): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->filesystem->remove($archiveFile);
|
||||||
|
} catch (IOException $e) {
|
||||||
|
$this->di['logger']->withChannel('update')->error($e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if an update is available for the current FOSSBilling version.
|
* Check if an update is available for the current FOSSBilling version.
|
||||||
*
|
*
|
||||||
@@ -194,6 +293,11 @@ class Update implements InjectionAwareInterface
|
|||||||
public function isUpdateAvailable(): bool
|
public function isUpdateAvailable(): bool
|
||||||
{
|
{
|
||||||
$version = $this->getLatestVersion();
|
$version = $this->getLatestVersion();
|
||||||
|
|
||||||
|
if ($this->getUpdateBranch() === 'preview') {
|
||||||
|
return $version !== Version::VERSION;
|
||||||
|
}
|
||||||
|
|
||||||
$result = Version::compareVersion($version);
|
$result = Version::compareVersion($version);
|
||||||
$result = (Version::isPreviewVersion() && $this->getUpdateBranch() === 'release') ? 1 : $result;
|
$result = (Version::isPreviewVersion() && $this->getUpdateBranch() === 'release') ? 1 : $result;
|
||||||
|
|
||||||
@@ -232,7 +336,7 @@ class Update implements InjectionAwareInterface
|
|||||||
$this->filesystem->remove(PATH_CACHE);
|
$this->filesystem->remove(PATH_CACHE);
|
||||||
$this->filesystem->mkdir(PATH_CACHE, 0o755);
|
$this->filesystem->mkdir(PATH_CACHE, 0o755);
|
||||||
} catch (IOException $e) {
|
} catch (IOException $e) {
|
||||||
error_log($e->getMessage());
|
$this->di['logger']->withChannel('update')->error($e->getMessage());
|
||||||
|
|
||||||
throw new Exception('Unable to clear the cache after applying manual update patches. Further details are available in the error log.');
|
throw new Exception('Unable to clear the cache after applying manual update patches. Further details are available in the error log.');
|
||||||
}
|
}
|
||||||
@@ -262,7 +366,7 @@ class Update implements InjectionAwareInterface
|
|||||||
throw new InformationException('You have the latest version of FOSSBilling. You do not need to update.');
|
throw new InformationException('You have the latest version of FOSSBilling. You do not need to update.');
|
||||||
}
|
}
|
||||||
|
|
||||||
error_log('Started FOSSBilling auto-update script');
|
$this->di['logger']->withChannel('update')->info('Started FOSSBilling auto-update script');
|
||||||
$latestVersionNum = $this->getLatestVersion();
|
$latestVersionNum = $this->getLatestVersion();
|
||||||
$archiveFile = Path::join(PATH_CACHE, "{$latestVersionNum}.zip");
|
$archiveFile = Path::join(PATH_CACHE, "{$latestVersionNum}.zip");
|
||||||
|
|
||||||
@@ -292,20 +396,37 @@ class Update implements InjectionAwareInterface
|
|||||||
'timeout' => 30,
|
'timeout' => 30,
|
||||||
'max_duration' => 120,
|
'max_duration' => 120,
|
||||||
]);
|
]);
|
||||||
$response = $httpClient->request('GET', $releaseInfo['download_url']);
|
$downloadOptions = [];
|
||||||
|
if (str_starts_with((string) $releaseInfo['download_url'], 'https://api.github.com/repos/FOSSBilling/FOSSBilling/releases/assets/')) {
|
||||||
|
$downloadOptions['headers'] = ['Accept' => 'application/octet-stream'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = $httpClient->request('GET', $releaseInfo['download_url'], $downloadOptions);
|
||||||
|
|
||||||
$fileHandler = fopen($archiveFile, 'w');
|
$fileHandler = fopen($archiveFile, 'w');
|
||||||
foreach ($httpClient->stream($response) as $chunk) {
|
if ($fileHandler === false) {
|
||||||
fwrite($fileHandler, (string) $chunk->getContent());
|
throw new \RuntimeException('Unable to create the update archive.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
foreach ($httpClient->stream($response) as $chunk) {
|
||||||
|
$content = (string) $chunk->getContent();
|
||||||
|
$written = fwrite($fileHandler, $content);
|
||||||
|
if ($written === false || $written !== strlen($content)) {
|
||||||
|
throw new \RuntimeException('Unable to write the update archive.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
fclose($fileHandler);
|
fclose($fileHandler);
|
||||||
} catch (TransportExceptionInterface|HttpExceptionInterface $e) {
|
}
|
||||||
error_log($e->getMessage());
|
} catch (\Throwable $e) {
|
||||||
|
$this->removeDownloadedArchive($archiveFile);
|
||||||
|
$this->di['logger']->withChannel('update')->error($e->getMessage());
|
||||||
|
|
||||||
throw new Exception('Failed to download the update archive. Further details are available in the error log.');
|
throw new Exception('Failed to download the update archive. Further details are available in the error log.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// @TODO - Validate downloaded file hash.
|
$this->validateDownloadedArchive($archiveFile, $releaseInfo);
|
||||||
|
|
||||||
$finalization->createPendingState(Version::VERSION, $latestVersionNum, [
|
$finalization->createPendingState(Version::VERSION, $latestVersionNum, [
|
||||||
'branch' => $updateBranch,
|
'branch' => $updateBranch,
|
||||||
@@ -361,7 +482,7 @@ class Update implements InjectionAwareInterface
|
|||||||
$zip->extractTo(PATH_ROOT);
|
$zip->extractTo(PATH_ROOT);
|
||||||
$zip->close();
|
$zip->close();
|
||||||
} catch (ZipException $e) {
|
} catch (ZipException $e) {
|
||||||
error_log($e->getMessage());
|
$this->di['logger']->withChannel('update')->error($e->getMessage());
|
||||||
|
|
||||||
throw new Exception('Failed to extract file, please check file and folder permissions. Further details are available in the error log.');
|
throw new Exception('Failed to extract file, please check file and folder permissions. Further details are available in the error log.');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ class UpdateFinalization implements InjectionAwareInterface
|
|||||||
$this->filesystem->remove(Path::join(PATH_ROOT, 'install'));
|
$this->filesystem->remove(Path::join(PATH_ROOT, 'install'));
|
||||||
$this->clearCache();
|
$this->clearCache();
|
||||||
} catch (IOException $e) {
|
} catch (IOException $e) {
|
||||||
error_log($e->getMessage());
|
$this->di['logger']->withChannel('update')->error($e->getMessage());
|
||||||
|
|
||||||
throw new Exception('Unable to clear cache and/or remove install folder while finalizing the update. Further details are available in the error log.');
|
throw new Exception('Unable to clear cache and/or remove install folder while finalizing the update. Further details are available in the error log.');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ class UpdatePatcher implements InjectionAwareInterface
|
|||||||
$this->filesystem->rename($file, $action);
|
$this->filesystem->rename($file, $action);
|
||||||
}
|
}
|
||||||
} catch (IOException $e) {
|
} catch (IOException $e) {
|
||||||
error_log($e->getMessage());
|
$this->di['logger']->withChannel('update')->error($e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -197,6 +197,14 @@ class UpdatePatcher implements InjectionAwareInterface
|
|||||||
return $this->di['pdo'];
|
return $this->di['pdo'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function prepareAndExecute(string $sql, array $params = []): \PDOStatement
|
||||||
|
{
|
||||||
|
$statement = $this->getPdo()->prepare($sql);
|
||||||
|
$statement->execute($params);
|
||||||
|
|
||||||
|
return $statement;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute the given SQL statement.
|
* Execute the given SQL statement.
|
||||||
*
|
*
|
||||||
@@ -205,11 +213,10 @@ class UpdatePatcher implements InjectionAwareInterface
|
|||||||
private function executeSql(string $sql, array $params = []): void
|
private function executeSql(string $sql, array $params = []): void
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$statement = $this->getPdo()->prepare($sql);
|
$this->prepareAndExecute($sql, $params);
|
||||||
$statement->execute($params);
|
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
// Log the error and then throw a user-friendly exception to prevent further patches from being applied.
|
// Log the error and then throw a user-friendly exception to prevent further patches from being applied.
|
||||||
error_log($e->getMessage());
|
$this->di['logger']->withChannel('update')->error($e->getMessage());
|
||||||
|
|
||||||
throw new Exception('There was an error while applying database patches. Please check the error log for information on the error, correct it, and then perform the backup patching method to complete the update.');
|
throw new Exception('There was an error while applying database patches. Please check the error log for information on the error, correct it, and then perform the backup patching method to complete the update.');
|
||||||
}
|
}
|
||||||
@@ -217,34 +224,22 @@ class UpdatePatcher implements InjectionAwareInterface
|
|||||||
|
|
||||||
private function fetchAll(string $sql, array $params = []): array
|
private function fetchAll(string $sql, array $params = []): array
|
||||||
{
|
{
|
||||||
$statement = $this->getPdo()->prepare($sql);
|
return $this->prepareAndExecute($sql, $params)->fetchAll(\PDO::FETCH_ASSOC);
|
||||||
$statement->execute($params);
|
|
||||||
|
|
||||||
return $statement->fetchAll(\PDO::FETCH_ASSOC);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function fetchOne(string $sql, array $params = []): mixed
|
private function fetchOne(string $sql, array $params = []): mixed
|
||||||
{
|
{
|
||||||
$statement = $this->getPdo()->prepare($sql);
|
return $this->prepareAndExecute($sql, $params)->fetchColumn();
|
||||||
$statement->execute($params);
|
|
||||||
|
|
||||||
return $statement->fetchColumn();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function fetchFirstColumn(string $sql, array $params = []): array
|
private function fetchFirstColumn(string $sql, array $params = []): array
|
||||||
{
|
{
|
||||||
$statement = $this->getPdo()->prepare($sql);
|
return $this->prepareAndExecute($sql, $params)->fetchAll(\PDO::FETCH_COLUMN);
|
||||||
$statement->execute($params);
|
|
||||||
|
|
||||||
return $statement->fetchAll(\PDO::FETCH_COLUMN);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function fetchKeyValue(string $sql, array $params = []): array
|
private function fetchKeyValue(string $sql, array $params = []): array
|
||||||
{
|
{
|
||||||
$statement = $this->getPdo()->prepare($sql);
|
return $this->prepareAndExecute($sql, $params)->fetchAll(\PDO::FETCH_KEY_PAIR);
|
||||||
$statement->execute($params);
|
|
||||||
|
|
||||||
return $statement->fetchAll(\PDO::FETCH_KEY_PAIR);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function updateTable(string $table, array $data, array $criteria): void
|
private function updateTable(string $table, array $data, array $criteria): void
|
||||||
@@ -361,7 +356,7 @@ class UpdatePatcher implements InjectionAwareInterface
|
|||||||
|
|
||||||
$rows = $this->fetchAll("SELECT {$idColumn} AS id, {$valueColumn} AS encrypted_value FROM {$quotedTable} WHERE {$where}", $params);
|
$rows = $this->fetchAll("SELECT {$idColumn} AS id, {$valueColumn} AS encrypted_value FROM {$quotedTable} WHERE {$where}", $params);
|
||||||
|
|
||||||
/** @var \Box_Crypt $crypt */
|
/** @var Crypt $crypt */
|
||||||
$crypt = $this->di['crypt'];
|
$crypt = $this->di['crypt'];
|
||||||
$salt = Config::getProperty('info.salt');
|
$salt = Config::getProperty('info.salt');
|
||||||
|
|
||||||
@@ -369,7 +364,7 @@ class UpdatePatcher implements InjectionAwareInterface
|
|||||||
|
|
||||||
foreach ($rows as $row) {
|
foreach ($rows as $row) {
|
||||||
$encryptedValue = $row['encrypted_value'] ?? null;
|
$encryptedValue = $row['encrypted_value'] ?? null;
|
||||||
if (!is_string($encryptedValue) || $encryptedValue === '' || str_starts_with($encryptedValue, \Box_Crypt::CURRENT_FORMAT_PREFIX)) {
|
if (!is_string($encryptedValue) || $encryptedValue === '' || str_starts_with($encryptedValue, Crypt::CURRENT_FORMAT_PREFIX)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1139,7 +1134,7 @@ class UpdatePatcher implements InjectionAwareInterface
|
|||||||
$this->di['cache']->delete('config_mod_spamchecker');
|
$this->di['cache']->delete('config_mod_spamchecker');
|
||||||
$this->di['cache']->delete('config_mod_antispam');
|
$this->di['cache']->delete('config_mod_antispam');
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
error_log('Spamchecker to Anti-Spam migration error: ' . $e->getMessage());
|
$this->di['logger']->withChannel('update')->error('Spamchecker to Anti-Spam migration error: ' . $e->getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
$fileActions = [
|
$fileActions = [
|
||||||
@@ -1174,7 +1169,7 @@ class UpdatePatcher implements InjectionAwareInterface
|
|||||||
try {
|
try {
|
||||||
$this->filesystem->remove($dir->getPathname());
|
$this->filesystem->remove($dir->getPathname());
|
||||||
} catch (IOException $e) {
|
} catch (IOException $e) {
|
||||||
error_log($e->getMessage());
|
$this->di['logger']->withChannel('update')->error($e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (\Symfony\Component\Finder\Exception\DirectoryNotFoundException) {
|
} catch (\Symfony\Component\Finder\Exception\DirectoryNotFoundException) {
|
||||||
@@ -1298,7 +1293,7 @@ class UpdatePatcher implements InjectionAwareInterface
|
|||||||
$needsSave = false;
|
$needsSave = false;
|
||||||
foreach ($fields as $field) {
|
foreach ($fields as $field) {
|
||||||
if (isset($config[$field]) && is_string($config[$field]) && preg_match('/\b(function|include|import|extends|range|max|min|dump|system|guest\.|admin\.|client\.)\b/i', $config[$field])) {
|
if (isset($config[$field]) && is_string($config[$field]) && preg_match('/\b(function|include|import|extends|range|max|min|dump|system|guest\.|admin\.|client\.)\b/i', $config[$field])) {
|
||||||
$this->di['logger']->setChannel('update')->warning('Custom payment adapter template for gateway ID %s contained incompatible Twig syntax and has been cleared. Please re-create it with compatible syntax.', $gateway['id']);
|
$this->di['logger']->withChannel('update')->warning('Custom payment adapter template for gateway ID {gateway_id} contained incompatible Twig syntax and has been cleared. Please re-create it with compatible syntax.', ['gateway_id' => $gateway['id']]);
|
||||||
unset($config[$field]);
|
unset($config[$field]);
|
||||||
$needsSave = true;
|
$needsSave = true;
|
||||||
}
|
}
|
||||||
@@ -1319,7 +1314,7 @@ class UpdatePatcher implements InjectionAwareInterface
|
|||||||
$this->executeSql("DELETE FROM extension WHERE type = 'mod' AND name = 'wysiwyg'");
|
$this->executeSql("DELETE FROM extension WHERE type = 'mod' AND name = 'wysiwyg'");
|
||||||
$this->di['cache']->delete('config_mod_wysiwyg');
|
$this->di['cache']->delete('config_mod_wysiwyg');
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
error_log('Wysiwyg cleanup migration error: ' . $e->getMessage());
|
$this->di['logger']->withChannel('update')->error('Wysiwyg cleanup migration error: ' . $e->getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->executeFileActions([
|
$this->executeFileActions([
|
||||||
@@ -1969,7 +1964,7 @@ class UpdatePatcher implements InjectionAwareInterface
|
|||||||
['value' => $documentNr, 'id' => $clientId]
|
['value' => $documentNr, 'id' => $clientId]
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
$this->di['logger']->setChannel('update')->warning('patch75: client #%d has no free custom field slot; unmigrated document_nr was "%s".', $clientId, $documentNr);
|
$this->di['logger']->withChannel('update')->warning('patch75: client #{client_id} has no free custom field slot; the document number could not be migrated.', ['client_id' => $clientId]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2654,7 +2649,7 @@ class UpdatePatcher implements InjectionAwareInterface
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legacy DB column prefix => Box_Period code.
|
// Legacy DB column prefix => billing period code.
|
||||||
$legacyPeriods = [
|
$legacyPeriods = [
|
||||||
'w' => '1W',
|
'w' => '1W',
|
||||||
'm' => '1M',
|
'm' => '1M',
|
||||||
|
|||||||
@@ -8,22 +8,28 @@ declare(strict_types=1);
|
|||||||
* @copyright FOSSBilling (https://www.fossbilling.org)
|
* @copyright FOSSBilling (https://www.fossbilling.org)
|
||||||
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache-2.0
|
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache-2.0
|
||||||
*/
|
*/
|
||||||
class Box_Url implements FOSSBilling\InjectionAwareInterface
|
|
||||||
{
|
|
||||||
protected ?Pimple\Container $di = null;
|
|
||||||
protected ?string $baseUri = null;
|
|
||||||
|
|
||||||
public function setDi(Pimple\Container $di): void
|
namespace FOSSBilling;
|
||||||
|
|
||||||
|
use Pimple\Container;
|
||||||
|
|
||||||
|
class Url implements InjectionAwareInterface
|
||||||
|
{
|
||||||
|
protected ?Container $di = null;
|
||||||
|
|
||||||
|
private string $baseUri = '';
|
||||||
|
|
||||||
|
public function setDi(Container $di): void
|
||||||
{
|
{
|
||||||
$this->di = $di;
|
$this->di = $di;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getDi(): ?Pimple\Container
|
public function getDi(): ?Container
|
||||||
{
|
{
|
||||||
return $this->di;
|
return $this->di;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setBaseUri($baseUri): void
|
public function setBaseUri(string $baseUri): void
|
||||||
{
|
{
|
||||||
$this->baseUri = $baseUri;
|
$this->baseUri = $baseUri;
|
||||||
}
|
}
|
||||||
@@ -36,22 +42,21 @@ class Box_Url implements FOSSBilling\InjectionAwareInterface
|
|||||||
return $this->baseUri . $uri;
|
return $this->baseUri . $uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function link(?string $uri = null, ?array $params = []): string
|
public function link(?string $uri = null, ?array $params = null): string
|
||||||
{
|
{
|
||||||
$uri ??= '';
|
$uri = trim($uri ?? '', '/');
|
||||||
$uri = trim($uri, '/');
|
$params ??= [];
|
||||||
$link = $this->baseUri . $uri;
|
$link = $this->baseUri . $uri;
|
||||||
if (!empty($params)) {
|
if ($params !== []) {
|
||||||
$link .= '?' . http_build_query($params);
|
$link .= '?' . http_build_query($params);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $link;
|
return $link;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function adminLink(?string $uri, ?array $params = []): string
|
public function adminLink(?string $uri = null, ?array $params = null): string
|
||||||
{
|
{
|
||||||
$uri ??= '';
|
$uri = trim($uri ?? '', '/');
|
||||||
$uri = trim($uri, '/');
|
|
||||||
$uri = ADMIN_PREFIX . '/' . $uri;
|
$uri = ADMIN_PREFIX . '/' . $uri;
|
||||||
|
|
||||||
return $this->link($uri, $params);
|
return $this->link($uri, $params);
|
||||||
@@ -259,7 +259,7 @@ class Payment_Adapter_PayPalEmail extends Payment_AdapterAbstract implements FOS
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
error_log('Unknown paypal transaction ' . $id);
|
$this->di['logger']->error('Unknown PayPal transaction ' . $id);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use Box\Mod\Client\Entity\Client;
|
|||||||
use Box\Mod\Invoice\Entity\Invoice;
|
use Box\Mod\Invoice\Entity\Invoice;
|
||||||
use Box\Mod\Invoice\Entity\Subscription;
|
use Box\Mod\Invoice\Entity\Subscription;
|
||||||
use Box\Mod\Invoice\Entity\Transaction;
|
use Box\Mod\Invoice\Entity\Transaction;
|
||||||
|
use FOSSBilling\Period;
|
||||||
use Stripe\StripeClient;
|
use Stripe\StripeClient;
|
||||||
use Symfony\Component\Intl\Currencies;
|
use Symfony\Component\Intl\Currencies;
|
||||||
|
|
||||||
@@ -52,6 +53,14 @@ class Payment_Adapter_Stripe implements FOSSBilling\InjectionAwareInterface
|
|||||||
return $this->di;
|
return $this->di;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function debugLog(string $message): void
|
||||||
|
{
|
||||||
|
// @phpstan-ignore if.alwaysFalse (DEBUG is a runtime constant that may be true during debugging)
|
||||||
|
if (DEBUG) {
|
||||||
|
$this->di['logger']->debug($message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function __construct(private $config)
|
public function __construct(private $config)
|
||||||
{
|
{
|
||||||
if ($this->config['test_mode']) {
|
if ($this->config['test_mode']) {
|
||||||
@@ -216,10 +225,7 @@ class Payment_Adapter_Stripe implements FOSSBilling\InjectionAwareInterface
|
|||||||
$tx->setUpdatedAt(new DateTime());
|
$tx->setUpdatedAt(new DateTime());
|
||||||
$this->di['em']->flush();
|
$this->di['em']->flush();
|
||||||
|
|
||||||
// @phpstan-ignore if.alwaysFalse (DEBUG is a runtime constant that may be true during debugging)
|
$this->debugLog((string) json_encode($e->getJsonBody()));
|
||||||
if (DEBUG) {
|
|
||||||
error_log(json_encode($e->getJsonBody()));
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Exception($tx->getError());
|
throw new Exception($tx->getError());
|
||||||
}
|
}
|
||||||
@@ -730,9 +736,7 @@ class Payment_Adapter_Stripe implements FOSSBilling\InjectionAwareInterface
|
|||||||
try {
|
try {
|
||||||
$this->updateSubscriptionStatusFromGateway($api_admin, $stripeSubscription->id, $status);
|
$this->updateSubscriptionStatusFromGateway($api_admin, $stripeSubscription->id, $status);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
if (DEBUG) {
|
$this->debugLog('Stripe subscription updated webhook: ' . $e->getMessage());
|
||||||
error_log('Stripe subscription updated webhook: ' . $e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -881,9 +885,7 @@ class Payment_Adapter_Stripe implements FOSSBilling\InjectionAwareInterface
|
|||||||
try {
|
try {
|
||||||
$this->updateSubscriptionStatusFromGateway($api_admin, $subscriptionId, 'canceled');
|
$this->updateSubscriptionStatusFromGateway($api_admin, $subscriptionId, 'canceled');
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
if (DEBUG) {
|
$this->debugLog('Stripe invoice payment failed webhook: ' . $e->getMessage());
|
||||||
error_log('Stripe invoice payment failed webhook: ' . $e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -979,11 +981,10 @@ class Payment_Adapter_Stripe implements FOSSBilling\InjectionAwareInterface
|
|||||||
|
|
||||||
if ($acquired !== 1) {
|
if ($acquired !== 1) {
|
||||||
$waitDurationMs = (hrtime(true) - $waitStartedAt) / 1_000_000;
|
$waitDurationMs = (hrtime(true) - $waitStartedAt) / 1_000_000;
|
||||||
$this->di['logger']->warning(sprintf(
|
$this->di['logger']->warning(
|
||||||
'Timed out after %.1f ms waiting for Stripe object lock %s',
|
'Timed out after {duration_ms} ms waiting for Stripe object lock {lock_name}',
|
||||||
$waitDurationMs,
|
['duration_ms' => $waitDurationMs, 'lock_name' => $lockName]
|
||||||
$lockName
|
);
|
||||||
));
|
|
||||||
|
|
||||||
throw new FOSSBilling\Exception('Timed out waiting to process this Stripe payment');
|
throw new FOSSBilling\Exception('Timed out waiting to process this Stripe payment');
|
||||||
}
|
}
|
||||||
@@ -1065,9 +1066,7 @@ class Payment_Adapter_Stripe implements FOSSBilling\InjectionAwareInterface
|
|||||||
throw $e;
|
throw $e;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (DEBUG) {
|
$this->debugLog('Stripe setup_intent webhook: subscription creation deferred to redirect flow: ' . $e->getMessage());
|
||||||
error_log('Stripe setup_intent webhook: subscription creation deferred to redirect flow: ' . $e->getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1129,9 +1128,7 @@ class Payment_Adapter_Stripe implements FOSSBilling\InjectionAwareInterface
|
|||||||
try {
|
try {
|
||||||
$api_admin->invoice_subscription_create($sd);
|
$api_admin->invoice_subscription_create($sd);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
if (DEBUG) {
|
$this->debugLog('Failed to create FOSSBilling subscription for ' . $subscription->id . ': ' . $e->getMessage());
|
||||||
error_log('Failed to create FOSSBilling subscription for ' . $subscription->id . ': ' . $e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1394,9 +1391,9 @@ class Payment_Adapter_Stripe implements FOSSBilling\InjectionAwareInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a Box_Period code (e.g. "1M", "3Y", "45D") into Stripe's recurring price
|
* Converts a billing period code (e.g. "1M", "3Y", "45D") into Stripe's recurring price
|
||||||
* parameters. Stripe caps how large interval_count can be per unit, so periods that
|
* parameters. Stripe caps how large interval_count can be per unit, so periods that
|
||||||
* exceed those caps (Box_Period already allows up to 5 years) are rejected outright
|
* exceed those caps (billing periods allow up to 5 years) are rejected outright
|
||||||
* rather than silently mis-billed.
|
* rather than silently mis-billed.
|
||||||
*
|
*
|
||||||
* @see https://docs.stripe.com/api/prices/create#create_price-recurring-interval_count
|
* @see https://docs.stripe.com/api/prices/create#create_price-recurring-interval_count
|
||||||
@@ -1405,7 +1402,7 @@ class Payment_Adapter_Stripe implements FOSSBilling\InjectionAwareInterface
|
|||||||
*/
|
*/
|
||||||
private function getStripeRecurringParams(string $periodCode): array
|
private function getStripeRecurringParams(string $periodCode): array
|
||||||
{
|
{
|
||||||
$period = new Box_Period($periodCode);
|
$period = new Period($periodCode);
|
||||||
$interval = $this->convertPeriodToStripe($period);
|
$interval = $this->convertPeriodToStripe($period);
|
||||||
$intervalCount = $period->getQty();
|
$intervalCount = $period->getQty();
|
||||||
|
|
||||||
@@ -1427,13 +1424,13 @@ class Payment_Adapter_Stripe implements FOSSBilling\InjectionAwareInterface
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function convertPeriodToStripe(Box_Period $period): string
|
private function convertPeriodToStripe(Period $period): string
|
||||||
{
|
{
|
||||||
return match ($period->getUnit()) {
|
return match ($period->getUnit()) {
|
||||||
Box_Period::UNIT_DAY => 'day',
|
Period::UNIT_DAY => 'day',
|
||||||
Box_Period::UNIT_WEEK => 'week',
|
Period::UNIT_WEEK => 'week',
|
||||||
Box_Period::UNIT_MONTH => 'month',
|
Period::UNIT_MONTH => 'month',
|
||||||
Box_Period::UNIT_YEAR => 'year',
|
Period::UNIT_YEAR => 'year',
|
||||||
default => 'month',
|
default => 'month',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ abstract class Payment_AdapterAbstract
|
|||||||
/**
|
/**
|
||||||
* Log object.
|
* Log object.
|
||||||
*/
|
*/
|
||||||
private ?Box_Log $_log = null;
|
private ?Psr\Log\LoggerInterface $_log = null;
|
||||||
|
|
||||||
// Stub function that can be overridden by a registrar
|
// Stub function that can be overridden by a registrar
|
||||||
public function init()
|
public function init()
|
||||||
@@ -125,16 +125,16 @@ abstract class Payment_AdapterAbstract
|
|||||||
return $data['invoice_id'] ?? null;
|
return $data['invoice_id'] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setLog(Box_Log $log): void
|
public function setLog(Psr\Log\LoggerInterface $log): void
|
||||||
{
|
{
|
||||||
$this->_log = $log;
|
$this->_log = $log;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getLog()
|
public function getLog(): Psr\Log\LoggerInterface
|
||||||
{
|
{
|
||||||
$log = $this->_log;
|
$log = $this->_log;
|
||||||
if (!$log instanceof Box_Log) {
|
if (!$log instanceof Psr\Log\LoggerInterface) {
|
||||||
$log = new Box_Log();
|
$log = new FOSSBilling\Logger();
|
||||||
}
|
}
|
||||||
|
|
||||||
return $log;
|
return $log;
|
||||||
|
|||||||
@@ -369,7 +369,7 @@ class Registrar_Adapter_Internetbs extends Registrar_AdapterAbstract
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($this->isTestEnv()) {
|
if ($this->isTestEnv()) {
|
||||||
error_log(print_r($result, true));
|
$this->getLog()->debug('Internet.bs response received.', ['status' => $result['status'] ?? null]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
use GeoIp2\Model\Domain;
|
|
||||||
use Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface;
|
use Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface;
|
||||||
|
|
||||||
class Registrar_Adapter_Namecheap extends Registrar_AdapterAbstract
|
class Registrar_Adapter_Namecheap extends Registrar_AdapterAbstract
|
||||||
@@ -151,14 +150,14 @@ class Registrar_Adapter_Namecheap extends Registrar_AdapterAbstract
|
|||||||
$result = simplexml_load_string($data);
|
$result = simplexml_load_string($data);
|
||||||
|
|
||||||
if (isset($result['status']) && strtolower((string) $result['status']) == 'error') {
|
if (isset($result['status']) && strtolower((string) $result['status']) == 'error') {
|
||||||
error_log('Namecheap error: ' . PHP_EOL . $result['error']);
|
$this->getLog()->error('Namecheap error: ' . PHP_EOL . $result['error']);
|
||||||
$placeholders = [':action:' => $params['Command'], ':type:' => 'Namecheap'];
|
$placeholders = [':action:' => $params['Command'], ':type:' => 'Namecheap'];
|
||||||
|
|
||||||
throw new Registrar_Exception('Failed to :action: with the :type: registrar, check the error logs for further details', $placeholders);
|
throw new Registrar_Exception('Failed to :action: with the :type: registrar, check the error logs for further details', $placeholders);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($result['status']) && strtolower((string) $result['status']) == 'failed') {
|
if (isset($result['status']) && strtolower((string) $result['status']) == 'failed') {
|
||||||
error_log('Namecheap error: ' . PHP_EOL . $result['actionstatusdesc']);
|
$this->getLog()->error('Namecheap error: ' . PHP_EOL . $result['actionstatusdesc']);
|
||||||
$placeholders = [':action:' => $params['Command'], ':type:' => 'Namecheap'];
|
$placeholders = [':action:' => $params['Command'], ':type:' => 'Namecheap'];
|
||||||
|
|
||||||
throw new Registrar_Exception('Failed to :action: with the :type: registrar, check the error logs for further details', $placeholders);
|
throw new Registrar_Exception('Failed to :action: with the :type: registrar, check the error logs for further details', $placeholders);
|
||||||
@@ -523,7 +522,7 @@ class Registrar_Adapter_Namecheap extends Registrar_AdapterAbstract
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO: Implement this correctly.
|
* Namecheap does not expose a domain deletion command through its API.
|
||||||
*
|
*
|
||||||
* @throws Registrar_Exception
|
* @throws Registrar_Exception
|
||||||
*/
|
*/
|
||||||
@@ -533,11 +532,11 @@ class Registrar_Adapter_Namecheap extends Registrar_AdapterAbstract
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return string[]
|
* @return array{enabled: bool, id: string}
|
||||||
*
|
*
|
||||||
* @throws Registrar_Exception
|
* @throws Registrar_Exception
|
||||||
*/
|
*/
|
||||||
private function getPrivacyInfo(Registrar_Domain $domain)
|
private function getPrivacyInfo(Registrar_Domain $domain): array
|
||||||
{
|
{
|
||||||
$params = [
|
$params = [
|
||||||
'DomainName' => $domain->getName(),
|
'DomainName' => $domain->getName(),
|
||||||
|
|||||||
@@ -626,21 +626,21 @@ class Registrar_Adapter_Resellerclub extends Registrar_AdapterAbstract
|
|||||||
$json = $result->toArray(false);
|
$json = $result->toArray(false);
|
||||||
|
|
||||||
if (isset($json['status']) && $json['status'] == 'ERROR') {
|
if (isset($json['status']) && $json['status'] == 'ERROR') {
|
||||||
error_log('ResellerClub error: ' . $json['message']);
|
$this->getLog()->error('ResellerClub error: ' . $json['message']);
|
||||||
$placeholders = [':action:' => $url, ':type:' => 'ResellerClub'];
|
$placeholders = [':action:' => $url, ':type:' => 'ResellerClub'];
|
||||||
|
|
||||||
throw new Registrar_Exception('Failed to :action: with the :type: registrar, check the error logs for further details', $placeholders);
|
throw new Registrar_Exception('Failed to :action: with the :type: registrar, check the error logs for further details', $placeholders);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($json['status']) && $json['status'] == 'error') {
|
if (isset($json['status']) && $json['status'] == 'error') {
|
||||||
error_log('ResellerClub error: ' . $json['error']);
|
$this->getLog()->error('ResellerClub error: ' . $json['error']);
|
||||||
$placeholders = [':action:' => $url, ':type:' => 'ResellerClub'];
|
$placeholders = [':action:' => $url, ':type:' => 'ResellerClub'];
|
||||||
|
|
||||||
throw new Registrar_Exception('Failed to :action: with the :type: registrar, check the error logs for further details', $placeholders);
|
throw new Registrar_Exception('Failed to :action: with the :type: registrar, check the error logs for further details', $placeholders);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($json['status']) && $json['status'] == 'Failed') {
|
if (isset($json['status']) && $json['status'] == 'Failed') {
|
||||||
error_log('ResellerClub error: ' . $json['actionstatusdesc']);
|
$this->getLog()->error('ResellerClub error: ' . $json['actionstatusdesc']);
|
||||||
$placeholders = [':action:' => $url, ':type:' => 'ResellerClub'];
|
$placeholders = [':action:' => $url, ':type:' => 'ResellerClub'];
|
||||||
|
|
||||||
throw new Registrar_Exception('Failed to :action: with the :type: registrar, check the error logs for further details', $placeholders);
|
throw new Registrar_Exception('Failed to :action: with the :type: registrar, check the error logs for further details', $placeholders);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ declare(strict_types=1);
|
|||||||
*/
|
*/
|
||||||
abstract class Registrar_AdapterAbstract
|
abstract class Registrar_AdapterAbstract
|
||||||
{
|
{
|
||||||
protected $_log;
|
protected ?Psr\Log\LoggerInterface $_log = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Are we in test mode ?
|
* Are we in test mode ?
|
||||||
@@ -190,11 +190,11 @@ abstract class Registrar_AdapterAbstract
|
|||||||
/**
|
/**
|
||||||
* Sets the logger object to use for logging messages.
|
* Sets the logger object to use for logging messages.
|
||||||
*
|
*
|
||||||
* @param Box_Log $log the logger object to use
|
* @param Psr\Log\LoggerInterface $log the logger object to use
|
||||||
*
|
*
|
||||||
* @return Registrar_AdapterAbstract the current adapter object, for method chaining
|
* @return static the current adapter object, for method chaining
|
||||||
*/
|
*/
|
||||||
public function setLog(Box_Log $log)
|
public function setLog(Psr\Log\LoggerInterface $log): static
|
||||||
{
|
{
|
||||||
$this->_log = $log;
|
$this->_log = $log;
|
||||||
|
|
||||||
@@ -204,13 +204,13 @@ abstract class Registrar_AdapterAbstract
|
|||||||
/**
|
/**
|
||||||
* Gets the logger object currently in use for logging messages.
|
* Gets the logger object currently in use for logging messages.
|
||||||
*
|
*
|
||||||
* @return Box_Log the logger object
|
* @return Psr\Log\LoggerInterface the logger object
|
||||||
*/
|
*/
|
||||||
public function getLog()
|
public function getLog(): Psr\Log\LoggerInterface
|
||||||
{
|
{
|
||||||
$log = $this->_log;
|
$log = $this->_log;
|
||||||
if (!$log instanceof Box_Log) {
|
if (!$log instanceof Psr\Log\LoggerInterface) {
|
||||||
$log = new Box_Log();
|
$log = new FOSSBilling\Logger();
|
||||||
}
|
}
|
||||||
|
|
||||||
return $log;
|
return $log;
|
||||||
|
|||||||
@@ -27,11 +27,22 @@ class Server_Client
|
|||||||
private ?string $telephone = null;
|
private ?string $telephone = null;
|
||||||
private ?string $fax = null;
|
private ?string $fax = null;
|
||||||
|
|
||||||
public function __call($name, $arguments)
|
public function __call(string $name, array $arguments): string
|
||||||
{
|
{
|
||||||
|
// This legacy DTO is dependency-free; keep inaccessible-method diagnostics
|
||||||
|
// on PHP's fallback logger rather than coupling it to the DI container.
|
||||||
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
|
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
|
||||||
|
$caller = $backtrace[1] ?? [];
|
||||||
|
|
||||||
error_log(sprintf('Calling %s inaccessible method %s from %s::%d', static::class, $name, $backtrace[1]['file'], $backtrace[1]['line']));
|
if (isset($caller['file'], $caller['line'])) {
|
||||||
|
error_log(sprintf(
|
||||||
|
'Calling %s inaccessible method %s from %s::%d',
|
||||||
|
static::class,
|
||||||
|
$name,
|
||||||
|
$caller['file'],
|
||||||
|
(int) $caller['line'],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ abstract class Server_Manager
|
|||||||
'port' => null,
|
'port' => null,
|
||||||
'passwordLength' => null,
|
'passwordLength' => null,
|
||||||
];
|
];
|
||||||
private ?Box_Log $_log = null;
|
private ?Psr\Log\LoggerInterface $_log = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor for the class.
|
* Constructor for the class.
|
||||||
@@ -148,12 +148,12 @@ abstract class Server_Manager
|
|||||||
/**
|
/**
|
||||||
* Returns the logger object.
|
* Returns the logger object.
|
||||||
*
|
*
|
||||||
* @return Box_Log|null the logger object
|
* @return Psr\Log\LoggerInterface|null the logger object
|
||||||
*/
|
*/
|
||||||
public function getLog(): ?Box_Log
|
public function getLog(): ?Psr\Log\LoggerInterface
|
||||||
{
|
{
|
||||||
if (!$this->_log instanceof Box_Log) {
|
if (!$this->_log instanceof Psr\Log\LoggerInterface) {
|
||||||
return new Box_Log();
|
return new FOSSBilling\Logger();
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->_log;
|
return $this->_log;
|
||||||
@@ -162,9 +162,9 @@ abstract class Server_Manager
|
|||||||
/**
|
/**
|
||||||
* Sets the logger object.
|
* Sets the logger object.
|
||||||
*
|
*
|
||||||
* @param Box_Log $value the logger object
|
* @param Psr\Log\LoggerInterface $value the logger object
|
||||||
*/
|
*/
|
||||||
public function setLog(Box_Log $value): static
|
public function setLog(Psr\Log\LoggerInterface $value): static
|
||||||
{
|
{
|
||||||
$this->_log = $value;
|
$this->_log = $value;
|
||||||
|
|
||||||
|
|||||||
@@ -410,7 +410,7 @@ class Server_Manager_CWP extends Server_Manager
|
|||||||
|
|
||||||
// If the status is not 'OK', log an error message and return false
|
// If the status is not 'OK', log an error message and return false
|
||||||
if ($status !== 'OK') {
|
if ($status !== 'OK') {
|
||||||
error_log('CWP Server manager error. Status: ' . $status . '. Message: ' . $msg);
|
$this->getLog()->error('CWP Server manager error. Status: ' . $status . '. Message: ' . $msg);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -510,84 +510,16 @@ class Server_Manager_Directadmin extends Server_Manager
|
|||||||
*/
|
*/
|
||||||
public function modifyAccount(Server_Account $account): bool
|
public function modifyAccount(Server_Account $account): bool
|
||||||
{
|
{
|
||||||
// Get the package associated with the account
|
|
||||||
$package = $account->getPackage();
|
$package = $account->getPackage();
|
||||||
|
|
||||||
// Prepare the fields for the request
|
$fields = $this->getCustomPackageFields($package);
|
||||||
$fields = [
|
$fields['dnscontrol'] = $this->getCustomPackageFlag($package, 'dnscontrol', 'ON');
|
||||||
'aftp' => $package->getHasAnonymousFtp() ? 'ON' : 'OFF', // ON or OFF. If ON, the User will be able to have anonymous ftp accounts.
|
$fields['sysinfo'] = $this->getCustomPackageFlag($package, 'sysinfo', 'ON');
|
||||||
'action' => 'customize',
|
$fields['action'] = 'customize';
|
||||||
'bandwidth' => $package->getBandwidth(), // Bandwidth quota in MB
|
$fields['ns1'] = $account->getNs1();
|
||||||
'catchall' => $package->getHasCatchAll() ? 'ON' : 'OFF', // ON or OFF. If ON, the User will have the ability to enable and customize a catch-all email (*@domain.com).
|
$fields['ns2'] = $account->getNs2();
|
||||||
'cgi' => $package->getHasCgi() ? 'ON' : 'OFF', // ON or OFF. If ON, the User will have the ability to run cgi scripts in their cgi-bin.
|
$fields['user'] = $account->getUsername();
|
||||||
'cron' => $package->getHasCron() ? 'ON' : 'OFF', // ON or OFF. If ON, the User will have the ability to create cronjobs.
|
|
||||||
'dnscontrol' => 'ON', // ON or OFF. If ON, the User will be able to modify his/her dns records.
|
|
||||||
'domainptr' => $package->getMaxParkedDomains(), // Domain pointer quota
|
|
||||||
'ftp' => $package->getMaxFtp(), // FTP account quota
|
|
||||||
'mysql' => $package->getMaxSql(), // MySQL database quota
|
|
||||||
'nemailf' => $package->getMaxEmailForwarders(), // Email forwarder quota
|
|
||||||
'nemailml' => $package->getMaxEmailLists(), // Mailing list quota
|
|
||||||
'nemailr' => $package->getMaxEmailAutoresponders(), // Autoresponder quota
|
|
||||||
'nemails' => $package->getMaxPop(), // Email account quota
|
|
||||||
'nsubdomains' => $package->getMaxSubdomains(), // Subdomain quota
|
|
||||||
'ns1' => $account->getNs1(),
|
|
||||||
'ns2' => $account->getNs2(),
|
|
||||||
'php' => $package->getHasPhp() ? 'ON' : 'OFF', // ON or OFF. If ON, the User will have the ability to run php scripts.
|
|
||||||
'quota' => $package->getQuota(), // Disk space quota in MB
|
|
||||||
'spam' => $package->getHasSpamFilter() ? 'ON' : 'OFF', // ON or OFF. If ON, the User will have the ability to run scan email with SpamAssassin.
|
|
||||||
'ssh' => $package->getHasShell() ? 'ON' : 'OFF', // ON or OFF. If ON, the User will have an ssh account.
|
|
||||||
'ssl' => $package->getHasSsl() ? 'ON' : 'OFF', // ON or OFF. If ON, the User will have the ability to access their websites through secure https://.
|
|
||||||
'sysinfo' => 'ON', // ON or OFF. If ON, the User will have access to a page that shows the system information.
|
|
||||||
'user' => $account->getUsername(),
|
|
||||||
'vdomains' => $package->getMaxDomains(), // Domain quota
|
|
||||||
];
|
|
||||||
|
|
||||||
// Check if certain parameters are set to 'unlimited' and set the corresponding fields to 'ON'
|
|
||||||
if ($package->getBandwidth() == 'unlimited') {
|
|
||||||
$fields['ubandwidth'] = 'ON'; // ON or OFF. If ON, bandwidth is ignored and no limit is set
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($package->getQuota() == 'unlimited') {
|
|
||||||
$fields['uquota'] = 'ON'; // ON or OFF. If ON, quota is ignored and no limit is set
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($package->getMaxDomains() == 'unlimited') {
|
|
||||||
$fields['uvdomains'] = 'ON'; // ON or OFF. If ON, vdomains is ignored and no limit is set
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($package->getMaxSubdomains() == 'unlimited') {
|
|
||||||
$fields['unsubdomains'] = 'ON'; // ON or OFF. If ON, nsubdomains is ignored and no limit is set
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($package->getMaxPop() == 'unlimited') {
|
|
||||||
$fields['unemails'] = 'ON'; // ON or OFF Unlimited option for nemails
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($package->getMaxEmailForwarders() == 'unlimited') {
|
|
||||||
$fields['unemailf'] = 'ON'; // ON or OFF Unlimited option for nemailf
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($package->getMaxEmailLists() == 'unlimited') {
|
|
||||||
$fields['unemailml'] = 'ON'; // ON or OFF Unlimited option for nemailml
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($package->getMaxEmailAutoresponders() == 'unlimited') {
|
|
||||||
$fields['unemailr'] = 'ON'; // ON or OFF Unlimited option for nemailr
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($package->getMaxSql() == 'unlimited') {
|
|
||||||
$fields['umysql'] = 'ON'; // ON or OFF Unlimited option for mysql
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($package->getMaxParkedDomains() == 'unlimited') {
|
|
||||||
$fields['udomainptr'] = 'ON'; // ON or OFF Unlimited option for domainptr
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($package->getMaxFtp() == 'unlimited') {
|
|
||||||
$fields['uftp'] = 'ON'; // ON or OFF Unlimited option for ftp
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send the request to the server
|
|
||||||
$this->request('API_MODIFY_USER', $fields);
|
$this->request('API_MODIFY_USER', $fields);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -739,28 +671,28 @@ class Server_Manager_Directadmin extends Server_Manager
|
|||||||
private function getCustomPackageFields(Server_Package $package): array
|
private function getCustomPackageFields(Server_Package $package): array
|
||||||
{
|
{
|
||||||
$fields = [
|
$fields = [
|
||||||
'aftp' => $package->getCustomValue('aftp') ? 'ON' : 'OFF',
|
'aftp' => $this->getCustomPackageFlag($package, 'aftp'),
|
||||||
'bandwidth' => $package->getBandwidth(),
|
'bandwidth' => $package->getBandwidth(),
|
||||||
'catchall' => $package->getCustomValue('catchall') ? 'ON' : 'OFF',
|
'catchall' => $this->getCustomPackageFlag($package, 'catchall'),
|
||||||
'cgi' => $package->getCustomValue('cgi') ? 'ON' : 'OFF',
|
'cgi' => $this->getCustomPackageFlag($package, 'cgi'),
|
||||||
'cron' => $package->getCustomValue('cron') ? 'ON' : 'OFF',
|
'cron' => $this->getCustomPackageFlag($package, 'cron'),
|
||||||
'dnscontrol' => $package->getCustomValue('dnscontrol') ? 'ON' : 'OFF',
|
'dnscontrol' => $this->getCustomPackageFlag($package, 'dnscontrol', 'ON'),
|
||||||
'domainptr' => $package->getMaxParkedDomains(),
|
'domainptr' => $package->getMaxParkedDomains(),
|
||||||
'ftp' => $package->getMaxFtp(),
|
'ftp' => $package->getMaxFtp(),
|
||||||
'login_keys' => $package->getCustomValue('login_keys') ? 'ON' : 'OFF',
|
'login_keys' => $this->getCustomPackageFlag($package, 'login_keys'),
|
||||||
'mysql' => $package->getMaxSql(),
|
'mysql' => $package->getMaxSql(),
|
||||||
'nemailf' => $package->getMaxEmailForwarders(),
|
'nemailf' => $package->getCustomValue('nemailf'),
|
||||||
'nemailml' => $package->getMaxEmailLists(),
|
'nemailml' => $package->getCustomValue('nemailml'),
|
||||||
'nemailr' => $package->getMaxEmailAutoresponders(),
|
'nemailr' => $package->getCustomValue('nemailr'),
|
||||||
'nemails' => $package->getMaxPop(),
|
'nemails' => $package->getMaxPop(),
|
||||||
'nsubdomains' => $package->getMaxSubdomains(),
|
'nsubdomains' => $package->getMaxSubdomains(),
|
||||||
'php' => $package->getCustomValue('php') ? 'ON' : 'OFF',
|
'php' => $this->getCustomPackageFlag($package, 'php'),
|
||||||
'quota' => $package->getQuota(),
|
'quota' => $package->getQuota(),
|
||||||
'spam' => $package->getCustomValue('spam') ? 'ON' : 'OFF',
|
'spam' => $this->getCustomPackageFlag($package, 'spam'),
|
||||||
'ssh' => $package->getCustomValue('ssh') ? 'ON' : 'OFF',
|
'ssh' => $this->getCustomPackageFlag($package, 'ssh'),
|
||||||
'ssl' => $package->getCustomValue('ssl') ? 'ON' : 'OFF',
|
'ssl' => $this->getCustomPackageFlag($package, 'ssl'),
|
||||||
'suspend_at_limit' => $package->getCustomValue('suspend_at_limit') ? 'ON' : 'OFF',
|
'suspend_at_limit' => $this->getCustomPackageFlag($package, 'suspend_at_limit'),
|
||||||
'sysinfo' => $package->getCustomValue('sysinfo') ? 'ON' : 'OFF',
|
'sysinfo' => $this->getCustomPackageFlag($package, 'sysinfo', 'ON'),
|
||||||
'vdomains' => $package->getMaxDomains(),
|
'vdomains' => $package->getMaxDomains(),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -811,6 +743,16 @@ class Server_Manager_Directadmin extends Server_Manager
|
|||||||
return $fields;
|
return $fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getCustomPackageFlag(Server_Package $package, string $key, string $default = 'OFF'): string
|
||||||
|
{
|
||||||
|
$value = $package->getCustomValue($key);
|
||||||
|
if ($value === null) {
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FOSSBilling\Tools::normalizeBoolean($value) ? 'ON' : 'OFF';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the information of an account from the DirectAdmin server.
|
* Retrieves the information of an account from the DirectAdmin server.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -375,7 +375,7 @@ class Server_Manager_Hestia extends Server_Manager
|
|||||||
if (str_contains($result, 'Error')) {
|
if (str_contains($result, 'Error')) {
|
||||||
throw new Server_Exception('Failed to connect to the :type: server. Please verify your credentials and configuration', [':type:' => 'HestiaCP']);
|
throw new Server_Exception('Failed to connect to the :type: server. Please verify your credentials and configuration', [':type:' => 'HestiaCP']);
|
||||||
} elseif (intval($result) !== 0) {
|
} elseif (intval($result) !== 0) {
|
||||||
error_log("HestiaCP returned error code $result for the " . $params['cmd'] . 'command');
|
$this->getLog()->error("HestiaCP returned error code $result for the " . $params['cmd'] . ' command');
|
||||||
}
|
}
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
|
|||||||
@@ -422,6 +422,8 @@ class Server_Manager_Plesk extends Server_Manager
|
|||||||
private function createSubscriptionProps(Server_Account $account, string $action): array
|
private function createSubscriptionProps(Server_Account $account, string $action): array
|
||||||
{
|
{
|
||||||
$package = $account->getPackage();
|
$package = $account->getPackage();
|
||||||
|
$maxMailLists = $package->getCustomValue('nemailml');
|
||||||
|
$manageMailLists = is_numeric($maxMailLists) && (int) $maxMailLists > 0;
|
||||||
|
|
||||||
// check if bandwidth quota is set as an integer. If so, convert it to bytes
|
// check if bandwidth quota is set as an integer. If so, convert it to bytes
|
||||||
$bandwidth = 0;
|
$bandwidth = 0;
|
||||||
@@ -484,7 +486,7 @@ class Server_Manager_Plesk extends Server_Manager
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
'name' => 'max_maillists',
|
'name' => 'max_maillists',
|
||||||
'value' => $package->getMaxEmailLists() ?: 0,
|
'value' => $maxMailLists ?: 0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'name' => 'max_box',
|
'name' => 'max_box',
|
||||||
@@ -524,19 +526,19 @@ class Server_Manager_Plesk extends Server_Manager
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
'name' => 'manage_crontab',
|
'name' => 'manage_crontab',
|
||||||
'value' => $package->getHasCron() ? 'true' : 'false',
|
'value' => FOSSBilling\Tools::normalizeBoolean($package->getCustomValue('cron')) ? 'true' : 'false',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'name' => 'manage_anonftp',
|
'name' => 'manage_anonftp',
|
||||||
'value' => $package->getHasAnonymousFtp() ? 'true' : 'false',
|
'value' => FOSSBilling\Tools::normalizeBoolean($package->getCustomValue('aftp')) ? 'true' : 'false',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'name' => 'manage_sh_access',
|
'name' => 'manage_sh_access',
|
||||||
'value' => $package->getHasShell() ? 'true' : 'false',
|
'value' => FOSSBilling\Tools::normalizeBoolean($package->getCustomValue('ssh')) ? 'true' : 'false',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'name' => 'manage_maillists',
|
'name' => 'manage_maillists',
|
||||||
'value' => $package->getMaxEmailLists() ? 'true' : 'false',
|
'value' => $manageMailLists ? 'true' : 'false',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'name' => 'create_domains',
|
'name' => 'create_domains',
|
||||||
@@ -552,7 +554,7 @@ class Server_Manager_Plesk extends Server_Manager
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
'name' => 'manage_not_chroot_shell',
|
'name' => 'manage_not_chroot_shell',
|
||||||
'value' => $package->getHasShell() ? 'true' : 'false',
|
'value' => FOSSBilling\Tools::normalizeBoolean($package->getCustomValue('ssh')) ? 'true' : 'false',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'name' => 'manage_domain_aliases',
|
'name' => 'manage_domain_aliases',
|
||||||
@@ -564,7 +566,7 @@ class Server_Manager_Plesk extends Server_Manager
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
'name' => 'manage_spamfilter',
|
'name' => 'manage_spamfilter',
|
||||||
'value' => $package->getHasSpamFilter() ? 'true' : 'false',
|
'value' => FOSSBilling\Tools::normalizeBoolean($package->getCustomValue('spam')) ? 'true' : 'false',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ class Server_Manager_Whm extends Server_Manager
|
|||||||
*/
|
*/
|
||||||
public function synchronizeAccount(Server_Account $account): Server_Account
|
public function synchronizeAccount(Server_Account $account): Server_Account
|
||||||
{
|
{
|
||||||
$this->getLog()->info(sprintf('Synchronizing account %s %s with server', $account->getDomain(), $account->getUsername()));
|
$this->getLog()->info('Synchronizing account {domain} {username} with server', ['domain' => $account->getDomain(), 'username' => $account->getUsername()]);
|
||||||
|
|
||||||
$action = 'accountsummary';
|
$action = 'accountsummary';
|
||||||
$varHash = [
|
$varHash = [
|
||||||
@@ -215,7 +215,7 @@ class Server_Manager_Whm extends Server_Manager
|
|||||||
|
|
||||||
$result = $this->request($action, $varHash);
|
$result = $this->request($action, $varHash);
|
||||||
if (!isset($result->acct[0])) {
|
if (!isset($result->acct[0])) {
|
||||||
error_log('Could not synchronize account with cPanel server. Account does not exist.');
|
$this->getLog()->error('Could not synchronize account with cPanel server. Account does not exist.');
|
||||||
|
|
||||||
return $account;
|
return $account;
|
||||||
}
|
}
|
||||||
@@ -599,7 +599,7 @@ class Server_Manager_Whm extends Server_Manager
|
|||||||
: 'Basic ' . $username . ':' . $password;
|
: 'Basic ' . $username . ':' . $password;
|
||||||
|
|
||||||
// Log the request
|
// Log the request
|
||||||
$this->getLog()->debug(sprintf('Requesting WHM server action "%s" with params "%s" ', $action, print_r($params, true)));
|
$this->getLog()->debug('Requesting WHM server action "{action}" with params "{params}"', ['action' => $action, 'params' => print_r($params, true)]);
|
||||||
|
|
||||||
// Send the request and handle any errors
|
// Send the request and handle any errors
|
||||||
try {
|
try {
|
||||||
@@ -629,28 +629,28 @@ class Server_Manager_Whm extends Server_Manager
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isset($json->cpanelresult->error)) {
|
if (isset($json->cpanelresult->error)) {
|
||||||
$this->getLog()->critical(sprintf('WHM server response error calling action %s: "%s"', $action, $json->cpanelresult->error));
|
$this->getLog()->critical('WHM server response error calling action {action}: "{error}"', ['action' => $action, 'error' => $json->cpanelresult->error]);
|
||||||
$placeholders = ['action' => $action, 'type' => 'cPanel'];
|
$placeholders = ['action' => $action, 'type' => 'cPanel'];
|
||||||
|
|
||||||
throw new Server_Exception('Failed to :action: on the :type: server, check the error logs for further details', $placeholders);
|
throw new Server_Exception('Failed to :action: on the :type: server, check the error logs for further details', $placeholders);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($json->data->result) && $json->data->result == '0') {
|
if (isset($json->data->result) && $json->data->result == '0') {
|
||||||
$this->getLog()->critical(sprintf('WHM server response error calling action %s: "%s"', $action, $json->data->reason));
|
$this->getLog()->critical('WHM server response error calling action {action}: "{error}"', ['action' => $action, 'error' => $json->data->reason]);
|
||||||
$placeholders = [':action:' => $action, ':type:' => 'cPanel'];
|
$placeholders = [':action:' => $action, ':type:' => 'cPanel'];
|
||||||
|
|
||||||
throw new Server_Exception('Failed to :action: on the :type: server, check the error logs for further details', $placeholders);
|
throw new Server_Exception('Failed to :action: on the :type: server, check the error logs for further details', $placeholders);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($json->result) && is_array($json->result) && $json->result[0]->status == 0) {
|
if (isset($json->result) && is_array($json->result) && $json->result[0]->status == 0) {
|
||||||
$this->getLog()->critical(sprintf('WHM server response error calling action %s: "%s"', $action, $json->result[0]->statusmsg));
|
$this->getLog()->critical('WHM server response error calling action {action}: "{error}"', ['action' => $action, 'error' => $json->result[0]->statusmsg]);
|
||||||
$placeholders = [':action:' => $action, ':type:' => 'cPanel'];
|
$placeholders = [':action:' => $action, ':type:' => 'cPanel'];
|
||||||
|
|
||||||
throw new Server_Exception('Failed to :action: on the :type: server, check the error logs for further details', $placeholders);
|
throw new Server_Exception('Failed to :action: on the :type: server, check the error logs for further details', $placeholders);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($json->status) && $json->status != '1') {
|
if (isset($json->status) && $json->status != '1') {
|
||||||
$this->getLog()->critical(sprintf('WHM server response error calling action %s: "%s"', $action, $json->statusmsg));
|
$this->getLog()->critical('WHM server response error calling action {action}: "{error}"', ['action' => $action, 'error' => $json->statusmsg]);
|
||||||
$placeholders = [':action:' => $action, ':type:' => 'cPanel'];
|
$placeholders = [':action:' => $action, ':type:' => 'cPanel'];
|
||||||
|
|
||||||
throw new Server_Exception('Failed to :action: on the :type: server, check the error logs for further details', $placeholders);
|
throw new Server_Exception('Failed to :action: on the :type: server, check the error logs for further details', $placeholders);
|
||||||
@@ -721,42 +721,4 @@ class Server_Manager_Whm extends Server_Manager
|
|||||||
{
|
{
|
||||||
return $this->_config['username'] . '_' . $package->getName();
|
return $this->_config['username'] . '_' . $package->getName();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Modifies the package of an account on the WHM server.
|
|
||||||
*
|
|
||||||
* @param Server_Account $a the account for which to modify the package
|
|
||||||
* @param Server_Package $p the new package
|
|
||||||
*
|
|
||||||
* @return true if the package was successfully modified
|
|
||||||
*
|
|
||||||
* @throws Server_Exception if an error occurs during the request
|
|
||||||
*/
|
|
||||||
private function modifyAccountPackage(Server_Account $a, Server_Package $p): bool
|
|
||||||
{
|
|
||||||
// Log the modification
|
|
||||||
$this->getLog()->info('Modifying account ' . $a->getUsername());
|
|
||||||
|
|
||||||
// Prepare the parameters for the API request
|
|
||||||
$varHash = [
|
|
||||||
'user' => $a->getUsername(),
|
|
||||||
'domain' => $a->getDomain(),
|
|
||||||
'HASCGI' => $p->getHasCgi(),
|
|
||||||
'CPTHEME' => $p->getTheme(),
|
|
||||||
'LANG' => $p->getLanguage(),
|
|
||||||
'MAXPOP' => $p->getMaxPop(),
|
|
||||||
'MAXFTP' => $p->getMaxFtp(),
|
|
||||||
'MAXLST' => $p->getMaxEmailLists(),
|
|
||||||
'MAXSUB' => $p->getMaxSubdomains(),
|
|
||||||
'MAXPARK' => $p->getMaxParkedDomains(),
|
|
||||||
'MAXADDON' => $p->getMaxAddons(),
|
|
||||||
'MAXSQL' => $p->getMaxSql(),
|
|
||||||
'shell' => $p->getHasShell(),
|
|
||||||
];
|
|
||||||
|
|
||||||
// Send a request to the WHM server to modify the account's package
|
|
||||||
$this->request('modifyacct', $varHash);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -29,10 +29,21 @@ class Server_Package
|
|||||||
*
|
*
|
||||||
* @return string always returns an empty string
|
* @return string always returns an empty string
|
||||||
*/
|
*/
|
||||||
public function __call(string $name, array $arguments)
|
public function __call(string $name, array $arguments): string
|
||||||
{
|
{
|
||||||
|
// This legacy DTO is dependency-free; keep inaccessible-method diagnostics
|
||||||
|
// on PHP's fallback logger rather than coupling it to the DI container.
|
||||||
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
|
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
|
||||||
error_log(sprintf('Calling %s inaccessible method %s from %s::%d', static::class, $name, $backtrace[1]['file'], $backtrace[1]['line']));
|
$caller = $backtrace[1] ?? [];
|
||||||
|
if (isset($caller['file'], $caller['line'])) {
|
||||||
|
error_log(sprintf(
|
||||||
|
'Calling %s inaccessible method %s from %s::%d',
|
||||||
|
static::class,
|
||||||
|
$name,
|
||||||
|
$caller['file'],
|
||||||
|
(int) $caller['line'],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
/**
|
||||||
|
* Copyright 2022-2025 FOSSBilling
|
||||||
|
* SPDX-License-Identifier: Apache-2.0.
|
||||||
|
*
|
||||||
|
* @copyright FOSSBilling (https://www.fossbilling.org)
|
||||||
|
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache-2.0
|
||||||
|
*/
|
||||||
|
if (!function_exists('__trans')) {
|
||||||
|
/**
|
||||||
|
* Translate a message with optional value substitution.
|
||||||
|
*/
|
||||||
|
function __trans(string $msgid, ?array $values = null): string
|
||||||
|
{
|
||||||
|
$translated = _gettext($msgid);
|
||||||
|
|
||||||
|
return is_array($values) ? strtr($translated, $values) : $translated;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('__pluralTrans')) {
|
||||||
|
/**
|
||||||
|
* Translate a plural message with optional value substitution.
|
||||||
|
*/
|
||||||
|
function __pluralTrans(string $msgid, string $msgidPlural, int $number, ?array $values = null): string
|
||||||
|
{
|
||||||
|
$translated = _ngettext($msgid, $msgidPlural, $number);
|
||||||
|
|
||||||
|
return is_array($values) ? strtr($translated, $values) : $translated;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -59,6 +59,7 @@ function checkInstaller(): void
|
|||||||
// This delete is irreversible, so it requires an explicit APP_ENV=prod rather than the ambiguous default above.
|
// This delete is irreversible, so it requires an explicit APP_ENV=prod rather than the ambiguous default above.
|
||||||
// @phpstan-ignore booleanNot.alwaysTrue (DEBUG is a runtime constant)
|
// @phpstan-ignore booleanNot.alwaysTrue (DEBUG is a runtime constant)
|
||||||
if (Environment::isExplicitlyProduction() && $filesystem->exists(PATH_CONFIG) && $filesystem->exists(Path::normalize('install')) && !DEBUG) {
|
if (Environment::isExplicitlyProduction() && $filesystem->exists(PATH_CONFIG) && $filesystem->exists(Path::normalize('install')) && !DEBUG) {
|
||||||
|
// Bootstrap runs before the DI logger and PHP error log are configured.
|
||||||
error_log('Removing the install directory now that installation is complete.');
|
error_log('Removing the install directory now that installation is complete.');
|
||||||
$filesystem->remove('install');
|
$filesystem->remove('install');
|
||||||
}
|
}
|
||||||
@@ -123,6 +124,7 @@ function hasDatabaseTables(): bool
|
|||||||
return $statement !== false && $statement->fetchColumn() !== false;
|
return $statement !== false && $statement->fetchColumn() !== false;
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
if ((bool) Config::getProperty('debug', false)) {
|
if ((bool) Config::getProperty('debug', false)) {
|
||||||
|
// Database inspection happens before the DI container is available.
|
||||||
error_log(sprintf(
|
error_log(sprintf(
|
||||||
'hasDatabaseTables() failed to inspect configured database tables: %s in %s on line %d',
|
'hasDatabaseTables() failed to inspect configured database tables: %s in %s on line %d',
|
||||||
$e->getMessage(),
|
$e->getMessage(),
|
||||||
@@ -160,6 +162,7 @@ function exceptionHandler(Exception|Error $e): void
|
|||||||
@file_put_contents(Path::join(PATH_LOG, 'exception_handler.log'), date('c') . ' ' . $msg, FILE_APPEND);
|
@file_put_contents(Path::join(PATH_LOG, 'exception_handler.log'), date('c') . ' ' . $msg, FILE_APPEND);
|
||||||
@file_put_contents(Path::join(PATH_ROOT, 'data', 'log', 'exception_handler.log'), date('c') . ' ' . $msg, FILE_APPEND);
|
@file_put_contents(Path::join(PATH_ROOT, 'data', 'log', 'exception_handler.log'), date('c') . ' ' . $msg, FILE_APPEND);
|
||||||
} else {
|
} else {
|
||||||
|
// The exception handler must also work when initialization itself fails.
|
||||||
error_log("{$e->getMessage()} at {$e->getFile()} : {$e->getLine()}");
|
error_log("{$e->getMessage()} at {$e->getFile()} : {$e->getLine()}");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,6 +242,7 @@ function preInit(): void
|
|||||||
|
|
||||||
// Define global paths.
|
// Define global paths.
|
||||||
define('PATH_LIBRARY', Path::join(PATH_ROOT, 'library'));
|
define('PATH_LIBRARY', Path::join(PATH_ROOT, 'library'));
|
||||||
|
require Path::join(PATH_LIBRARY, 'TranslationFunctions.php');
|
||||||
define('PATH_THEMES', Path::join(PATH_ROOT, 'themes'));
|
define('PATH_THEMES', Path::join(PATH_ROOT, 'themes'));
|
||||||
define('PATH_MODS', Path::join(PATH_ROOT, 'modules'));
|
define('PATH_MODS', Path::join(PATH_ROOT, 'modules'));
|
||||||
define('PATH_LANGS', Path::join(PATH_ROOT, 'locale'));
|
define('PATH_LANGS', Path::join(PATH_ROOT, 'locale'));
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ class Service implements InjectionAwareInterface
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
error_log($e->getMessage());
|
$di['logger']->error($e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ test('fetchNavigation returns array', function (): void {
|
|||||||
$di = container();
|
$di = container();
|
||||||
$link = 'activity';
|
$link = 'activity';
|
||||||
|
|
||||||
$urlMock = Mockery::mock('Box_Url');
|
$urlMock = Mockery::mock(FOSSBilling\Url::class);
|
||||||
/** @var Mockery\Expectation $expectation */
|
/** @var Mockery\Expectation $expectation */
|
||||||
$expectation = $urlMock->shouldReceive('adminLink');
|
$expectation = $urlMock->shouldReceive('adminLink');
|
||||||
$expectation->atLeast()->once();
|
$expectation->atLeast()->once();
|
||||||
|
|||||||
@@ -116,6 +116,35 @@ test('login events persist history entities', function (string $method, string $
|
|||||||
['onAfterAdminLogin', ActivityAdminHistory::class, 'getAdminId'],
|
['onAfterAdminLogin', ActivityAdminHistory::class, 'getAdminId'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
test('logs retention failures through the application logger', function (): void {
|
||||||
|
$dbal = Mockery::mock(Doctrine\DBAL\Connection::class);
|
||||||
|
$dbal->shouldReceive('executeStatement')
|
||||||
|
->once()
|
||||||
|
->andThrow(new RuntimeException('activity cleanup failed'));
|
||||||
|
|
||||||
|
$extensionService = Mockery::mock(Box\Mod\Extension\Service::class);
|
||||||
|
$extensionService->shouldReceive('getConfig')
|
||||||
|
->once()
|
||||||
|
->with('mod_activity')
|
||||||
|
->andReturn(['max_age' => 30, 'email_max_age' => 0]);
|
||||||
|
|
||||||
|
$logger = new Tests\Helpers\TestLogger();
|
||||||
|
$di = container();
|
||||||
|
$di['dbal'] = $dbal;
|
||||||
|
$di['logger'] = $logger;
|
||||||
|
$di['mod_service'] = $di->protect(fn (string $name): object => $extensionService);
|
||||||
|
|
||||||
|
$event = new Box_Event(null, 'onBeforeAdminCronRun');
|
||||||
|
$event->setDi($di);
|
||||||
|
|
||||||
|
Box\Mod\Activity\Service::onBeforeAdminCronRun($event);
|
||||||
|
|
||||||
|
expect($logger->calls)->toContain([
|
||||||
|
'method' => 'error',
|
||||||
|
'params' => ['activity cleanup failed'],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
test('log email', function (): void {
|
test('log email', function (): void {
|
||||||
$service = new Box\Mod\Activity\Service();
|
$service = new Box\Mod\Activity\Service();
|
||||||
$data = [
|
$data = [
|
||||||
|
|||||||
@@ -432,7 +432,7 @@ class Client implements InjectionAwareInterface
|
|||||||
$this->_loadConfig();
|
$this->_loadConfig();
|
||||||
|
|
||||||
if ($e instanceof \Exception) {
|
if ($e instanceof \Exception) {
|
||||||
error_log("{$e->getMessage()} {$e->getCode()}.");
|
$this->getDi()['logger']->error("{$e->getMessage()} {$e->getCode()}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return (new ApiResponseFactory())->create($data, $e);
|
return (new ApiResponseFactory())->create($data, $e);
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ class Admin extends \FOSSBilling\Api\AbstractApi
|
|||||||
$conn->executeStatement('DELETE FROM cart_product WHERE cart_id = :id', ['id' => $id]);
|
$conn->executeStatement('DELETE FROM cart_product WHERE cart_id = :id', ['id' => $id]);
|
||||||
$conn->executeStatement('DELETE FROM cart WHERE id = :id', ['id' => $id]);
|
$conn->executeStatement('DELETE FROM cart WHERE id = :id', ['id' => $id]);
|
||||||
}
|
}
|
||||||
$this->getDi()['logger']->info('Removed %s expired shopping carts', \FOSSBilling\Tools::safeCount($expiredCarts));
|
$this->getDi()['logger']->info('Removed {expired_count} expired shopping carts', ['expired_count' => \FOSSBilling\Tools::safeCount($expiredCarts)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ class Service implements InjectionAwareInterface
|
|||||||
$this->addProduct($cart, $productFromList, $productFromListConfig);
|
$this->addProduct($cart, $productFromList, $productFromListConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->di['logger']->info('Added "%s" to shopping cart', $this->getProductTitle($product));
|
$this->di['logger']->info('Added "{product_title}" to shopping cart', ['product_title' => $this->getProductTitle($product)]);
|
||||||
|
|
||||||
$this->di['events_manager']->fire(['event' => 'onAfterProductAddedToCart', 'params' => $event_params]);
|
$this->di['events_manager']->fire(['event' => 'onAfterProductAddedToCart', 'params' => $event_params]);
|
||||||
|
|
||||||
@@ -366,7 +366,7 @@ class Service implements InjectionAwareInterface
|
|||||||
$cart->setCurrencyId($currency->getId());
|
$cart->setCurrencyId($currency->getId());
|
||||||
$this->persistCart($cart);
|
$this->persistCart($cart);
|
||||||
|
|
||||||
$this->di['logger']->info('Changed shopping cart #%s currency to %s', $cart->getId(), $currency->getCode());
|
$this->di['logger']->info('Changed shopping cart #{cart_id} currency to {currency_code}', ['cart_id' => $cart->getId(), 'currency_code' => $currency->getCode()]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -390,7 +390,7 @@ class Service implements InjectionAwareInterface
|
|||||||
$cart->setUpdatedAt(new \DateTime());
|
$cart->setUpdatedAt(new \DateTime());
|
||||||
$this->persistCart($cart);
|
$this->persistCart($cart);
|
||||||
|
|
||||||
$this->di['logger']->info('Removed promo code from shopping cart #%s', $cart->getId());
|
$this->di['logger']->info('Removed promo code from shopping cart #{cart_id}', ['cart_id' => $cart->getId()]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -411,7 +411,7 @@ class Service implements InjectionAwareInterface
|
|||||||
$cart->setPromoId($promoId);
|
$cart->setPromoId($promoId);
|
||||||
$this->persistCart($cart);
|
$this->persistCart($cart);
|
||||||
|
|
||||||
$this->di['logger']->info('Applied promo code %s to shopping cart', $promoCode);
|
$this->di['logger']->info('Applied promo code {promo_code} to shopping cart', ['promo_code' => $promoCode]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -627,6 +627,8 @@ class Service implements InjectionAwareInterface
|
|||||||
$promoProductService = $promoId ? $this->getProductService() : null;
|
$promoProductService = $promoId ? $this->getProductService() : null;
|
||||||
$promo = $promoId ? $promoProductService?->findPromoById($promoId) : null;
|
$promo = $promoId ? $promoProductService?->findPromoById($promoId) : null;
|
||||||
|
|
||||||
|
$reservedOrderIds = [];
|
||||||
|
$reservedCount = 0;
|
||||||
$stockReservedOrders = [];
|
$stockReservedOrders = [];
|
||||||
|
|
||||||
if (!$client->getCurrency()) {
|
if (!$client->getCurrency()) {
|
||||||
@@ -635,7 +637,7 @@ class Service implements InjectionAwareInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return $this->di['em']->wrapInTransaction(function () use ($ca, $cart, $client, $currency, $currencyCode, $gateway_id, $taxed, $promo, $promoProductService, $promoId, &$stockReservedOrders) {
|
return $this->di['em']->wrapInTransaction(function () use ($ca, $cart, $client, $currency, $currencyCode, $gateway_id, $taxed, $promo, $promoProductService, $promoId, &$reservedOrderIds, &$reservedCount, &$stockReservedOrders) {
|
||||||
if ($client->getCurrency() != $currencyCode) {
|
if ($client->getCurrency() != $currencyCode) {
|
||||||
throw new \FOSSBilling\InformationException('Selected currency :selected does not match your profile currency :code. Please change cart currency to continue.', [':selected' => $currencyCode, ':code' => $client->getCurrency()]);
|
throw new \FOSSBilling\InformationException('Selected currency :selected does not match your profile currency :code. Please change cart currency to continue.', [':selected' => $currencyCode, ':code' => $client->getCurrency()]);
|
||||||
}
|
}
|
||||||
@@ -710,8 +712,10 @@ class Service implements InjectionAwareInterface
|
|||||||
$stockReservedOrders[] = $order;
|
$stockReservedOrders[] = $order;
|
||||||
|
|
||||||
// Reserve promo capacity at order creation time.
|
// Reserve promo capacity at order creation time.
|
||||||
if ($promo instanceof Promo && $promoProductService !== null) {
|
if ($promo instanceof Promo) {
|
||||||
$promoProductService->reservePromoForOrder($promo, $order);
|
$promoProductService->reservePromoForOrder($promo, $order);
|
||||||
|
$reservedOrderIds[] = $order->getId();
|
||||||
|
++$reservedCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
$orderService = $this->di['mod_service']('order');
|
$orderService = $this->di['mod_service']('order');
|
||||||
@@ -781,7 +785,7 @@ class Service implements InjectionAwareInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($promo instanceof Promo && $promoProductService !== null) {
|
if ($promo instanceof Promo) {
|
||||||
$redemptionStatus = $invoiceModel instanceof Invoice
|
$redemptionStatus = $invoiceModel instanceof Invoice
|
||||||
&& $invoiceModel->getStatus() === Invoice::STATUS_UNPAID
|
&& $invoiceModel->getStatus() === Invoice::STATUS_UNPAID
|
||||||
? \Box\Mod\Product\Entity\PromoRedemption::STATUS_RESERVED
|
? \Box\Mod\Product\Entity\PromoRedemption::STATUS_RESERVED
|
||||||
@@ -818,7 +822,7 @@ class Service implements InjectionAwareInterface
|
|||||||
// An escaped failure here would roll back the whole
|
// An escaped failure here would roll back the whole
|
||||||
// wrapInTransaction() below, including every order
|
// wrapInTransaction() below, including every order
|
||||||
// already created for this cart - not just this one.
|
// already created for this cart - not just this one.
|
||||||
$this->di['logger']->error('Order activation failed after checkout: %s', $e->getMessage());
|
$this->di['logger']->error('Order activation failed after checkout: {exception}', ['exception' => $e]);
|
||||||
$notes = "Order could not be activated after checkout due to error: {$e->getMessage()}.";
|
$notes = "Order could not be activated after checkout due to error: {$e->getMessage()}.";
|
||||||
$orderService->orderStatusAdd($order, Order::STATUS_FAILED_SETUP, $notes);
|
$orderService->orderStatusAdd($order, Order::STATUS_FAILED_SETUP, $notes);
|
||||||
}
|
}
|
||||||
@@ -831,12 +835,23 @@ class Service implements InjectionAwareInterface
|
|||||||
];
|
];
|
||||||
});
|
});
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
|
if ($promo instanceof Promo && $reservedCount > 0) {
|
||||||
|
try {
|
||||||
|
$promoProductService->compensateCheckoutPromoFailure($promo, $reservedOrderIds, $reservedCount);
|
||||||
|
} catch (\Throwable $compensationError) {
|
||||||
|
$this->di['logger']->error('Failed to compensate promo checkout failure', [
|
||||||
|
'exception' => $compensationError,
|
||||||
|
'promo_id' => $promo->getId(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($stockReservedOrders as $stockReservedOrder) {
|
foreach ($stockReservedOrders as $stockReservedOrder) {
|
||||||
try {
|
try {
|
||||||
$this->getProductService()->releaseReservedStockForOrder($stockReservedOrder, 'checkout_failed');
|
$this->getProductService()->releaseReservedStockForOrder($stockReservedOrder, 'checkout_failed');
|
||||||
} catch (\Throwable $compensationError) {
|
} catch (\Throwable $compensationError) {
|
||||||
$this->di['logger']->error('Failed to compensate stock checkout failure', [
|
$this->di['logger']->error('Failed to compensate stock checkout failure', [
|
||||||
'exception' => $compensationError->getMessage(),
|
'exception' => $compensationError,
|
||||||
'order_id' => $stockReservedOrder->getId(),
|
'order_id' => $stockReservedOrder->getId(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ test('get returns array', function (): void {
|
|||||||
test('batchExpire returns true', function (): void {
|
test('batchExpire returns true', function (): void {
|
||||||
$adminApi = apiEndpoint(new Box\Mod\Cart\Api\Admin());
|
$adminApi = apiEndpoint(new Box\Mod\Cart\Api\Admin());
|
||||||
|
|
||||||
$logStub = $this->createStub('\Box_Log');
|
$logStub = $this->createStub('\FOSSBilling\Logger');
|
||||||
|
|
||||||
$conn = Mockery::mock(Doctrine\DBAL\Connection::class);
|
$conn = Mockery::mock(Doctrine\DBAL\Connection::class);
|
||||||
$conn->shouldReceive('fetchAllKeyValue')->atLeast()->once()->andReturn([1 => date('Y-m-d H:i:s')]);
|
$conn->shouldReceive('fetchAllKeyValue')->atLeast()->once()->andReturn([1 => date('Y-m-d H:i:s')]);
|
||||||
|
|||||||
@@ -889,6 +889,107 @@ test('createFromCart with promo entity uses product promo service', function ():
|
|||||||
expect(count($result[2]))->toBe(1);
|
expect(count($result[2]))->toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('createFromCart compensates promo usage on transaction failure', function (): void {
|
||||||
|
$cart = createEntity(Cart::class);
|
||||||
|
$cart->id = 3;
|
||||||
|
$cart->currency_id = 2;
|
||||||
|
$cart->promo_id = 7;
|
||||||
|
|
||||||
|
$client = createEntity(Client::class);
|
||||||
|
$client->id = 9;
|
||||||
|
$client->currency = 'USD';
|
||||||
|
|
||||||
|
$currency = Mockery::mock(Currency::class)->makePartial();
|
||||||
|
$currency->shouldReceive('getCode')->once()->andReturn('USD');
|
||||||
|
$currency->shouldReceive('getConversionRate')->atLeast()->once()->andReturn(1.0);
|
||||||
|
|
||||||
|
$currencyRepository = Mockery::mock(CurrencyRepository::class);
|
||||||
|
$currencyRepository->shouldReceive('find')->once()->with(2)->andReturn($currency);
|
||||||
|
|
||||||
|
$currencyService = Mockery::mock(CurrencyService::class);
|
||||||
|
$currencyService->shouldReceive('getCurrencyRepository')->once()->andReturn($currencyRepository);
|
||||||
|
|
||||||
|
$clientService = Mockery::mock(Box\Mod\Client\Service::class);
|
||||||
|
$clientService->shouldReceive('isClientTaxable')->once()->with($client)->andReturn(false);
|
||||||
|
|
||||||
|
$promo = new Promo();
|
||||||
|
$promo->setCode('PROMO');
|
||||||
|
$promoIdReflection = new ReflectionProperty($promo, 'id');
|
||||||
|
$promoIdReflection->setValue($promo, 7);
|
||||||
|
|
||||||
|
$productService = Mockery::mock(ProductService::class);
|
||||||
|
$productService->shouldReceive('findPromoById')->once()->with(7)->andReturn($promo);
|
||||||
|
$productService->shouldReceive('reserveStockForOrder')->once()->with(Mockery::type(Order::class));
|
||||||
|
$productService->shouldReceive('reservePromoForOrder')->once()->with($promo, Mockery::type(Order::class));
|
||||||
|
$productService->shouldReceive('createCheckoutPromoRedemptions')
|
||||||
|
->andThrow(new RuntimeException('Doctrine flush failed'));
|
||||||
|
$productService->shouldReceive('compensateCheckoutPromoFailure')
|
||||||
|
->once()
|
||||||
|
->with($promo, Mockery::any(), Mockery::any());
|
||||||
|
$productService->shouldReceive('releaseReservedStockForOrder')
|
||||||
|
->once()
|
||||||
|
->with(Mockery::type(Order::class), 'checkout_failed');
|
||||||
|
|
||||||
|
$product = new Product();
|
||||||
|
$productIdReflection = new ReflectionProperty($product, 'id');
|
||||||
|
$productIdReflection->setValue($product, 5);
|
||||||
|
$product->setStatus('enabled');
|
||||||
|
$product->setType('service');
|
||||||
|
$product->setSetup('manual');
|
||||||
|
|
||||||
|
$cartProduct = createEntity(CartProduct::class);
|
||||||
|
$cartProduct->id = 13;
|
||||||
|
|
||||||
|
$orderService = Mockery::mock(Box\Mod\Order\Service::class)->makePartial();
|
||||||
|
$orderService->shouldReceive('saveStatusChange')->once()->with(Mockery::type(Order::class), 'Order Created');
|
||||||
|
|
||||||
|
$emMock = Mockery::mock(Doctrine\ORM\EntityManagerInterface::class);
|
||||||
|
$emMock->shouldReceive('wrapInTransaction')->once()->with(Mockery::type(Closure::class))->andReturnUsing(fn (Closure $callback) => $callback());
|
||||||
|
$emMock->shouldReceive('persist')->atLeast()->once();
|
||||||
|
$emMock->shouldReceive('flush')->atLeast()->once();
|
||||||
|
|
||||||
|
$serviceMock = Mockery::mock(Service::class)->makePartial();
|
||||||
|
$serviceMock->shouldReceive('getSessionCart')->once()->andReturn($cart);
|
||||||
|
$serviceMock->shouldReceive('toApiArray')->once()->with($cart)->andReturn([
|
||||||
|
'items' => [['id' => 1]],
|
||||||
|
'total' => 0,
|
||||||
|
]);
|
||||||
|
$serviceMock->shouldReceive('getCartProducts')->once()->with($cart)->andReturn([$cartProduct]);
|
||||||
|
$serviceMock->shouldReceive('cartProductToApiArray')->once()->with($cartProduct)->andReturn([
|
||||||
|
'product_id' => 5,
|
||||||
|
'form_id' => null,
|
||||||
|
'title' => 'Example product',
|
||||||
|
'type' => 'service',
|
||||||
|
'unit' => 'service',
|
||||||
|
'period' => '1M',
|
||||||
|
'quantity' => 1,
|
||||||
|
'price' => 0,
|
||||||
|
'discount_price' => 0,
|
||||||
|
'setup_price' => 0,
|
||||||
|
'discount_setup' => 0,
|
||||||
|
'notes' => null,
|
||||||
|
]);
|
||||||
|
$serviceMock->shouldReceive('isStockAvailable')->once()->with($product, 1)->andReturn(true);
|
||||||
|
|
||||||
|
$productService->shouldReceive('findProductById')->once()->with(5)->andReturn($product);
|
||||||
|
|
||||||
|
$di = container();
|
||||||
|
$di['em'] = $emMock;
|
||||||
|
$di['logger'] = new FOSSBilling\Logger();
|
||||||
|
$di['mod_service'] = $di->protect(fn ($serviceName, $sub = '') => match ($serviceName) {
|
||||||
|
'currency' => $currencyService,
|
||||||
|
'client' => $clientService,
|
||||||
|
'Product' => $productService,
|
||||||
|
'order', 'Order' => $orderService,
|
||||||
|
default => null,
|
||||||
|
});
|
||||||
|
|
||||||
|
$serviceMock->setDi($di);
|
||||||
|
|
||||||
|
expect(fn () => $serviceMock->createFromCart($client))
|
||||||
|
->toThrow(RuntimeException::class, 'Doctrine flush failed');
|
||||||
|
});
|
||||||
|
|
||||||
test('createFromCart releases reserved stock on transaction failure', function (): void {
|
test('createFromCart releases reserved stock on transaction failure', function (): void {
|
||||||
$cart = createEntity(Cart::class);
|
$cart = createEntity(Cart::class);
|
||||||
$cart->id = 3;
|
$cart->id = 3;
|
||||||
@@ -975,7 +1076,7 @@ test('createFromCart releases reserved stock on transaction failure', function (
|
|||||||
|
|
||||||
$di = container();
|
$di = container();
|
||||||
$di['em'] = $emMock;
|
$di['em'] = $emMock;
|
||||||
$di['logger'] = new Box_Log();
|
$di['logger'] = new FOSSBilling\Logger();
|
||||||
$di['mod_service'] = $di->protect(fn ($serviceName, $sub = '') => match ($serviceName) {
|
$di['mod_service'] = $di->protect(fn ($serviceName, $sub = '') => match ($serviceName) {
|
||||||
'currency' => $currencyService,
|
'currency' => $currencyService,
|
||||||
'client' => $clientService,
|
'client' => $clientService,
|
||||||
@@ -1073,7 +1174,7 @@ test('createFromCart does not roll back order creation when synchronous activati
|
|||||||
|
|
||||||
$di = container();
|
$di = container();
|
||||||
$di['em'] = $emMock;
|
$di['em'] = $emMock;
|
||||||
$di['logger'] = new Box_Log();
|
$di['logger'] = new FOSSBilling\Logger();
|
||||||
$di['mod_service'] = $di->protect(fn ($serviceName, $sub = '') => match ($serviceName) {
|
$di['mod_service'] = $di->protect(fn ($serviceName, $sub = '') => match ($serviceName) {
|
||||||
'currency' => $currencyService,
|
'currency' => $currencyService,
|
||||||
'client' => $clientService,
|
'client' => $clientService,
|
||||||
@@ -1451,7 +1552,7 @@ test('addItem for hosting type returns true', function (): void {
|
|||||||
|
|
||||||
return $serviceHostingServiceMock;
|
return $serviceHostingServiceMock;
|
||||||
});
|
});
|
||||||
$di['logger'] = new Box_Log();
|
$di['logger'] = new FOSSBilling\Logger();
|
||||||
|
|
||||||
$productService->setDi($di);
|
$productService->setDi($di);
|
||||||
$serviceMock->setDi($di);
|
$serviceMock->setDi($di);
|
||||||
@@ -1497,7 +1598,7 @@ test('addItem for license type returns true', function (): void {
|
|||||||
|
|
||||||
return $serviceLicenseServiceMock;
|
return $serviceLicenseServiceMock;
|
||||||
});
|
});
|
||||||
$di['logger'] = new Box_Log();
|
$di['logger'] = new FOSSBilling\Logger();
|
||||||
|
|
||||||
$productService->setDi($di);
|
$productService->setDi($di);
|
||||||
$serviceMock->setDi($di);
|
$serviceMock->setDi($di);
|
||||||
@@ -1543,7 +1644,7 @@ test('addItem for custom type returns true', function (): void {
|
|||||||
|
|
||||||
return $serviceCustomServiceMock;
|
return $serviceCustomServiceMock;
|
||||||
});
|
});
|
||||||
$di['logger'] = new Box_Log();
|
$di['logger'] = new FOSSBilling\Logger();
|
||||||
|
|
||||||
$productService->setDi($di);
|
$productService->setDi($di);
|
||||||
$serviceMock->setDi($di);
|
$serviceMock->setDi($di);
|
||||||
@@ -1857,7 +1958,7 @@ test('addItem strips client-injected hosting_plan_id', function (): void {
|
|||||||
|
|
||||||
return $serviceHostingServiceMock;
|
return $serviceHostingServiceMock;
|
||||||
});
|
});
|
||||||
$di['logger'] = new Box_Log();
|
$di['logger'] = new FOSSBilling\Logger();
|
||||||
|
|
||||||
$productService->setDi($di);
|
$productService->setDi($di);
|
||||||
$serviceHostingServiceMock->setDi($di);
|
$serviceHostingServiceMock->setDi($di);
|
||||||
|
|||||||
@@ -120,11 +120,11 @@ class Admin extends \FOSSBilling\Api\AbstractApi
|
|||||||
|
|
||||||
$session = $this->getDi()['session'];
|
$session = $this->getDi()['session'];
|
||||||
$session->set('client_id', $client->getId());
|
$session->set('client_id', $client->getId());
|
||||||
$this->getDi()['logger']->info('Logged in as client #%s', $client->getId());
|
$this->getDi()['logger']->info('Logged in as client #{client_id}', ['client_id' => $client->getId()]);
|
||||||
|
|
||||||
if (Tools::normalizeBoolean($data['strip_admin_identity'] ?? false)) {
|
if (Tools::normalizeBoolean($data['strip_admin_identity'] ?? false)) {
|
||||||
$session->destroy('admin');
|
$session->destroy('admin');
|
||||||
$this->getDi()['logger']->info('Stripped admin identity from session after logging in as client #%s', $client->getId());
|
$this->getDi()['logger']->info('Stripped admin identity from session after logging in as client #{client_id}', ['client_id' => $client->getId()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
@@ -238,7 +238,7 @@ class Admin extends \FOSSBilling\Api\AbstractApi
|
|||||||
$this->getService()->remove($model);
|
$this->getService()->remove($model);
|
||||||
$this->getDi()['events_manager']->fire(['event' => 'onAfterAdminClientDelete', 'params' => ['id' => $clientId]]);
|
$this->getDi()['events_manager']->fire(['event' => 'onAfterAdminClientDelete', 'params' => ['id' => $clientId]]);
|
||||||
|
|
||||||
$this->getDi()['logger']->info('Removed client #%s', $clientId);
|
$this->getDi()['logger']->info('Removed client #{client_id}', ['client_id' => $clientId]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -459,7 +459,7 @@ class Admin extends \FOSSBilling\Api\AbstractApi
|
|||||||
|
|
||||||
$this->getDi()['events_manager']->fire(['event' => 'onAfterAdminClientUpdate', 'params' => ['id' => $client->getId()]]);
|
$this->getDi()['events_manager']->fire(['event' => 'onAfterAdminClientUpdate', 'params' => ['id' => $client->getId()]]);
|
||||||
|
|
||||||
$this->getDi()['logger']->info('Updated client #%s profile', $client->getId());
|
$this->getDi()['logger']->info('Updated client #{client_id} profile', ['client_id' => $client->getId()]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -489,7 +489,7 @@ class Admin extends \FOSSBilling\Api\AbstractApi
|
|||||||
|
|
||||||
$this->getDi()['events_manager']->fire(['event' => 'onAfterAdminClientPasswordChange', 'params' => ['id' => $client->getId()]]);
|
$this->getDi()['events_manager']->fire(['event' => 'onAfterAdminClientPasswordChange', 'params' => ['id' => $client->getId()]]);
|
||||||
|
|
||||||
$this->getDi()['logger']->info('Changed client #%s password', $client->getId());
|
$this->getDi()['logger']->info('Changed client #{client_id} password', ['client_id' => $client->getId()]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -537,7 +537,7 @@ class Admin extends \FOSSBilling\Api\AbstractApi
|
|||||||
$this->getDi()['em']->remove($model);
|
$this->getDi()['em']->remove($model);
|
||||||
$this->getDi()['em']->flush();
|
$this->getDi()['em']->flush();
|
||||||
|
|
||||||
$this->getDi()['logger']->info('Removed line %s from client #%s balance for %s', $id, $client_id, $amount);
|
$this->getDi()['logger']->info('Removed line {id} from client #{client_id} balance for {amount}', ['id' => $id, 'client_id' => $client_id, 'amount' => $amount]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ class Guest extends \FOSSBilling\Api\AbstractApi
|
|||||||
try {
|
try {
|
||||||
$this->login(['email' => $client->getEmail(), 'password' => $data['password']]);
|
$this->login(['email' => $client->getEmail(), 'password' => $data['password']]);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
error_log($e->getMessage());
|
$this->getDi()['logger']->error($e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,7 +145,7 @@ class Guest extends \FOSSBilling\Api\AbstractApi
|
|||||||
$result = $service->toSessionArray($client);
|
$result = $service->toSessionArray($client);
|
||||||
$this->getDi()['session']->set('client_id', $client->getId());
|
$this->getDi()['session']->set('client_id', $client->getId());
|
||||||
|
|
||||||
$this->getDi()['logger']->info('Client #%s logged in', $client->getId());
|
$this->getDi()['logger']->info('Client #{client_id} logged in', ['client_id' => $client->getId()]);
|
||||||
$this->getDi()['session']->delete('redirect_uri');
|
$this->getDi()['session']->delete('redirect_uri');
|
||||||
|
|
||||||
if (!empty($client->getLang())) {
|
if (!empty($client->getLang())) {
|
||||||
@@ -179,32 +179,32 @@ class Guest extends \FOSSBilling\Api\AbstractApi
|
|||||||
|
|
||||||
$ipLimit = $this->getDi()['rate_limiter']->consume('client_password_reset_ip', (string) $this->getIp());
|
$ipLimit = $this->getDi()['rate_limiter']->consume('client_password_reset_ip', (string) $this->getIp());
|
||||||
if ($ipLimit->isLimited()) {
|
if ($ipLimit->isLimited()) {
|
||||||
$this->getDi()['logger']->setChannel('security')->info('Client password reset rate limited from IP %s: email %s', $this->getIp(), $data['email']);
|
$this->getDi()['logger']->withChannel('security')->info('Client password reset rate limited from IP {ip}.', ['ip' => $this->getIp()]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
$emailLimit = $this->getDi()['rate_limiter']->consume('client_password_reset_email', (string) $data['email']);
|
$emailLimit = $this->getDi()['rate_limiter']->consume('client_password_reset_email', (string) $data['email']);
|
||||||
if ($emailLimit->isLimited()) {
|
if ($emailLimit->isLimited()) {
|
||||||
$this->getDi()['logger']->setChannel('security')->info('Client password reset rate limited for email %s from IP %s', $data['email'], $this->getIp());
|
$this->getDi()['logger']->withChannel('security')->info('Client password reset rate limited for an account from IP {ip}.', ['ip' => $this->getIp()]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->checkPasswordResetCaptcha($data);
|
$this->checkCaptchaIfEnabled($data);
|
||||||
|
|
||||||
$this->getDi()['events_manager']->fire(['event' => 'onBeforeGuestPasswordResetRequest', 'params' => $data]);
|
$this->getDi()['events_manager']->fire(['event' => 'onBeforeGuestPasswordResetRequest', 'params' => $data]);
|
||||||
|
|
||||||
$em = $this->getDi()['em'];
|
$em = $this->getDi()['em'];
|
||||||
$client = $em->getRepository(Client::class)->findOneByEmailAndActive($data['email']);
|
$client = $em->getRepository(Client::class)->findOneByEmailAndActive($data['email']);
|
||||||
if (!$client instanceof Client) {
|
if (!$client instanceof Client) {
|
||||||
$this->getDi()['logger']->setChannel('security')->info('Client password reset requested for unknown email %s from IP %s', $data['email'], $this->getIp());
|
$this->getDi()['logger']->withChannel('security')->info('Client password reset requested for an unknown account from IP {ip}.', ['ip' => $this->getIp()]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($client->getStatus() !== Client::ACTIVE) {
|
if ($client->getStatus() !== Client::ACTIVE) {
|
||||||
$this->getDi()['logger']->setChannel('security')->info('Client password reset requested for ineligible client #%s from IP %s: email %s, account status %s', $client->getId(), $this->getIp(), $data['email'], $client->getStatus());
|
$this->getDi()['logger']->withChannel('security')->info('Client password reset requested for ineligible client #{client_id} from IP {ip}: account status {status}.', ['client_id' => $client->getId(), 'ip' => $this->getIp(), 'status' => $client->getStatus()]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -212,7 +212,7 @@ class Guest extends \FOSSBilling\Api\AbstractApi
|
|||||||
$hash = $service->createPasswordResetRequestForClient($client);
|
$hash = $service->createPasswordResetRequestForClient($client);
|
||||||
$service->sendPasswordResetRequestEmailForClient($client, $hash);
|
$service->sendPasswordResetRequestEmailForClient($client, $hash);
|
||||||
|
|
||||||
$this->getDi()['logger']->setChannel('security')->info('Client password reset email queued for client #%s from IP %s: email %s', $client->getId(), $this->getIp(), $data['email']);
|
$this->getDi()['logger']->withChannel('security')->info('Client password reset email queued for client #{client_id} from IP {ip}.', ['client_id' => $client->getId(), 'ip' => $this->getIp()]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -220,16 +220,6 @@ class Guest extends \FOSSBilling\Api\AbstractApi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function checkPasswordResetCaptcha(array $data): void
|
|
||||||
{
|
|
||||||
$extensionService = $this->getDi()['mod_service']('extension');
|
|
||||||
if (!$extensionService->isExtensionActive('mod', 'antispam')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->getDi()['mod_service']('Antispam')->checkCaptcha($data);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[RequiredParams(['hash' => 'No Hash provided', 'password' => 'Password required', 'password_confirm' => 'Password confirmation required'])]
|
#[RequiredParams(['hash' => 'No Hash provided', 'password' => 'Password required', 'password_confirm' => 'Password confirmation required'])]
|
||||||
public function update_password($data): bool
|
public function update_password($data): bool
|
||||||
{
|
{
|
||||||
@@ -246,13 +236,13 @@ class Guest extends \FOSSBilling\Api\AbstractApi
|
|||||||
$em = $this->getDi()['em'];
|
$em = $this->getDi()['em'];
|
||||||
$reset = $em->getRepository(ClientPasswordReset::class)->findOneByHash($data['hash']);
|
$reset = $em->getRepository(ClientPasswordReset::class)->findOneByHash($data['hash']);
|
||||||
if (!$reset instanceof ClientPasswordReset) {
|
if (!$reset instanceof ClientPasswordReset) {
|
||||||
$this->getDi()['logger']->setChannel('security')->info('Client password reset confirmation failed from IP %s: reset token not found', $this->getIp());
|
$this->getDi()['logger']->withChannel('security')->info('Client password reset confirmation failed from IP {ip}: reset token not found', ['ip' => $this->getIp()]);
|
||||||
|
|
||||||
throw new \FOSSBilling\InformationException('The link has expired or you have already reset your password.');
|
throw new \FOSSBilling\InformationException('The link has expired or you have already reset your password.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (strtotime((string) $reset->getCreatedAt()?->format('Y-m-d H:i:s')) - time() + 900 < 0) {
|
if (strtotime((string) $reset->getCreatedAt()?->format('Y-m-d H:i:s')) - time() + 900 < 0) {
|
||||||
$this->getDi()['logger']->setChannel('security')->info('Client password reset confirmation failed for client #%s from IP %s: reset token expired', $reset->getClient()?->getId(), $this->getIp());
|
$this->getDi()['logger']->withChannel('security')->info('Client password reset confirmation failed for client #{client_id} from IP {ip}: reset token expired', ['client_id' => $reset->getClient()?->getId(), 'ip' => $this->getIp()]);
|
||||||
|
|
||||||
throw new \FOSSBilling\InformationException('The link has expired or you have already reset your password.');
|
throw new \FOSSBilling\InformationException('The link has expired or you have already reset your password.');
|
||||||
}
|
}
|
||||||
@@ -263,7 +253,7 @@ class Guest extends \FOSSBilling\Api\AbstractApi
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($client->getStatus() !== Client::ACTIVE) {
|
if ($client->getStatus() !== Client::ACTIVE) {
|
||||||
$this->getDi()['logger']->setChannel('security')->info('Client password reset confirmation failed for client #%s from IP %s: account status %s', $client->getId(), $this->getIp(), $client->getStatus());
|
$this->getDi()['logger']->withChannel('security')->info('Client password reset confirmation failed for client #{client_id} from IP {ip}: account status {status}', ['client_id' => $client->getId(), 'ip' => $this->getIp(), 'status' => $client->getStatus()]);
|
||||||
|
|
||||||
throw new \FOSSBilling\InformationException('The link has expired or you have already reset your password.');
|
throw new \FOSSBilling\InformationException('The link has expired or you have already reset your password.');
|
||||||
}
|
}
|
||||||
@@ -276,7 +266,7 @@ class Guest extends \FOSSBilling\Api\AbstractApi
|
|||||||
$profileService = $this->getDi()['mod_service']('profile');
|
$profileService = $this->getDi()['mod_service']('profile');
|
||||||
$profileService->invalidateSessions('client', (int) $client->getId());
|
$profileService->invalidateSessions('client', (int) $client->getId());
|
||||||
|
|
||||||
$this->getDi()['logger']->setChannel('security')->info('Client password reset completed for client #%s from IP %s', $client->getId(), $this->getIp());
|
$this->getDi()['logger']->withChannel('security')->info('Client password reset completed for client #{client_id} from IP {ip}', ['client_id' => $client->getId(), 'ip' => $this->getIp()]);
|
||||||
|
|
||||||
// send email
|
// send email
|
||||||
$email = [];
|
$email = [];
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ class Service implements InjectionAwareInterface
|
|||||||
|
|
||||||
$emailService->sendTemplate($email);
|
$emailService->sendTemplate($email);
|
||||||
} catch (\Exception $exc) {
|
} catch (\Exception $exc) {
|
||||||
$di['logger']->setChannel('email')->error('Failed to send client signup email', ['exception' => $exc->getMessage()]);
|
$di['logger']->withChannel('email')->error('Failed to send client signup email', ['exception' => $exc]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -571,7 +571,7 @@ class Service implements InjectionAwareInterface
|
|||||||
$this->di['em']->persist($group);
|
$this->di['em']->persist($group);
|
||||||
$this->di['em']->flush();
|
$this->di['em']->flush();
|
||||||
|
|
||||||
$this->di['logger']->info('Created new client group #%s', $group->getId());
|
$this->di['logger']->info('Created new client group #{group_id}', ['group_id' => $group->getId()]);
|
||||||
|
|
||||||
return (int) $group->getId();
|
return (int) $group->getId();
|
||||||
}
|
}
|
||||||
@@ -588,7 +588,7 @@ class Service implements InjectionAwareInterface
|
|||||||
$this->di['em']->remove($group);
|
$this->di['em']->remove($group);
|
||||||
$this->di['em']->flush();
|
$this->di['em']->flush();
|
||||||
}
|
}
|
||||||
$this->di['logger']->info('Removed client group #%s', $model->getId());
|
$this->di['logger']->info('Removed client group #{model_id}', ['model_id' => $model->getId()]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -701,7 +701,7 @@ class Service implements InjectionAwareInterface
|
|||||||
$this->sendAdminCreatedWelcomeEmailForClient($client);
|
$this->sendAdminCreatedWelcomeEmailForClient($client);
|
||||||
}
|
}
|
||||||
$this->di['events_manager']->fire(['event' => 'onAfterAdminCreateClient', 'params' => ['id' => $client->getId()]]);
|
$this->di['events_manager']->fire(['event' => 'onAfterAdminCreateClient', 'params' => ['id' => $client->getId()]]);
|
||||||
$this->di['logger']->info('Created new client #%s', $client->getId());
|
$this->di['logger']->info('Created new client #{client_id}', ['client_id' => $client->getId()]);
|
||||||
|
|
||||||
return (int) $client->getId();
|
return (int) $client->getId();
|
||||||
}
|
}
|
||||||
@@ -745,7 +745,7 @@ class Service implements InjectionAwareInterface
|
|||||||
'ip' => $safeData['ip'],
|
'ip' => $safeData['ip'],
|
||||||
];
|
];
|
||||||
$this->di['events_manager']->fire(['event' => 'onAfterClientSignUp', 'params' => $event_params]);
|
$this->di['events_manager']->fire(['event' => 'onAfterClientSignUp', 'params' => $event_params]);
|
||||||
$this->di['logger']->info('Client #%s signed up', $client->getId());
|
$this->di['logger']->info('Client #{client_id} signed up', ['client_id' => $client->getId()]);
|
||||||
|
|
||||||
return $client;
|
return $client;
|
||||||
}
|
}
|
||||||
@@ -813,7 +813,7 @@ class Service implements InjectionAwareInterface
|
|||||||
$emailService = $this->di['mod_service']('email');
|
$emailService = $this->di['mod_service']('email');
|
||||||
$emailService->sendTemplate($email);
|
$emailService->sendTemplate($email);
|
||||||
} catch (\Exception $exc) {
|
} catch (\Exception $exc) {
|
||||||
$this->di['logger']->setChannel('email')->error('Failed to send client welcome email', ['exception' => $exc->getMessage()]);
|
$this->di['logger']->withChannel('email')->error('Failed to send client welcome email', ['exception' => $exc]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -889,7 +889,7 @@ class Service implements InjectionAwareInterface
|
|||||||
$emailService = $this->di['mod_service']('email');
|
$emailService = $this->di['mod_service']('email');
|
||||||
$emailService->sendTemplate($email);
|
$emailService->sendTemplate($email);
|
||||||
} catch (\Exception $exc) {
|
} catch (\Exception $exc) {
|
||||||
$this->di['logger']->setChannel('email')->error('Failed to send email confirmation email', ['exception' => $exc->getMessage()]);
|
$this->di['logger']->withChannel('email')->error('Failed to send email confirmation email', ['exception' => $exc]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1033,7 +1033,7 @@ class Service implements InjectionAwareInterface
|
|||||||
->execute();
|
->execute();
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
if (!\FOSSBilling\Environment::isTesting()) {
|
if (!\FOSSBilling\Environment::isTesting()) {
|
||||||
error_log($e->getMessage());
|
$di['logger']->error($e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,13 +64,13 @@ class Service
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if ($this->di['update_finalization']->isRequired()) {
|
if ($this->di['update_finalization']->isRequired()) {
|
||||||
$this->di['logger']->setChannel('cron')->warning('Skipped cron execution because update finalization is pending.');
|
$this->di['logger']->withChannel('cron')->warning('Skipped cron execution because update finalization is pending.');
|
||||||
|
|
||||||
throw new \FOSSBilling\InformationException('Update finalization is pending. Cron jobs are paused until finalization is completed.', [], 503);
|
throw new \FOSSBilling\InformationException('Update finalization is pending. Cron jobs are paused until finalization is completed.', [], 503);
|
||||||
}
|
}
|
||||||
|
|
||||||
$api = $this->di['api_system'];
|
$api = $this->di['api_system'];
|
||||||
$this->di['logger']->setChannel('cron')->info('Started executing cron jobs.');
|
$this->di['logger']->withChannel('cron')->info('Started executing cron jobs.');
|
||||||
|
|
||||||
// @core tasks
|
// @core tasks
|
||||||
$this->_exec($api, 'hook_batch_connect');
|
$this->_exec($api, 'hook_batch_connect');
|
||||||
@@ -96,14 +96,17 @@ class Service
|
|||||||
$this->_exec($api, $method);
|
$this->_exec($api, $method);
|
||||||
} catch (\Throwable $exception) {
|
} catch (\Throwable $exception) {
|
||||||
$failedTasks[] = $method;
|
$failedTasks[] = $method;
|
||||||
$this->di['logger']->setChannel('cron')->error(sprintf(
|
$this->di['logger']->withChannel('cron')->error(
|
||||||
'Failed to run cron task %s: %s: %s in %s:%d',
|
'Failed to run cron task {task}: {exception_class}: {exception_message} in {file}:{line}',
|
||||||
$method,
|
[
|
||||||
$exception::class,
|
'task' => $method,
|
||||||
$exception->getMessage(),
|
'exception_class' => $exception::class,
|
||||||
$exception->getFile(),
|
'exception_message' => $exception->getMessage(),
|
||||||
$exception->getLine()
|
'file' => $exception->getFile(),
|
||||||
));
|
'line' => $exception->getLine(),
|
||||||
|
'exception' => $exception,
|
||||||
|
]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,17 +115,17 @@ class Service
|
|||||||
|
|
||||||
// Purge old sessions from the DB
|
// Purge old sessions from the DB
|
||||||
$count = $this->clearOldSessions() ?? 0;
|
$count = $this->clearOldSessions() ?? 0;
|
||||||
$this->di['logger']->setChannel('cron')->info("Cleared {$count} outdated sessions from the database.");
|
$this->di['logger']->withChannel('cron')->info("Cleared {$count} outdated sessions from the database.");
|
||||||
|
|
||||||
$this->di['events_manager']->fire(['event' => 'onAfterAdminCronRun']);
|
$this->di['events_manager']->fire(['event' => 'onAfterAdminCronRun']);
|
||||||
|
|
||||||
if ($failedTasks !== []) {
|
if ($failedTasks !== []) {
|
||||||
$this->di['logger']->setChannel('cron')->warning('Finished executing cron jobs, but the following tasks failed: ' . implode(', ', $failedTasks) . '.');
|
$this->di['logger']->withChannel('cron')->warning('Finished executing cron jobs, but the following tasks failed: ' . implode(', ', $failedTasks) . '.');
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->di['logger']->setChannel('cron')->info('Finished executing cron jobs.');
|
$this->di['logger']->withChannel('cron')->info('Finished executing cron jobs.');
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ class Service implements InjectionAwareInterface
|
|||||||
$em->persist($currency);
|
$em->persist($currency);
|
||||||
$em->flush();
|
$em->flush();
|
||||||
|
|
||||||
$this->di['logger']->info('Set currency %s as default.', $currency->getCode());
|
$this->di['logger']->info('Set currency {currency_code} as default.', ['currency_code' => $currency->getCode()]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -260,11 +260,7 @@ class Service implements InjectionAwareInterface
|
|||||||
$conversionRate = $this->getRate(null, $currencyCode);
|
$conversionRate = $this->getRate(null, $currencyCode);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
// If rate fetch fails, log a warning and use a default rate of 1.0
|
// If rate fetch fails, log a warning and use a default rate of 1.0
|
||||||
$this->di['logger']->warning(
|
$this->di['logger']->warning('Failed to fetch conversion rate for {currency_code}: {exception}. Using default rate of 1.0.', ['currency_code' => $currencyCode, 'exception' => $e]);
|
||||||
'Failed to fetch conversion rate for %s: %s. Using default rate of 1.0.',
|
|
||||||
$currencyCode,
|
|
||||||
$e->getMessage()
|
|
||||||
);
|
|
||||||
$conversionRate = 1.0;
|
$conversionRate = 1.0;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -284,7 +280,7 @@ class Service implements InjectionAwareInterface
|
|||||||
$em->persist($currency);
|
$em->persist($currency);
|
||||||
$em->flush();
|
$em->flush();
|
||||||
|
|
||||||
$this->di['logger']->info('Added new currency %s.', $currency->getCode());
|
$this->di['logger']->info('Added new currency {currency_code}.', ['currency_code' => $currency->getCode()]);
|
||||||
|
|
||||||
return $currency->getCode();
|
return $currency->getCode();
|
||||||
}
|
}
|
||||||
@@ -317,7 +313,7 @@ class Service implements InjectionAwareInterface
|
|||||||
|
|
||||||
$this->di['events_manager']->fire(['event' => 'onAfterAdminDeleteCurrency', 'params' => ['code' => $currencyCode]]);
|
$this->di['events_manager']->fire(['event' => 'onAfterAdminDeleteCurrency', 'params' => ['code' => $currencyCode]]);
|
||||||
|
|
||||||
$this->di['logger']->info('Removed currency %s.', $currency->getCode());
|
$this->di['logger']->info('Removed currency {currency_code}.', ['currency_code' => $currency->getCode()]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -380,7 +376,7 @@ class Service implements InjectionAwareInterface
|
|||||||
$em->flush();
|
$em->flush();
|
||||||
|
|
||||||
unset($this->formattingCache[$currencyCode]);
|
unset($this->formattingCache[$currencyCode]);
|
||||||
$this->di['logger']->info('Updated currency %s.', $model->getCode());
|
$this->di['logger']->info('Updated currency {model_code}.', ['model_code' => $model->getCode()]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -541,7 +537,7 @@ class Service implements InjectionAwareInterface
|
|||||||
|
|
||||||
$em->flush();
|
$em->flush();
|
||||||
|
|
||||||
$this->di['logger']->info('Updated %d currency rates.', $updatedCount);
|
$this->di['logger']->info('Updated {updated_count} currency rates.', ['updated_count' => $updatedCount]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -632,7 +628,7 @@ class Service implements InjectionAwareInterface
|
|||||||
|
|
||||||
if ($array['result'] !== 'success') {
|
if ($array['result'] !== 'success') {
|
||||||
$item->expiresAfter(15 * 60 * 60); // Try again in 15 min
|
$item->expiresAfter(15 * 60 * 60); // Try again in 15 min
|
||||||
error_log('ExchangeRate-API Gave an error: ' . $array['error-type']);
|
$this->di['logger']->error('ExchangeRate-API Gave an error: ' . $array['error-type']);
|
||||||
|
|
||||||
throw new \FOSSBilling\Exception('There was an error when fetching currency rates from ExchangeRate-API. See the error log for details.');
|
throw new \FOSSBilling\Exception('There was an error when fetching currency rates from ExchangeRate-API. See the error log for details.');
|
||||||
}
|
}
|
||||||
@@ -685,7 +681,7 @@ class Service implements InjectionAwareInterface
|
|||||||
$array = $response->toArray();
|
$array = $response->toArray();
|
||||||
|
|
||||||
if ($array['success'] !== true) {
|
if ($array['success'] !== true) {
|
||||||
error_log($array['error']['info']);
|
$this->di['logger']->error($array['error']['info']);
|
||||||
|
|
||||||
throw new \FOSSBilling\Exception('There was an error when fetching currency rates from Currency Data API. See the error log for details.');
|
throw new \FOSSBilling\Exception('There was an error when fetching currency rates from Currency Data API. See the error log for details.');
|
||||||
}
|
}
|
||||||
@@ -718,7 +714,7 @@ class Service implements InjectionAwareInterface
|
|||||||
$array = $response->toArray();
|
$array = $response->toArray();
|
||||||
|
|
||||||
if ($array['success'] !== true) {
|
if ($array['success'] !== true) {
|
||||||
error_log($array['error']['info']);
|
$this->di['logger']->error($array['error']['info']);
|
||||||
|
|
||||||
throw new \FOSSBilling\Exception('There was an error when fetching currency rates from currencylayer. See the error log for details.');
|
throw new \FOSSBilling\Exception('There was an error when fetching currency rates from currencylayer. See the error log for details.');
|
||||||
}
|
}
|
||||||
@@ -761,7 +757,7 @@ class Service implements InjectionAwareInterface
|
|||||||
$currencyService->updateCurrencyRates();
|
$currencyService->updateCurrencyRates();
|
||||||
}
|
}
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
error_log($e->getMessage());
|
$di['logger']->error($e->getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ class Service
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$id = (int) $connection->lastInsertId();
|
$id = (int) $connection->lastInsertId();
|
||||||
$this->di['logger']->info('Created new custom page #%s', $id);
|
$this->di['logger']->info('Created new custom page #{id}', ['id' => $id]);
|
||||||
|
|
||||||
return $id;
|
return $id;
|
||||||
} catch (UniqueConstraintViolationException) {
|
} catch (UniqueConstraintViolationException) {
|
||||||
@@ -161,7 +161,7 @@ class Service
|
|||||||
// and the flush. Surface it as the same uniqueness error as above.
|
// and the flush. Surface it as the same uniqueness error as above.
|
||||||
throw new \FOSSBilling\Exception('You need to set unique slug.', null, 9999);
|
throw new \FOSSBilling\Exception('You need to set unique slug.', null, 9999);
|
||||||
}
|
}
|
||||||
$this->di['logger']->info('Updated custom page #%s', $id);
|
$this->di['logger']->info('Updated custom page #{id}', ['id' => $id]);
|
||||||
|
|
||||||
return (int) $id;
|
return (int) $id;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ function buildCustompagesService(object $repo, ?EntityManagerInterface $em = nul
|
|||||||
$em->allows('getRepository')->with(CustomPage::class)->andReturn($repo);
|
$em->allows('getRepository')->with(CustomPage::class)->andReturn($repo);
|
||||||
|
|
||||||
$di['em'] = $em;
|
$di['em'] = $em;
|
||||||
// Logger is a Box_Log in production (untyped __call dispatch); mock loosely.
|
// The production logger follows PSR-3; this test only needs a lightweight stub.
|
||||||
$di['logger'] = Mockery::mock()->shouldIgnoreMissing();
|
$di['logger'] = Mockery::mock()->shouldIgnoreMissing();
|
||||||
$di['tools'] = Mockery::mock(FOSSBilling\Tools::class);
|
$di['tools'] = Mockery::mock(FOSSBilling\Tools::class);
|
||||||
$di['tools']->allows('slug')->andReturnUsing(fn ($s): string => strtolower(preg_replace('/[^a-zA-Z0-9]+/', '-', (string) $s)));
|
$di['tools']->allows('slug')->andReturnUsing(fn ($s): string => strtolower(preg_replace('/[^a-zA-Z0-9]+/', '-', (string) $s)));
|
||||||
@@ -129,7 +129,7 @@ test('create page generates unique slug and inserts via dbal', function (): void
|
|||||||
$connection->expects('lastInsertId')->once()->andReturn(42);
|
$connection->expects('lastInsertId')->once()->andReturn(42);
|
||||||
|
|
||||||
$logger = Mockery::mock();
|
$logger = Mockery::mock();
|
||||||
$logger->expects('info')->with('Created new custom page #%s', 42)->once();
|
$logger->expects('info')->with('Created new custom page #{id}', ['id' => 42])->once();
|
||||||
|
|
||||||
$em = Mockery::mock(EntityManagerInterface::class);
|
$em = Mockery::mock(EntityManagerInterface::class);
|
||||||
$em->allows('getRepository')->with(CustomPage::class)->andReturn($repo);
|
$em->allows('getRepository')->with(CustomPage::class)->andReturn($repo);
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ class Admin extends \FOSSBilling\Api\AbstractApi
|
|||||||
$em->remove($model);
|
$em->remove($model);
|
||||||
$em->flush();
|
$em->flush();
|
||||||
|
|
||||||
$this->getDi()['logger']->info('Deleted email #%s', $id);
|
$this->getDi()['logger']->info('Deleted email #{id}', ['id' => $id]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -191,7 +191,7 @@ class Admin extends \FOSSBilling\Api\AbstractApi
|
|||||||
$this->getDi()['em']->remove($template);
|
$this->getDi()['em']->remove($template);
|
||||||
$this->getDi()['em']->flush();
|
$this->getDi()['em']->flush();
|
||||||
|
|
||||||
$this->getDi()['logger']->info('Deleted email template #%s', $id);
|
$this->getDi()['logger']->info('Deleted email template #{id}', ['id' => $id]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -699,7 +699,7 @@ class Service implements \FOSSBilling\InjectionAwareInterface
|
|||||||
if (Environment::isTesting()) {
|
if (Environment::isTesting()) {
|
||||||
// @phpstan-ignore if.alwaysFalse
|
// @phpstan-ignore if.alwaysFalse
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
$this->di['logger']->setChannel('email')->info('Skipping email sending in test environment');
|
$this->di['logger']->withChannel('email')->info('Skipping email sending in test environment');
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -714,7 +714,7 @@ class Service implements \FOSSBilling\InjectionAwareInterface
|
|||||||
|
|
||||||
$this->sendMail($email->getRecipients(), $email->getSender(), $email->getSubject(), $email->getContentHtml(), $customer['first_name'] . ' ' . $customer['last_name'], $from_name, $email->getClientId(), null, false, false, $this->loggedAttachmentToArray($email));
|
$this->sendMail($email->getRecipients(), $email->getSender(), $email->getSubject(), $email->getContentHtml(), $customer['first_name'] . ' ' . $customer['last_name'], $from_name, $email->getClientId(), null, false, false, $this->loggedAttachmentToArray($email));
|
||||||
|
|
||||||
$this->di['logger']->info('Resent email #%s', $email->getId());
|
$this->di['logger']->info('Resent email #{email_id}', ['email_id' => $email->getId()]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -841,7 +841,7 @@ class Service implements \FOSSBilling\InjectionAwareInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
$this->di['em']->flush();
|
$this->di['em']->flush();
|
||||||
$this->di['logger']->info('Updated email template #%s', $template->getId());
|
$this->di['logger']->info('Updated email template #{template_id}', ['template_id' => $template->getId()]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -868,7 +868,7 @@ class Service implements \FOSSBilling\InjectionAwareInterface
|
|||||||
$this->di['em']->persist(new EmailTemplateGroup($template, $groupId));
|
$this->di['em']->persist(new EmailTemplateGroup($template, $groupId));
|
||||||
$this->di['em']->flush();
|
$this->di['em']->flush();
|
||||||
|
|
||||||
$this->di['logger']->info('Assigned email template #%s to staff group #%s', $template->getId(), $groupId);
|
$this->di['logger']->info('Assigned email template #{template_id} to staff group #{group_id}', ['template_id' => $template->getId(), 'group_id' => $groupId]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -883,7 +883,7 @@ class Service implements \FOSSBilling\InjectionAwareInterface
|
|||||||
$this->di['em']->remove($association);
|
$this->di['em']->remove($association);
|
||||||
$this->di['em']->flush();
|
$this->di['em']->flush();
|
||||||
|
|
||||||
$this->di['logger']->info('Removed email template #%s from staff group #%s', $template->getId(), $groupId);
|
$this->di['logger']->info('Removed email template #{template_id} from staff group #{group_id}', ['template_id' => $template->getId(), 'group_id' => $groupId]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -902,7 +902,7 @@ class Service implements \FOSSBilling\InjectionAwareInterface
|
|||||||
|
|
||||||
$this->resetBuiltinTemplate($template, $default);
|
$this->resetBuiltinTemplate($template, $default);
|
||||||
$this->di['em']->flush();
|
$this->di['em']->flush();
|
||||||
$this->di['logger']->info('Reset email template: %s', $template->getActionCode());
|
$this->di['logger']->info('Reset email template: {action_code}', ['action_code' => $template->getActionCode()]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -930,7 +930,7 @@ class Service implements \FOSSBilling\InjectionAwareInterface
|
|||||||
$em->persist($template);
|
$em->persist($template);
|
||||||
$em->flush();
|
$em->flush();
|
||||||
|
|
||||||
$this->di['logger']->info('Added new email template #%s', $template->getId());
|
$this->di['logger']->info('Added new email template #{template_id}', ['template_id' => $template->getId()]);
|
||||||
|
|
||||||
return $template;
|
return $template;
|
||||||
}
|
}
|
||||||
@@ -959,7 +959,7 @@ class Service implements \FOSSBilling\InjectionAwareInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
$this->di['em']->flush();
|
$this->di['em']->flush();
|
||||||
$this->di['logger']->info(sprintf('Regenerated %d existing file-backed email templates.', $regenerated));
|
$this->di['logger']->info('Regenerated {count} existing file-backed email templates.', ['count' => $regenerated]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1090,7 +1090,10 @@ class Service implements \FOSSBilling\InjectionAwareInterface
|
|||||||
if ($error !== null) {
|
if ($error !== null) {
|
||||||
$template->setLastError($error);
|
$template->setLastError($error);
|
||||||
$template->setErrorCheckedAt(new \DateTimeImmutable());
|
$template->setErrorCheckedAt(new \DateTimeImmutable());
|
||||||
$this->di['logger']->warning(sprintf('Email template validation failed for "%s": %s', $template->getActionCode(), $error));
|
$this->di['logger']->warning(
|
||||||
|
'Email template validation failed for "{action_code}": {error}',
|
||||||
|
['action_code' => $template->getActionCode(), 'error' => $error]
|
||||||
|
);
|
||||||
++$results['invalid'];
|
++$results['invalid'];
|
||||||
$results['errors'][] = [
|
$results['errors'][] = [
|
||||||
'id' => $template->getId(),
|
'id' => $template->getId(),
|
||||||
@@ -1166,7 +1169,7 @@ class Service implements \FOSSBilling\InjectionAwareInterface
|
|||||||
if (filter_var($address, FILTER_VALIDATE_EMAIL)) {
|
if (filter_var($address, FILTER_VALIDATE_EMAIL)) {
|
||||||
$addresses[] = $address;
|
$addresses[] = $address;
|
||||||
} else {
|
} else {
|
||||||
$this->di['logger']->setChannel('email')->warning('Skipping invalid Bcc address: ' . $address);
|
$this->di['logger']->withChannel('email')->warning('Skipping invalid Bcc address: ' . $address);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1211,7 +1214,7 @@ class Service implements \FOSSBilling\InjectionAwareInterface
|
|||||||
if (filter_var($settings['reply_to'], FILTER_VALIDATE_EMAIL)) {
|
if (filter_var($settings['reply_to'], FILTER_VALIDATE_EMAIL)) {
|
||||||
$mail->addReplyTo($settings['reply_to']);
|
$mail->addReplyTo($settings['reply_to']);
|
||||||
} else {
|
} else {
|
||||||
$this->di['logger']->setChannel('email')->warning('Skipping invalid Reply-To address: ' . $settings['reply_to']);
|
$this->di['logger']->withChannel('email')->warning('Skipping invalid Reply-To address: ' . $settings['reply_to']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1224,7 +1227,7 @@ class Service implements \FOSSBilling\InjectionAwareInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!Environment::isProduction()) {
|
if (!Environment::isProduction()) {
|
||||||
$this->di['logger']->setChannel('email')->info('Skip email sending. Application ENV: ' . Environment::getCurrentEnvironment());
|
$this->di['logger']->withChannel('email')->info('Skip email sending. Application ENV: ' . Environment::getCurrentEnvironment());
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1241,11 +1244,11 @@ class Service implements \FOSSBilling\InjectionAwareInterface
|
|||||||
$this->di['em']->remove($queue);
|
$this->di['em']->remove($queue);
|
||||||
$this->di['em']->flush();
|
$this->di['em']->flush();
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$this->di['logger']->setChannel('email')->error($e->getMessage());
|
$this->di['logger']->withChannel('email')->error($e->getMessage());
|
||||||
}
|
}
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$message = $e->getMessage();
|
$message = $e->getMessage();
|
||||||
$this->di['logger']->setChannel('email')->error($e->getMessage());
|
$this->di['logger']->withChannel('email')->error($e->getMessage());
|
||||||
|
|
||||||
if ($queue->getPriority()) {
|
if ($queue->getPriority()) {
|
||||||
$queue->setPriority($queue->getPriority() - 1);
|
$queue->setPriority($queue->getPriority() - 1);
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ test('email delete', function (): void {
|
|||||||
$em->shouldReceive('remove')->atLeast()->once();
|
$em->shouldReceive('remove')->atLeast()->once();
|
||||||
$em->shouldReceive('flush')->atLeast()->once();
|
$em->shouldReceive('flush')->atLeast()->once();
|
||||||
|
|
||||||
$loggerStub = $this->createStub('\Box_Log');
|
$loggerStub = $this->createStub('\FOSSBilling\Logger');
|
||||||
|
|
||||||
$di = container();
|
$di = container();
|
||||||
$di['em'] = $em;
|
$di['em'] = $em;
|
||||||
@@ -309,7 +309,7 @@ test('template delete', function (): void {
|
|||||||
$templateGroupRepo->shouldReceive('deleteAssociationsForTemplate')->atLeast()->once()->with(1);
|
$templateGroupRepo->shouldReceive('deleteAssociationsForTemplate')->atLeast()->once()->with(1);
|
||||||
$emailService->shouldReceive('getTemplateGroupRepository')->andReturn($templateGroupRepo);
|
$emailService->shouldReceive('getTemplateGroupRepository')->andReturn($templateGroupRepo);
|
||||||
|
|
||||||
$loggerStub = $this->createStub('\Box_Log');
|
$loggerStub = $this->createStub('\FOSSBilling\Logger');
|
||||||
$em = Mockery::mock(Doctrine\ORM\EntityManagerInterface::class);
|
$em = Mockery::mock(Doctrine\ORM\EntityManagerInterface::class);
|
||||||
$em->shouldReceive('remove')->atLeast()->once()->with($model);
|
$em->shouldReceive('remove')->atLeast()->once()->with($model);
|
||||||
$em->shouldReceive('flush')->atLeast()->once();
|
$em->shouldReceive('flush')->atLeast()->once();
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ test('setVars encrypts and sets variables', function (): void {
|
|||||||
$service = new Box\Mod\Email\Service();
|
$service = new Box\Mod\Email\Service();
|
||||||
|
|
||||||
$di = container();
|
$di = container();
|
||||||
$cryptMock = Mockery::mock('\Box_Crypt');
|
$cryptMock = Mockery::mock(FOSSBilling\Crypt::class);
|
||||||
$cryptMock->shouldReceive('encrypt')
|
$cryptMock->shouldReceive('encrypt')
|
||||||
->atLeast()->once()
|
->atLeast()->once()
|
||||||
->andReturn('encrypted-vars');
|
->andReturn('encrypted-vars');
|
||||||
@@ -200,7 +200,7 @@ test('getVars decrypts and returns variables', function (): void {
|
|||||||
$service = new Box\Mod\Email\Service();
|
$service = new Box\Mod\Email\Service();
|
||||||
|
|
||||||
$di = container();
|
$di = container();
|
||||||
$cryptMock = Mockery::mock('\Box_Crypt');
|
$cryptMock = Mockery::mock(FOSSBilling\Crypt::class);
|
||||||
$cryptMock->shouldReceive('decrypt')
|
$cryptMock->shouldReceive('decrypt')
|
||||||
->atLeast()->once()
|
->atLeast()->once()
|
||||||
->andReturn('{"param1":"value1"}');
|
->andReturn('{"param1":"value1"}');
|
||||||
@@ -265,7 +265,7 @@ test('getVars supplies preview variables for support and staff templates before
|
|||||||
test('getVars merges stored examples over preview defaults', function (): void {
|
test('getVars merges stored examples over preview defaults', function (): void {
|
||||||
$service = new Box\Mod\Email\Service();
|
$service = new Box\Mod\Email\Service();
|
||||||
|
|
||||||
$cryptMock = Mockery::mock('\Box_Crypt');
|
$cryptMock = Mockery::mock(FOSSBilling\Crypt::class);
|
||||||
$cryptMock->shouldReceive('decrypt')->once()->andReturn('{"ticket":{"subject":"Stored subject"}}');
|
$cryptMock->shouldReceive('decrypt')->once()->andReturn('{"ticket":{"subject":"Stored subject"}}');
|
||||||
|
|
||||||
$di = container();
|
$di = container();
|
||||||
@@ -299,7 +299,7 @@ test('sendTemplate returns false when template does not exist', function (): voi
|
|||||||
$em->shouldReceive('persist')->atLeast()->once();
|
$em->shouldReceive('persist')->atLeast()->once();
|
||||||
$em->shouldReceive('flush')->atLeast()->once();
|
$em->shouldReceive('flush')->atLeast()->once();
|
||||||
|
|
||||||
$cryptMock = Mockery::mock('\Box_Crypt');
|
$cryptMock = Mockery::mock(FOSSBilling\Crypt::class);
|
||||||
$cryptMock->shouldReceive('encrypt')
|
$cryptMock->shouldReceive('encrypt')
|
||||||
->atLeast()->once();
|
->atLeast()->once();
|
||||||
|
|
||||||
@@ -363,7 +363,7 @@ test('sendTemplate sends email when template exists', function (): void {
|
|||||||
$validatorMock->shouldReceive('checkRequiredParamsForArray')->byDefault();
|
$validatorMock->shouldReceive('checkRequiredParamsForArray')->byDefault();
|
||||||
$di['validator'] = $validatorMock;
|
$di['validator'] = $validatorMock;
|
||||||
|
|
||||||
$cryptMock = Mockery::mock('\Box_Crypt');
|
$cryptMock = Mockery::mock(FOSSBilling\Crypt::class);
|
||||||
$cryptMock->shouldReceive('encrypt')
|
$cryptMock->shouldReceive('encrypt')
|
||||||
->atLeast()->once();
|
->atLeast()->once();
|
||||||
|
|
||||||
@@ -444,7 +444,7 @@ test('sendTemplate forwards the attachment to the queue and strips it from the s
|
|||||||
$di['validator'] = $validatorMock;
|
$di['validator'] = $validatorMock;
|
||||||
|
|
||||||
$encryptedVars = null;
|
$encryptedVars = null;
|
||||||
$cryptMock = Mockery::mock('\Box_Crypt');
|
$cryptMock = Mockery::mock(FOSSBilling\Crypt::class);
|
||||||
$cryptMock->shouldReceive('encrypt')
|
$cryptMock->shouldReceive('encrypt')
|
||||||
->atLeast()->once()
|
->atLeast()->once()
|
||||||
->with(Mockery::on(function ($json) use (&$encryptedVars): bool {
|
->with(Mockery::on(function ($json) use (&$encryptedVars): bool {
|
||||||
@@ -589,7 +589,7 @@ test('sendTemplate handles to_staff and to_client options', function (array $dat
|
|||||||
|
|
||||||
$twigStub = Mockery::mock(Twig\Environment::class);
|
$twigStub = Mockery::mock(Twig\Environment::class);
|
||||||
|
|
||||||
$cryptMock = Mockery::mock('\Box_Crypt');
|
$cryptMock = Mockery::mock(FOSSBilling\Crypt::class);
|
||||||
$cryptMock->shouldReceive('encrypt')
|
$cryptMock->shouldReceive('encrypt')
|
||||||
->atLeast()->once();
|
->atLeast()->once();
|
||||||
|
|
||||||
@@ -683,7 +683,7 @@ test('sendTemplate sends to a specific admin via to_admin using the Admin entity
|
|||||||
$clientServiceMock->shouldReceive('get')->never();
|
$clientServiceMock->shouldReceive('get')->never();
|
||||||
$clientServiceMock->shouldReceive('toApiArray')->never();
|
$clientServiceMock->shouldReceive('toApiArray')->never();
|
||||||
|
|
||||||
$cryptMock = Mockery::mock('\Box_Crypt');
|
$cryptMock = Mockery::mock(FOSSBilling\Crypt::class);
|
||||||
$cryptMock->shouldReceive('encrypt')->atLeast()->once();
|
$cryptMock->shouldReceive('encrypt')->atLeast()->once();
|
||||||
|
|
||||||
$validatorMock = Mockery::mock(FOSSBilling\Validate::class);
|
$validatorMock = Mockery::mock(FOSSBilling\Validate::class);
|
||||||
@@ -782,7 +782,7 @@ test('sendTemplate does not send to staff when template has no assigned groups',
|
|||||||
'from_email' => 'test@test.com',
|
'from_email' => 'test@test.com',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$cryptMock = Mockery::mock('\Box_Crypt');
|
$cryptMock = Mockery::mock(FOSSBilling\Crypt::class);
|
||||||
$cryptMock->shouldReceive('encrypt')->atLeast()->once();
|
$cryptMock->shouldReceive('encrypt')->atLeast()->once();
|
||||||
|
|
||||||
$validatorMock = Mockery::mock(FOSSBilling\Validate::class);
|
$validatorMock = Mockery::mock(FOSSBilling\Validate::class);
|
||||||
@@ -1136,7 +1136,7 @@ test('updateTemplate updates template', function (array $data, string $templateR
|
|||||||
|
|
||||||
$loggerStub = new Tests\Helpers\TestLogger();
|
$loggerStub = new Tests\Helpers\TestLogger();
|
||||||
|
|
||||||
$cryptMock = Mockery::mock('\Box_Crypt');
|
$cryptMock = Mockery::mock(FOSSBilling\Crypt::class);
|
||||||
$cryptMock->shouldReceive('decrypt')
|
$cryptMock->shouldReceive('decrypt')
|
||||||
->never();
|
->never();
|
||||||
$configMock = ['salt' => md5(random_bytes(13))];
|
$configMock = ['salt' => md5(random_bytes(13))];
|
||||||
@@ -1225,10 +1225,14 @@ test('templateBatchRegenerate resets existing built-in templates without module
|
|||||||
->and($template->hasError())->toBeFalse()
|
->and($template->hasError())->toBeFalse()
|
||||||
->and($customTemplate->getContent())->toBe('Custom template content');
|
->and($customTemplate->getContent())->toBe('Custom template content');
|
||||||
|
|
||||||
$logMessages = array_map(static fn (array $call): string => $call['params'][0], $di['logger']->calls);
|
expect($di['logger']->calls)->not->toContainEqual([
|
||||||
expect($logMessages)
|
'method' => 'info',
|
||||||
->not->toContain('Synced file-backed email templates for installed modules.')
|
'params' => ['Synced file-backed email templates for installed modules.', []],
|
||||||
->toContain('Regenerated 1 existing file-backed email templates.');
|
]);
|
||||||
|
expect($di['logger']->calls)->toContainEqual([
|
||||||
|
'method' => 'info',
|
||||||
|
'params' => ['Regenerated {count} existing file-backed email templates.', ['count' => 1]],
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('templateBatchDisable disables all templates', function (): void {
|
test('templateBatchDisable disables all templates', function (): void {
|
||||||
@@ -1452,7 +1456,13 @@ test('validateAllTemplates reports invalid templates', function (): void {
|
|||||||
expect($result['errors'][0]['action_code'])->toBe('mod_email_broken');
|
expect($result['errors'][0]['action_code'])->toBe('mod_email_broken');
|
||||||
expect($di['logger']->calls)->toContainEqual([
|
expect($di['logger']->calls)->toContainEqual([
|
||||||
'method' => 'warning',
|
'method' => 'warning',
|
||||||
'params' => ['Email template validation failed for "mod_email_broken": Email template syntax error: Unknown "filter" filter', []],
|
'params' => [
|
||||||
|
'Email template validation failed for "{action_code}": {error}',
|
||||||
|
[
|
||||||
|
'action_code' => 'mod_email_broken',
|
||||||
|
'error' => 'Email template syntax error: Unknown "filter" filter',
|
||||||
|
],
|
||||||
|
],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1551,7 +1561,7 @@ test('validateAllTemplates renders templates with stored vars to enforce sandbox
|
|||||||
->once()
|
->once()
|
||||||
->andReturn([$template]);
|
->andReturn([$template]);
|
||||||
|
|
||||||
$cryptMock = Mockery::mock('\Box_Crypt');
|
$cryptMock = Mockery::mock(FOSSBilling\Crypt::class);
|
||||||
$cryptMock->shouldReceive('decrypt')
|
$cryptMock->shouldReceive('decrypt')
|
||||||
->once()
|
->once()
|
||||||
->with('encrypted-vars', Mockery::type('string'))
|
->with('encrypted-vars', Mockery::type('string'))
|
||||||
|
|||||||
@@ -52,7 +52,10 @@ class Admin extends \FOSSBilling\Api\AbstractApi
|
|||||||
try {
|
try {
|
||||||
$list = $this->getDi()['extension_manager']->getExtensionList($type);
|
$list = $this->getDi()['extension_manager']->getExtensionList($type);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$this->getDi()['logger']->warning(sprintf('Failed to fetch extension list for type "%s": %s', $type ?? 'all', $e->getMessage()));
|
$this->getDi()['logger']->warning(
|
||||||
|
'Failed to fetch extension list for type "{type}": {exception_message}',
|
||||||
|
['type' => $type ?? 'all', 'exception_message' => $e->getMessage(), 'exception' => $e]
|
||||||
|
);
|
||||||
|
|
||||||
$list = [];
|
$list = [];
|
||||||
}
|
}
|
||||||
@@ -189,7 +192,7 @@ class Admin extends \FOSSBilling\Api\AbstractApi
|
|||||||
|
|
||||||
$this->getDi()['events_manager']->fire(['event' => 'onAfterAdminDeactivateExtension', 'params' => ['id' => $data['id'], 'type' => $data['type']]]);
|
$this->getDi()['events_manager']->fire(['event' => 'onAfterAdminDeactivateExtension', 'params' => ['id' => $data['id'], 'type' => $data['type']]]);
|
||||||
|
|
||||||
$this->getDi()['logger']->info('Deactivated extension "%s"', $data['type'] . ' ' . $data['id']);
|
$this->getDi()['logger']->info('Deactivated extension "{extension}"', ['extension' => $data['type'] . ' ' . $data['id']]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ class Service implements InjectionAwareInterface
|
|||||||
try {
|
try {
|
||||||
$extensionService->getExtensionsList([]);
|
$extensionService->getExtensionsList([]);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
error_log($e->getMessage());
|
$di['logger']->error($e->getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -165,7 +165,7 @@ class Service implements InjectionAwareInterface
|
|||||||
try {
|
try {
|
||||||
$manifest = $m->getManifest();
|
$manifest = $m->getManifest();
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
error_log("Error while decoding the manifest file for {$ext->getName()} : {$e->getMessage()}.");
|
$this->di['logger']->error("Error while decoding the manifest file for {$ext->getName()} : {$e->getMessage()}.");
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -261,7 +261,7 @@ class Service implements InjectionAwareInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!$mod->hasManifest()) {
|
if (!$mod->hasManifest()) {
|
||||||
error_log("Module {$m} manifest file is missing or is not readable.");
|
$this->di['logger']->error("Module {$m} manifest file is missing or is not readable.");
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -313,13 +313,13 @@ class Service implements InjectionAwareInterface
|
|||||||
$nav = $this->di['tools']->sortByOneKey($nav, 'index');
|
$nav = $this->di['tools']->sortByOneKey($nav, 'index');
|
||||||
foreach ($subpages as $page) {
|
foreach ($subpages as $page) {
|
||||||
if (!isset($page['location'])) {
|
if (!isset($page['location'])) {
|
||||||
error_log('Invalid module menu item: ' . print_r($page, true));
|
$this->di['logger']->error('Invalid module menu item: ' . print_r($page, true));
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isset($nav[$page['location']])) {
|
if (!isset($nav[$page['location']])) {
|
||||||
error_log("Submenu item belongs to not existing location: {$page['location']}.");
|
$this->di['logger']->error("Submenu item belongs to not existing location: {$page['location']}.");
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -493,9 +493,9 @@ class Service implements InjectionAwareInterface
|
|||||||
if ($this->filesystem->exists($path)) {
|
if ($this->filesystem->exists($path)) {
|
||||||
try {
|
try {
|
||||||
$this->filesystem->remove($path);
|
$this->filesystem->remove($path);
|
||||||
$this->di['logger']->info('Removed extension files for "%s" from %s', $id, $path);
|
$this->di['logger']->info('Removed extension files for "{id}" from {path}', ['id' => $id, 'path' => $path]);
|
||||||
} catch (IOException $e) {
|
} catch (IOException $e) {
|
||||||
$this->di['logger']->warning('Failed to remove extension files for "%s": %s', $id, $e->getMessage());
|
$this->di['logger']->warning('Failed to remove extension files for "{id}": {exception}', ['id' => $id, 'exception' => $e]);
|
||||||
|
|
||||||
throw new \FOSSBilling\Exception('Failed to remove extension files. Please check file permissions and try again or manually remove the files from :path', [':path' => $path]);
|
throw new \FOSSBilling\Exception('Failed to remove extension files. Please check file permissions and try again or manually remove the files from :path', [':path' => $path]);
|
||||||
}
|
}
|
||||||
@@ -549,7 +549,7 @@ class Service implements InjectionAwareInterface
|
|||||||
$zip->extractTo($extractedPath);
|
$zip->extractTo($extractedPath);
|
||||||
$zip->close();
|
$zip->close();
|
||||||
} catch (\PhpZip\Exception\ZipException $e) {
|
} catch (\PhpZip\Exception\ZipException $e) {
|
||||||
error_log($e->getMessage());
|
$this->di['logger']->error($e->getMessage());
|
||||||
|
|
||||||
throw new \FOSSBilling\Exception('Failed to extract file, please check file and folder permissions. Further details are available in the error log.');
|
throw new \FOSSBilling\Exception('Failed to extract file, please check file and folder permissions. Further details are available in the error log.');
|
||||||
}
|
}
|
||||||
@@ -641,7 +641,7 @@ class Service implements InjectionAwareInterface
|
|||||||
throw $e;
|
throw $e;
|
||||||
}
|
}
|
||||||
$this->di['events_manager']->fire(['event' => 'onAfterAdminActivateExtension', 'params' => ['id' => $ext_id]]);
|
$this->di['events_manager']->fire(['event' => 'onAfterAdminActivateExtension', 'params' => ['id' => $ext_id]]);
|
||||||
$this->di['logger']->info('Activated extension "%s"', $data['id']);
|
$this->di['logger']->info('Activated extension "{data_id}"', ['data_id' => $data['id']]);
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -702,7 +702,7 @@ test('getConfig returns extension config', function (): void {
|
|||||||
->once()
|
->once()
|
||||||
->andReturn($meta);
|
->andReturn($meta);
|
||||||
|
|
||||||
$cryptMock = Mockery::mock(Box_Crypt::class);
|
$cryptMock = Mockery::mock(FOSSBilling\Crypt::class);
|
||||||
$cryptMock->shouldReceive('decrypt')->atLeast()->once();
|
$cryptMock->shouldReceive('decrypt')->atLeast()->once();
|
||||||
|
|
||||||
$em = extensionBuildEm(null, $metaRepo);
|
$em = extensionBuildEm(null, $metaRepo);
|
||||||
@@ -764,7 +764,7 @@ test('setConfig sets extension config', function (): void {
|
|||||||
|
|
||||||
$toolsMock = Mockery::mock(FOSSBilling\Tools::class);
|
$toolsMock = Mockery::mock(FOSSBilling\Tools::class);
|
||||||
|
|
||||||
$cryptMock = Mockery::mock(Box_Crypt::class);
|
$cryptMock = Mockery::mock(FOSSBilling\Crypt::class);
|
||||||
$cryptMock->shouldReceive('encrypt')
|
$cryptMock->shouldReceive('encrypt')
|
||||||
->atLeast()
|
->atLeast()
|
||||||
->once()
|
->once()
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ class Service implements InjectionAwareInterface
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$formId = (int) $this->getDbal()->lastInsertId();
|
$formId = (int) $this->getDbal()->lastInsertId();
|
||||||
$this->di['logger']->info('Created new form %s with id %s', $data['name'], $formId);
|
$this->di['logger']->info('Created new form {form_name} with id {form_id}', ['form_name' => $data['name'], 'form_id' => $formId]);
|
||||||
|
|
||||||
return $formId;
|
return $formId;
|
||||||
}
|
}
|
||||||
@@ -161,7 +161,7 @@ class Service implements InjectionAwareInterface
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$fieldId = (int) $this->getDbal()->lastInsertId();
|
$fieldId = (int) $this->getDbal()->lastInsertId();
|
||||||
$this->di['logger']->info('Added new field %s to form %s', $fieldId, $field['form_id']);
|
$this->di['logger']->info('Added new field {field_id} to form {form_id}', ['field_id' => $fieldId, 'form_id' => $field['form_id']]);
|
||||||
|
|
||||||
return $fieldId;
|
return $fieldId;
|
||||||
}
|
}
|
||||||
@@ -252,7 +252,7 @@ class Service implements InjectionAwareInterface
|
|||||||
'updated_at' => date('Y-m-d H:i:s'),
|
'updated_at' => date('Y-m-d H:i:s'),
|
||||||
], ['id' => $fieldId]);
|
], ['id' => $fieldId]);
|
||||||
|
|
||||||
$this->di['logger']->info('Updated custom form %s', $fieldId);
|
$this->di['logger']->info('Updated custom form {field_id}', ['field_id' => $fieldId]);
|
||||||
|
|
||||||
return $fieldId;
|
return $fieldId;
|
||||||
}
|
}
|
||||||
@@ -349,7 +349,7 @@ class Service implements InjectionAwareInterface
|
|||||||
$this->getDbal()->executeStatement('UPDATE product SET form_id = NULL WHERE form_id = ?', [$form_id]);
|
$this->getDbal()->executeStatement('UPDATE product SET form_id = NULL WHERE form_id = ?', [$form_id]);
|
||||||
$this->getDbal()->executeStatement('UPDATE client_order SET form_id = NULL WHERE form_id = ?', [$form_id]);
|
$this->getDbal()->executeStatement('UPDATE client_order SET form_id = NULL WHERE form_id = ?', [$form_id]);
|
||||||
|
|
||||||
$this->di['logger']->info('Deleted custom form %s', $form_id);
|
$this->di['logger']->info('Deleted custom form {form_id}', ['form_id' => $form_id]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -361,7 +361,7 @@ class Service implements InjectionAwareInterface
|
|||||||
throw new \FOSSBilling\InformationException('Field was not found');
|
throw new \FOSSBilling\InformationException('Field was not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->di['logger']->info('Deleted custom field %s', $data['id']);
|
$this->di['logger']->info('Deleted custom field {data_id}', ['data_id' => $data['id']]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -397,7 +397,7 @@ class Service implements InjectionAwareInterface
|
|||||||
$this->addNewField($fieldData);
|
$this->addNewField($fieldData);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->di['logger']->info('Copied form with id %s to new form %s with id %s', $data['form_id'], $data['name'], $newFormId);
|
$this->di['logger']->info('Copied form with id {form_id} to new form {form_name} with id {new_form_id}', ['form_id' => $data['form_id'], 'form_name' => $data['name'], 'new_form_id' => $newFormId]);
|
||||||
|
|
||||||
return $newFormId;
|
return $newFormId;
|
||||||
}
|
}
|
||||||
@@ -415,8 +415,8 @@ class Service implements InjectionAwareInterface
|
|||||||
'updated_at' => date('Y-m-d H:i:s'),
|
'updated_at' => date('Y-m-d H:i:s'),
|
||||||
], ['id' => $data['form_id']]);
|
], ['id' => $data['form_id']]);
|
||||||
|
|
||||||
$this->di['logger']->info('Updated form %s name to %s', $data['form_id'], $data['form_name']);
|
$this->di['logger']->info('Updated form {form_id} name to {form_name}', ['form_id' => $data['form_id'], 'form_name' => $data['form_name']]);
|
||||||
$this->di['logger']->info('Updated form %s type to %s', $data['form_id'], $data['type']);
|
$this->di['logger']->info('Updated form {form_id} type to {form_type}', ['form_id' => $data['form_id'], 'form_type' => $data['type']]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,6 +91,8 @@ test('throws exception when adding field with missing type', function (): void {
|
|||||||
$api = apiEndpoint(new Admin());
|
$api = apiEndpoint(new Admin());
|
||||||
$data = [];
|
$data = [];
|
||||||
|
|
||||||
|
$api->setService(Mockery::mock(Service::class));
|
||||||
|
|
||||||
expect(fn () => $api->add_field($data))
|
expect(fn () => $api->add_field($data))
|
||||||
->toThrow(FOSSBilling\Exception::class, 'Form field type is invalid');
|
->toThrow(FOSSBilling\Exception::class, 'Form field type is invalid');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -57,8 +57,10 @@ class Admin extends \FOSSBilling\Api\AbstractApi
|
|||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
try {
|
try {
|
||||||
$this->getDi()['logger']->info($event . ': ' . var_export($params, true));
|
$this->getDi()['logger']->info($event . ': ' . var_export($params, true));
|
||||||
} catch (\Exception $e) {
|
} catch (\Throwable $e) {
|
||||||
error_log($e->getMessage());
|
// The application logger failed; retain a PHP fallback rather than
|
||||||
|
// masking hook dispatch with a logging failure.
|
||||||
|
error_log('[Hook] logger failure: ' . $e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ class Service implements InjectionAwareInterface
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
error_log($e->getMessage());
|
$this->di['logger']->error($e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ class Client extends \FOSSBilling\Api\AbstractApi
|
|||||||
$service = $this->getService();
|
$service = $this->getService();
|
||||||
$invoice = $service->generateForOrder($model);
|
$invoice = $service->generateForOrder($model);
|
||||||
$service->approveInvoice($invoice, ['id' => $invoice->getId(), 'use_credits' => true]);
|
$service->approveInvoice($invoice, ['id' => $invoice->getId(), 'use_credits' => true]);
|
||||||
$this->getDi()['logger']->info('Generated new renewal invoice #%s', $invoice->getId());
|
$this->getDi()['logger']->info('Generated new renewal invoice #{invoice_id}', ['invoice_id' => $invoice->getId()]);
|
||||||
|
|
||||||
return $invoice->getHash();
|
return $invoice->getHash();
|
||||||
}
|
}
|
||||||
@@ -100,7 +100,7 @@ class Client extends \FOSSBilling\Api\AbstractApi
|
|||||||
$service = $this->getService();
|
$service = $this->getService();
|
||||||
$invoice = $service->generateFundsInvoice($this->getIdentity(), $data['amount']);
|
$invoice = $service->generateFundsInvoice($this->getIdentity(), $data['amount']);
|
||||||
$service->approveInvoice($invoice, ['id' => $invoice->getId()]);
|
$service->approveInvoice($invoice, ['id' => $invoice->getId()]);
|
||||||
$this->getDi()['logger']->info('Generated add funds invoice #%s', $invoice->getId());
|
$this->getDi()['logger']->info('Generated add funds invoice #{invoice_id}', ['invoice_id' => $invoice->getId()]);
|
||||||
|
|
||||||
return $invoice->getHash();
|
return $invoice->getHash();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -414,13 +414,6 @@ class Service implements InjectionAwareInterface
|
|||||||
|
|
||||||
if (!empty($orderIds)) {
|
if (!empty($orderIds)) {
|
||||||
// Batch load orders
|
// Batch load orders
|
||||||
$orderIdPlaceholders = [];
|
|
||||||
$orderIdParams = [];
|
|
||||||
foreach ($orderIds as $idx => $id) {
|
|
||||||
$placeholder = ':order_id_' . $idx;
|
|
||||||
$orderIdPlaceholders[] = $placeholder;
|
|
||||||
$orderIdParams['order_id_' . $idx] = $id;
|
|
||||||
}
|
|
||||||
$orders = $this->di['em']->getRepository(Order::class)->findBy(['id' => $orderIds]);
|
$orders = $this->di['em']->getRepository(Order::class)->findBy(['id' => $orderIds]);
|
||||||
|
|
||||||
// Batch load related products
|
// Batch load related products
|
||||||
@@ -473,7 +466,7 @@ class Service implements InjectionAwareInterface
|
|||||||
$service->sendInvoiceEmail($invoiceModel, $invoice, 'mod_invoice_paid');
|
$service->sendInvoiceEmail($invoiceModel, $invoice, 'mod_invoice_paid');
|
||||||
}
|
}
|
||||||
} catch (\Exception $exc) {
|
} catch (\Exception $exc) {
|
||||||
$di['logger']->setChannel('email')->error('Failed to send email for invoice payment', ['exception' => $exc->getMessage()]);
|
$di['logger']->withChannel('email')->error('Failed to send email for invoice payment', ['exception' => $exc]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -494,7 +487,7 @@ class Service implements InjectionAwareInterface
|
|||||||
$invoice = $service->toApiArray($invoiceModel, true, null, true);
|
$invoice = $service->toApiArray($invoiceModel, true, null, true);
|
||||||
$service->sendInvoiceEmail($invoiceModel, $invoice, 'mod_invoice_created');
|
$service->sendInvoiceEmail($invoiceModel, $invoice, 'mod_invoice_created');
|
||||||
} catch (\Exception $exc) {
|
} catch (\Exception $exc) {
|
||||||
$di['logger']->setChannel('email')->error('Failed to send email for invoice creation', ['exception' => $exc->getMessage()]);
|
$di['logger']->withChannel('email')->error('Failed to send email for invoice creation', ['exception' => $exc]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -524,7 +517,7 @@ class Service implements InjectionAwareInterface
|
|||||||
$service->extendInvoiceHashLifetime($invoiceModel);
|
$service->extendInvoiceHashLifetime($invoiceModel);
|
||||||
}
|
}
|
||||||
} catch (\Exception $exc) {
|
} catch (\Exception $exc) {
|
||||||
$di['logger']->setChannel('email')->error('Failed to send email for invoice approval', ['exception' => $exc->getMessage()]);
|
$di['logger']->withChannel('email')->error('Failed to send email for invoice approval', ['exception' => $exc]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -576,7 +569,7 @@ class Service implements InjectionAwareInterface
|
|||||||
// since the recipient is being re-engaged via the same link.
|
// since the recipient is being re-engaged via the same link.
|
||||||
$service->extendInvoiceHashLifetime($invoiceModel);
|
$service->extendInvoiceHashLifetime($invoiceModel);
|
||||||
} catch (\Exception $exc) {
|
} catch (\Exception $exc) {
|
||||||
$di['logger']->setChannel('email')->error('Failed to send invoice reminder email', ['exception' => $exc->getMessage()]);
|
$di['logger']->withChannel('email')->error('Failed to send invoice reminder email', ['exception' => $exc]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -616,7 +609,7 @@ class Service implements InjectionAwareInterface
|
|||||||
// of the reminder being silently lost for the day.
|
// of the reminder being silently lost for the day.
|
||||||
$di['em']->getConnection()->executeStatement('UPDATE invoice SET reminded_at = NULL WHERE id = :id', ['id' => $params['id'] ?? 0]);
|
$di['em']->getConnection()->executeStatement('UPDATE invoice SET reminded_at = NULL WHERE id = :id', ['id' => $params['id'] ?? 0]);
|
||||||
}
|
}
|
||||||
$di['logger']->setChannel('email')->error('Failed to send invoice reminder email', ['id' => $params['id'] ?? null, 'exception' => $exc->getMessage()]);
|
$di['logger']->withChannel('email')->error('Failed to send invoice reminder email', ['id' => $params['id'] ?? null, 'exception' => $exc]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -688,7 +681,7 @@ class Service implements InjectionAwareInterface
|
|||||||
// cron run retries this invoice instead of losing the reminder.
|
// cron run retries this invoice instead of losing the reminder.
|
||||||
$di['em']->getConnection()->executeStatement('UPDATE invoice SET reminded_at = NULL WHERE id = :id', ['id' => $params['id'] ?? 0]);
|
$di['em']->getConnection()->executeStatement('UPDATE invoice SET reminded_at = NULL WHERE id = :id', ['id' => $params['id'] ?? 0]);
|
||||||
}
|
}
|
||||||
$di['logger']->setChannel('email')->error('Failed to send overdue invoice email', ['id' => $params['id'] ?? null, 'exception' => $exc->getMessage()]);
|
$di['logger']->withChannel('email')->error('Failed to send overdue invoice email', ['id' => $params['id'] ?? null, 'exception' => $exc]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -751,13 +744,7 @@ class Service implements InjectionAwareInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($execute) {
|
if ($execute) {
|
||||||
foreach ($invoiceItems as $item) {
|
$this->executeInvoiceItemTasks($invoiceItems, $invoiceItemService);
|
||||||
try {
|
|
||||||
$invoiceItemService->executeTask($item);
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$this->di['logger']->warning($e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->di['logger']->info("Marked invoice {$invoice->getId()} as paid.");
|
$this->di['logger']->info("Marked invoice {$invoice->getId()} as paid.");
|
||||||
@@ -913,7 +900,7 @@ class Service implements InjectionAwareInterface
|
|||||||
$this->di['em']->persist($client);
|
$this->di['em']->persist($client);
|
||||||
$this->di['em']->flush();
|
$this->di['em']->flush();
|
||||||
if (isset($this->di['logger'])) {
|
if (isset($this->di['logger'])) {
|
||||||
$this->di['logger']->info('Client #%s currency was not defined. Set default currency %s.', $client->getId(), $currencyCode);
|
$this->di['logger']->info('Client #{client_id} currency was not defined. Set default currency {currency_code}.', ['client_id' => $client->getId(), 'currency_code' => $currencyCode]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1051,8 +1038,8 @@ class Service implements InjectionAwareInterface
|
|||||||
$overpaymentTolerance = 1.00;
|
$overpaymentTolerance = 1.00;
|
||||||
if ($received > $expected + $overpaymentTolerance) {
|
if ($received > $expected + $overpaymentTolerance) {
|
||||||
$this->di['logger']->warning(
|
$this->di['logger']->warning(
|
||||||
'Payment amount significantly exceeds the expected invoice total. Expected :expected, received :received.',
|
'Payment amount significantly exceeds the expected invoice total. Expected {expected}, received {received}.',
|
||||||
[':expected' => number_format($expected, 2, '.', ''), ':received' => number_format($received, 2, '.', '')]
|
['expected' => number_format($expected, 2, '.', ''), 'received' => number_format($received, 2, '.', '')]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1064,7 +1051,7 @@ class Service implements InjectionAwareInterface
|
|||||||
}
|
}
|
||||||
if ($invoice->getStatus() == Invoice::STATUS_PAID) {
|
if ($invoice->getStatus() == Invoice::STATUS_PAID) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
$this->di['logger']->setChannel('billing')->info("Skipping credit payment for already paid invoice {$invoice->getId()}.");
|
$this->di['logger']->withChannel('billing')->info("Skipping credit payment for already paid invoice {$invoice->getId()}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -1091,7 +1078,7 @@ class Service implements InjectionAwareInterface
|
|||||||
if ($difference < -$epsilon) {
|
if ($difference < -$epsilon) {
|
||||||
// @phpstan-ignore if.alwaysFalse (DEBUG is a runtime constant that may be true during debugging)
|
// @phpstan-ignore if.alwaysFalse (DEBUG is a runtime constant that may be true during debugging)
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
$this->di['logger']->setChannel('billing')->info("Invoice {$invoice->getId()} could not be paid with credits. Money in balance {$balance} Required: {$required}.");
|
$this->di['logger']->withChannel('billing')->info("Invoice {$invoice->getId()} could not be paid with credits. Money in balance {$balance} Required: {$required}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -1099,7 +1086,7 @@ class Service implements InjectionAwareInterface
|
|||||||
|
|
||||||
// @phpstan-ignore if.alwaysFalse
|
// @phpstan-ignore if.alwaysFalse
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
$this->di['logger']->setChannel('billing')->info("Setting invoice {$invoice->getId()} as paid with credits for the amount of {$required}.");
|
$this->di['logger']->withChannel('billing')->info("Setting invoice {$invoice->getId()} as paid with credits for the amount of {$required}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($required > $epsilon) {
|
if ($required > $epsilon) {
|
||||||
@@ -1127,7 +1114,10 @@ class Service implements InjectionAwareInterface
|
|||||||
|
|
||||||
if ($paid) {
|
if ($paid) {
|
||||||
$this->firePaymentReceivedEvent($invoice);
|
$this->firePaymentReceivedEvent($invoice);
|
||||||
$this->executeInvoiceItemTasks($invoice);
|
$this->executeInvoiceItemTasks(
|
||||||
|
$this->getInvoiceItemRepository()->findByInvoiceId((int) $invoice->getId()),
|
||||||
|
$this->di['mod_service']('Invoice', 'InvoiceItem')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $paid;
|
return $paid;
|
||||||
@@ -1139,13 +1129,14 @@ class Service implements InjectionAwareInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The task execution markAsPaid() performs with $execute, for callers that must run it after
|
* Execute invoice-item tasks after the payment transaction has committed.
|
||||||
* their transaction commits.
|
*
|
||||||
|
* @param InvoiceItem[] $invoiceItems
|
||||||
|
* @param ServiceInvoiceItem $invoiceItemService
|
||||||
*/
|
*/
|
||||||
private function executeInvoiceItemTasks(Invoice $invoice): void
|
private function executeInvoiceItemTasks(array $invoiceItems, $invoiceItemService): void
|
||||||
{
|
{
|
||||||
$invoiceItemService = $this->di['mod_service']('Invoice', 'InvoiceItem');
|
foreach ($invoiceItems as $item) {
|
||||||
foreach ($this->getInvoiceItemRepository()->findByInvoiceId((int) $invoice->getId()) as $item) {
|
|
||||||
try {
|
try {
|
||||||
$invoiceItemService->executeTask($item);
|
$invoiceItemService->executeTask($item);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
@@ -1458,7 +1449,7 @@ class Service implements InjectionAwareInterface
|
|||||||
|
|
||||||
$this->di['events_manager']->fire(['event' => 'onAfterAdminInvoiceDelete', 'params' => ['id' => $id]]);
|
$this->di['events_manager']->fire(['event' => 'onAfterAdminInvoiceDelete', 'params' => ['id' => $id]]);
|
||||||
|
|
||||||
$this->di['logger']->info('Removed invoice #%s', $id);
|
$this->di['logger']->info('Removed invoice #{id}', ['id' => $id]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1688,7 +1679,6 @@ class Service implements InjectionAwareInterface
|
|||||||
$ss = $this->di['mod_service']('System');
|
$ss = $this->di['mod_service']('System');
|
||||||
$last_time = $ss->getParamValue($key);
|
$last_time = $ss->getParamValue($key);
|
||||||
if ($once_per_day && $last_time && (time() - strtotime((string) $last_time)) < 86400) {
|
if ($once_per_day && $last_time && (time() - strtotime((string) $last_time)) < 86400) {
|
||||||
// error_log('Already executed today.');
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1880,7 +1870,7 @@ class Service implements InjectionAwareInterface
|
|||||||
$i = clone $invoice;
|
$i = clone $invoice;
|
||||||
$mpi = $this->getPaymentInvoice($i, $subscribe);
|
$mpi = $this->getPaymentInvoice($i, $subscribe);
|
||||||
$r = ($subscribe) ? $adapter->recurrentPayment($mpi) : $adapter->singlePayment($mpi);
|
$r = ($subscribe) ? $adapter->recurrentPayment($mpi) : $adapter->singlePayment($mpi);
|
||||||
$this->di['logger']->info('Went to pay for invoice #%s via %s', $invoice->getId(), $gtw->getGateway());
|
$this->di['logger']->info('Went to pay for invoice #{invoice_id} via {gateway}', ['invoice_id' => $invoice->getId(), 'gateway' => $gtw->getGateway()]);
|
||||||
|
|
||||||
// @bug https://github.com/boxbilling/boxbilling/issues/108
|
// @bug https://github.com/boxbilling/boxbilling/issues/108
|
||||||
if ($adapter->getType() != 'html') {
|
if ($adapter->getType() != 'html') {
|
||||||
@@ -1935,7 +1925,7 @@ class Service implements InjectionAwareInterface
|
|||||||
'mime' => 'application/pdf',
|
'mime' => 'application/pdf',
|
||||||
];
|
];
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$this->di['logger']->setChannel('email')->error('Failed to generate PDF invoice attachment: ' . $e->getMessage());
|
$this->di['logger']->withChannel('email')->error('Failed to generate PDF invoice attachment: ' . $e->getMessage());
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ class ServiceInvoiceItem implements InjectionAwareInterface
|
|||||||
} catch (UniqueConstraintViolationException) {
|
} catch (UniqueConstraintViolationException) {
|
||||||
// Idempotency: the unique constraint on invoice_item_id means a prior
|
// Idempotency: the unique constraint on invoice_item_id means a prior
|
||||||
// attempt already credited this item. Mark it as charged without re-crediting.
|
// attempt already credited this item. Mark it as charged without re-crediting.
|
||||||
$this->di['logger']->setChannel('billing')->info(sprintf('Invoice item #%d was already credited; skipping duplicate credit.', (int) $item->getId()));
|
$this->di['logger']->withChannel('billing')->info('Invoice item #{item_id} was already credited; skipping duplicate credit.', ['item_id' => $item->getId()]);
|
||||||
$this->resetEntityManager();
|
$this->resetEntityManager();
|
||||||
$item = $this->di['em']->find(InvoiceItem::class, $item->getId());
|
$item = $this->di['em']->find(InvoiceItem::class, $item->getId());
|
||||||
$invoice = $this->di['em']->find(Invoice::class, $invoice->getId());
|
$invoice = $this->di['em']->find(Invoice::class, $invoice->getId());
|
||||||
@@ -114,7 +114,7 @@ class ServiceInvoiceItem implements InjectionAwareInterface
|
|||||||
try {
|
try {
|
||||||
$orderService->activateOrder($order);
|
$orderService->activateOrder($order);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
error_log($e->getMessage());
|
$this->di['logger']->error($e->getMessage());
|
||||||
$orderService->saveStatusChange($order, "Order could not be activated due to error: {$e->getMessage()}.");
|
$orderService->saveStatusChange($order, "Order could not be activated due to error: {$e->getMessage()}.");
|
||||||
$taskFailed = true;
|
$taskFailed = true;
|
||||||
}
|
}
|
||||||
@@ -132,7 +132,7 @@ class ServiceInvoiceItem implements InjectionAwareInterface
|
|||||||
$order = $this->di['em']->getRepository(Order::class)->find($order_id);
|
$order = $this->di['em']->getRepository(Order::class)->find($order_id);
|
||||||
$orderService->renewOrder($order);
|
$orderService->renewOrder($order);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
error_log($e->getMessage());
|
$this->di['logger']->error($e->getMessage());
|
||||||
$orderService->saveStatusChange($order, "Order could not renew due to error: {$e->getMessage()}.");
|
$orderService->saveStatusChange($order, "Order could not renew due to error: {$e->getMessage()}.");
|
||||||
$taskFailed = true;
|
$taskFailed = true;
|
||||||
}
|
}
|
||||||
@@ -144,7 +144,7 @@ class ServiceInvoiceItem implements InjectionAwareInterface
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
error_log($e->getMessage());
|
$this->di['logger']->error($e->getMessage());
|
||||||
$taskFailed = true;
|
$taskFailed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,7 +162,7 @@ class ServiceInvoiceItem implements InjectionAwareInterface
|
|||||||
$params = json_decode($item->getRelId() ?? '');
|
$params = json_decode($item->getRelId() ?? '');
|
||||||
$this->di['events_manager']->fire(['event' => $item->getTask(), 'params' => $params]);
|
$this->di['events_manager']->fire(['event' => $item->getTask(), 'params' => $params]);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
error_log($e->getMessage());
|
$this->di['logger']->error($e->getMessage());
|
||||||
$taskFailed = true;
|
$taskFailed = true;
|
||||||
}
|
}
|
||||||
if (!$taskFailed) {
|
if (!$taskFailed) {
|
||||||
@@ -269,7 +269,7 @@ class ServiceInvoiceItem implements InjectionAwareInterface
|
|||||||
$id = $model->getId();
|
$id = $model->getId();
|
||||||
$this->di['em']->remove($model);
|
$this->di['em']->remove($model);
|
||||||
$this->di['em']->flush();
|
$this->di['em']->flush();
|
||||||
$this->di['logger']->info('Removed invoice item "%s"', $id);
|
$this->di['logger']->info('Removed invoice item "{id}"', ['id' => $id]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -307,7 +307,7 @@ class ServiceInvoiceItem implements InjectionAwareInterface
|
|||||||
try {
|
try {
|
||||||
$this->di['em']->flush();
|
$this->di['em']->flush();
|
||||||
} catch (UniqueConstraintViolationException) {
|
} catch (UniqueConstraintViolationException) {
|
||||||
$this->di['logger']->setChannel('billing')->info(sprintf('Invoice item #%d was already credited; skipping duplicate credit.', (int) $item->getId()));
|
$this->di['logger']->withChannel('billing')->info('Invoice item #{item_id} was already credited; skipping duplicate credit.', ['item_id' => $item->getId()]);
|
||||||
$this->resetEntityManager();
|
$this->resetEntityManager();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@@ -376,7 +376,7 @@ class ServiceInvoiceItem implements InjectionAwareInterface
|
|||||||
|
|
||||||
if ($attempts >= self::MAX_TASK_ATTEMPTS) {
|
if ($attempts >= self::MAX_TASK_ATTEMPTS) {
|
||||||
$item->setStatus(InvoiceItem::STATUS_FAILED);
|
$item->setStatus(InvoiceItem::STATUS_FAILED);
|
||||||
$this->di['logger']->setChannel('billing')->error(sprintf('Invoice item #%d marked as failed after %d task execution attempts.', (int) $item->getId(), $attempts));
|
$this->di['logger']->withChannel('billing')->error('Invoice item #{item_id} marked as failed after {attempts} task execution attempts.', ['item_id' => $item->getId(), 'attempts' => $attempts]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->di['em']->persist($item);
|
$this->di['em']->persist($item);
|
||||||
@@ -403,7 +403,7 @@ class ServiceInvoiceItem implements InjectionAwareInterface
|
|||||||
$item->setAttempts(0);
|
$item->setAttempts(0);
|
||||||
$this->di['em']->persist($item);
|
$this->di['em']->persist($item);
|
||||||
$this->di['em']->flush();
|
$this->di['em']->flush();
|
||||||
$this->di['logger']->setChannel('billing')->info(sprintf('Invoice item #%d re-queued for execution by an admin.', (int) $item->getId()));
|
$this->di['logger']->withChannel('billing')->info('Invoice item #{item_id} re-queued for execution by an admin.', ['item_id' => $item->getId()]);
|
||||||
|
|
||||||
return $item;
|
return $item;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ class ServicePayGateway implements InjectionAwareInterface
|
|||||||
$this->di['em']->persist($new);
|
$this->di['em']->persist($new);
|
||||||
$this->di['em']->flush();
|
$this->di['em']->flush();
|
||||||
|
|
||||||
$this->di['logger']->info('Installed new payment gateway %s', $code);
|
$this->di['logger']->info('Installed new payment gateway {code}', ['code' => $code]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -175,7 +175,7 @@ class ServicePayGateway implements InjectionAwareInterface
|
|||||||
$this->di['em']->persist($new);
|
$this->di['em']->persist($new);
|
||||||
$this->di['em']->flush();
|
$this->di['em']->flush();
|
||||||
$newId = (int) $new->getId();
|
$newId = (int) $new->getId();
|
||||||
$this->di['logger']->info('Copied payment gateway #%s - %s', $newId, $model->getGateway());
|
$this->di['logger']->info('Copied payment gateway #{gateway_id} - {gateway}', ['gateway_id' => $newId, 'gateway' => $model->getGateway()]);
|
||||||
|
|
||||||
return $newId;
|
return $newId;
|
||||||
}
|
}
|
||||||
@@ -208,7 +208,7 @@ class ServicePayGateway implements InjectionAwareInterface
|
|||||||
$model->setAllowRecurrent((bool) ($data['allow_recurrent'] ?? $model->isAllowRecurrent()));
|
$model->setAllowRecurrent((bool) ($data['allow_recurrent'] ?? $model->isAllowRecurrent()));
|
||||||
$model->setTestMode($newTestMode);
|
$model->setTestMode($newTestMode);
|
||||||
$this->di['em']->flush();
|
$this->di['em']->flush();
|
||||||
$this->di['logger']->info('Updated payment gateway %s', $model->getGateway());
|
$this->di['logger']->info('Updated payment gateway {model_gateway}', ['model_gateway' => $model->getGateway()]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -242,7 +242,7 @@ class ServicePayGateway implements InjectionAwareInterface
|
|||||||
$id = $model->getId();
|
$id = $model->getId();
|
||||||
$this->di['em']->remove($model);
|
$this->di['em']->remove($model);
|
||||||
$this->di['em']->flush();
|
$this->di['em']->flush();
|
||||||
$this->di['logger']->info('Removed payment gateway %s', $id);
|
$this->di['logger']->info('Removed payment gateway {id}', ['id' => $id]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -365,7 +365,7 @@ class ServicePayGateway implements InjectionAwareInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!method_exists($class, 'getConfig')) {
|
if (!method_exists($class, 'getConfig')) {
|
||||||
error_log("Payment $class gateway does not have getConfig method");
|
$this->di['logger']->error("Payment $class gateway does not have getConfig method");
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ class ServiceSubscription implements InjectionAwareInterface
|
|||||||
|
|
||||||
$this->di['events_manager']->fire(['event' => 'onAfterAdminSubscriptionCreate', 'params' => ['id' => $newId]]);
|
$this->di['events_manager']->fire(['event' => 'onAfterAdminSubscriptionCreate', 'params' => ['id' => $newId]]);
|
||||||
|
|
||||||
$this->di['logger']->info('Created subscription %s', $newId);
|
$this->di['logger']->info('Created subscription {subscription_id}', ['subscription_id' => $newId]);
|
||||||
|
|
||||||
return $newId;
|
return $newId;
|
||||||
}
|
}
|
||||||
@@ -96,7 +96,7 @@ class ServiceSubscription implements InjectionAwareInterface
|
|||||||
$this->di['em']->flush();
|
$this->di['em']->flush();
|
||||||
$newId = (int) $model->getId();
|
$newId = (int) $model->getId();
|
||||||
|
|
||||||
$this->di['logger']->info('Updated subscription %s', $newId);
|
$this->di['logger']->info('Updated subscription {subscription_id}', ['subscription_id' => $newId]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -140,7 +140,7 @@ class ServiceSubscription implements InjectionAwareInterface
|
|||||||
|
|
||||||
$this->di['events_manager']->fire(['event' => 'onAfterAdminSubscriptionDelete', 'params' => ['id' => $id]]);
|
$this->di['events_manager']->fire(['event' => 'onAfterAdminSubscriptionDelete', 'params' => ['id' => $id]]);
|
||||||
|
|
||||||
$this->di['logger']->info('Removed subscription %s', $id);
|
$this->di['logger']->info('Removed subscription {id}', ['id' => $id]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ class ServiceTax implements InjectionAwareInterface
|
|||||||
$name = $model->getName();
|
$name = $model->getName();
|
||||||
$this->di['em']->remove($model);
|
$this->di['em']->remove($model);
|
||||||
$this->di['em']->flush();
|
$this->di['em']->flush();
|
||||||
$this->di['logger']->info('Deleted tax rule %s', $name);
|
$this->di['logger']->info('Deleted tax rule {name}', ['name' => $name]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -111,7 +111,7 @@ class ServiceTax implements InjectionAwareInterface
|
|||||||
$this->di['em']->persist($model);
|
$this->di['em']->persist($model);
|
||||||
$this->di['em']->flush();
|
$this->di['em']->flush();
|
||||||
|
|
||||||
$this->di['logger']->info('Created new tax rule %s', $model->getName());
|
$this->di['logger']->info('Created new tax rule {model_name}', ['model_name' => $model->getName()]);
|
||||||
|
|
||||||
return $model->getId();
|
return $model->getId();
|
||||||
}
|
}
|
||||||
@@ -124,7 +124,7 @@ class ServiceTax implements InjectionAwareInterface
|
|||||||
$model->setTaxrate($data['taxrate']);
|
$model->setTaxrate($data['taxrate']);
|
||||||
$this->di['em']->flush();
|
$this->di['em']->flush();
|
||||||
|
|
||||||
$this->di['logger']->info('Updated tax rule %s', $model->getName());
|
$this->di['logger']->info('Updated tax rule {model_name}', ['model_name' => $model->getName()]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,13 @@ class ServiceTransaction implements InjectionAwareInterface
|
|||||||
if ($model === null) {
|
if ($model === null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
$this->preProcessTransaction($model);
|
$this->preProcessTransaction($model);
|
||||||
|
} catch (\Throwable) {
|
||||||
|
// The individual transaction has already been marked as failed.
|
||||||
|
// Continue processing the rest of the batch.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -97,7 +103,7 @@ class ServiceTransaction implements InjectionAwareInterface
|
|||||||
$this->di['em']->flush();
|
$this->di['em']->flush();
|
||||||
$this->di['events_manager']->fire(['event' => 'onAfterAdminTransactionUpdate', 'params' => ['id' => $model->getId()]]);
|
$this->di['events_manager']->fire(['event' => 'onAfterAdminTransactionUpdate', 'params' => ['id' => $model->getId()]]);
|
||||||
|
|
||||||
$this->di['logger']->info('Updated transaction #%s', $model->getId());
|
$this->di['logger']->info('Updated transaction #{model_id}', ['model_id' => $model->getId()]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -114,13 +120,7 @@ class ServiceTransaction implements InjectionAwareInterface
|
|||||||
return $id;
|
return $id;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
$this->processTransactionWithErrorHandling((int) $id);
|
||||||
$this->processTransaction($id);
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
$this->markTransactionError($id, $e);
|
|
||||||
|
|
||||||
throw $e;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $id;
|
return $id;
|
||||||
}
|
}
|
||||||
@@ -149,6 +149,17 @@ class ServiceTransaction implements InjectionAwareInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function processTransactionWithErrorHandling(int $id): mixed
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return $this->processTransaction($id);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$this->markTransactionError($id, $e);
|
||||||
|
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function create(array $data): ?int
|
public function create(array $data): ?int
|
||||||
{
|
{
|
||||||
$this->di['events_manager']->fire(['event' => 'onBeforeAdminTransactionCreate', 'params' => $data]);
|
$this->di['events_manager']->fire(['event' => 'onBeforeAdminTransactionCreate', 'params' => $data]);
|
||||||
@@ -195,7 +206,7 @@ class ServiceTransaction implements InjectionAwareInterface
|
|||||||
if ($txnIdCandidate && !empty($data['gateway_id'])) {
|
if ($txnIdCandidate && !empty($data['gateway_id'])) {
|
||||||
$existing = $this->getTransactionRepository()->findOneByTxnIdAndGatewayId((string) $txnIdCandidate, (int) $data['gateway_id']);
|
$existing = $this->getTransactionRepository()->findOneByTxnIdAndGatewayId((string) $txnIdCandidate, (int) $data['gateway_id']);
|
||||||
if ($existing !== null && $existing->getStatus() === Transaction::STATUS_PROCESSED) {
|
if ($existing !== null && $existing->getStatus() === Transaction::STATUS_PROCESSED) {
|
||||||
$this->di['logger']->info('Duplicate transaction ignored, returning existing processed transaction #%s', $existing->getId());
|
$this->di['logger']->info('Duplicate transaction ignored, returning existing processed transaction #{existing_id}', ['existing_id' => $existing->getId()]);
|
||||||
|
|
||||||
return $existing->getId();
|
return $existing->getId();
|
||||||
}
|
}
|
||||||
@@ -216,7 +227,7 @@ class ServiceTransaction implements InjectionAwareInterface
|
|||||||
if ($supportsIpnHash && !empty($data['gateway_id']) && !empty($ipn_hash)) {
|
if ($supportsIpnHash && !empty($data['gateway_id']) && !empty($ipn_hash)) {
|
||||||
$existingByHash = $this->getTransactionRepository()->findOneByGatewayIdAndIpnHash((int) $data['gateway_id'], $ipn_hash);
|
$existingByHash = $this->getTransactionRepository()->findOneByGatewayIdAndIpnHash((int) $data['gateway_id'], $ipn_hash);
|
||||||
if ($existingByHash !== null) {
|
if ($existingByHash !== null) {
|
||||||
$this->di['logger']->info('Duplicate transaction detected by IPN hash, returning existing transaction #%s', $existingByHash->getId());
|
$this->di['logger']->info('Duplicate transaction detected by IPN hash, returning existing transaction #{transaction_id}', ['transaction_id' => $existingByHash->getId()]);
|
||||||
|
|
||||||
return $existingByHash->getId();
|
return $existingByHash->getId();
|
||||||
}
|
}
|
||||||
@@ -241,7 +252,7 @@ class ServiceTransaction implements InjectionAwareInterface
|
|||||||
$this->di['em']->flush();
|
$this->di['em']->flush();
|
||||||
$newId = (int) $transaction->getId();
|
$newId = (int) $transaction->getId();
|
||||||
|
|
||||||
$this->di['logger']->info('Received transaction %s from payment gateway %s', $newId, $transaction->getGateway()?->getId());
|
$this->di['logger']->info('Received transaction {transaction_id} from payment gateway {gateway_id}', ['transaction_id' => $newId, 'gateway_id' => $transaction->getGateway()?->getId()]);
|
||||||
|
|
||||||
$this->di['events_manager']->fire(['event' => 'onAfterAdminTransactionCreate', 'params' => ['id' => $newId]]);
|
$this->di['events_manager']->fire(['event' => 'onAfterAdminTransactionCreate', 'params' => ['id' => $newId]]);
|
||||||
|
|
||||||
@@ -262,7 +273,7 @@ class ServiceTransaction implements InjectionAwareInterface
|
|||||||
$supported = in_array('ipn_hash', $columns, true) && in_array('transaction_ipn_hash_idx', $indexes, true);
|
$supported = in_array('ipn_hash', $columns, true) && in_array('transaction_ipn_hash_idx', $indexes, true);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
if (isset($this->di['logger'])) {
|
if (isset($this->di['logger'])) {
|
||||||
$this->di['logger']->warning('Could not determine whether transaction.ipn_hash exists; disabling IPN hash dedupe: %s', $e->getMessage());
|
$this->di['logger']->warning('Could not determine whether transaction.ipn_hash exists; disabling IPN hash dedupe: {exception}', ['exception' => $e]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -278,7 +289,7 @@ class ServiceTransaction implements InjectionAwareInterface
|
|||||||
$id = $model->getId();
|
$id = $model->getId();
|
||||||
$this->di['em']->remove($model);
|
$this->di['em']->remove($model);
|
||||||
$this->di['em']->flush();
|
$this->di['em']->flush();
|
||||||
$this->di['logger']->info('Removed transaction #%s', $id);
|
$this->di['logger']->info('Removed transaction #{id}', ['id' => $id]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -466,16 +477,10 @@ class ServiceTransaction implements InjectionAwareInterface
|
|||||||
|
|
||||||
public function preProcessTransaction(Transaction $model)
|
public function preProcessTransaction(Transaction $model)
|
||||||
{
|
{
|
||||||
try {
|
$output = $this->processTransactionWithErrorHandling((int) $model->getId());
|
||||||
$output = $this->processTransaction($model->getId());
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
$this->markTransactionError((int) $model->getId(), $e);
|
|
||||||
|
|
||||||
throw $e;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->di['events_manager']->fire(['event' => 'onAfterAdminTransactionProcess', 'params' => ['id' => $model->getId()]]);
|
$this->di['events_manager']->fire(['event' => 'onAfterAdminTransactionProcess', 'params' => ['id' => $model->getId()]]);
|
||||||
$this->di['logger']->info('Processed transaction #%s', $model->getId());
|
$this->di['logger']->info('Processed transaction #{model_id}', ['model_id' => $model->getId()]);
|
||||||
|
|
||||||
return !empty($output) ? $output : true;
|
return !empty($output) ? $output : true;
|
||||||
}
|
}
|
||||||
@@ -505,7 +510,7 @@ class ServiceTransaction implements InjectionAwareInterface
|
|||||||
$tx->setUpdatedAt(new \DateTime());
|
$tx->setUpdatedAt(new \DateTime());
|
||||||
$this->di['em']->flush();
|
$this->di['em']->flush();
|
||||||
|
|
||||||
$this->di['logger']->error('Failed to process transaction #%s: %s', $id, $e->getMessage());
|
$this->di['logger']->error('Failed to process transaction #{id}: {exception}', ['id' => $id, 'exception' => $e]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -568,8 +573,8 @@ class ServiceTransaction implements InjectionAwareInterface
|
|||||||
$transaction->setUpdatedAt(new \DateTime());
|
$transaction->setUpdatedAt(new \DateTime());
|
||||||
$this->di['em']->flush();
|
$this->di['em']->flush();
|
||||||
|
|
||||||
if (defined('DEBUG')) {
|
if (DEBUG) {
|
||||||
error_log($e->getMessage());
|
$this->di['logger']->debug($e->getMessage());
|
||||||
}
|
}
|
||||||
if (Environment::isTesting()) {
|
if (Environment::isTesting()) {
|
||||||
throw $e;
|
throw $e;
|
||||||
@@ -771,8 +776,8 @@ class ServiceTransaction implements InjectionAwareInterface
|
|||||||
$invoiceService = $this->di['mod_service']('Invoice');
|
$invoiceService = $this->di['mod_service']('Invoice');
|
||||||
$invoiceService->tryPayWithCredits($invoice);
|
$invoiceService->tryPayWithCredits($invoice);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
if (defined('DEBUG')) {
|
if (DEBUG) {
|
||||||
error_log($e->getMessage());
|
$this->di['logger']->debug($e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -276,7 +276,7 @@ test('gets payment adapter', function (): void {
|
|||||||
->atLeast()->once()
|
->atLeast()->once()
|
||||||
->andReturn($expected);
|
->andReturn($expected);
|
||||||
|
|
||||||
$urlMock = Mockery::mock('\Box_Url');
|
$urlMock = Mockery::mock(FOSSBilling\Url::class);
|
||||||
$urlMock->shouldReceive('link')
|
$urlMock->shouldReceive('link')
|
||||||
->atLeast()->once();
|
->atLeast()->once();
|
||||||
|
|
||||||
@@ -311,7 +311,7 @@ test('throws exception when payment gateway adapter class is missing', function
|
|||||||
->atLeast()->once()
|
->atLeast()->once()
|
||||||
->andReturn('');
|
->andReturn('');
|
||||||
|
|
||||||
$urlMock = Mockery::mock('\Box_Url');
|
$urlMock = Mockery::mock(FOSSBilling\Url::class);
|
||||||
$urlMock->shouldReceive('link')
|
$urlMock->shouldReceive('link')
|
||||||
->atLeast()->once();
|
->atLeast()->once();
|
||||||
|
|
||||||
@@ -350,6 +350,26 @@ test('gets adapter config', function (): void {
|
|||||||
expect($result)->toBeArray();
|
expect($result)->toBeArray();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('logs and returns an empty config when an adapter has no getConfig method', function (): void {
|
||||||
|
$payGateway = createEntity(PayGateway::class, ['gateway' => 'Custom']);
|
||||||
|
|
||||||
|
$serviceMock = Mockery::mock(ServicePayGateway::class)->makePartial();
|
||||||
|
$serviceMock->shouldReceive('getAdapterClassName')
|
||||||
|
->once()
|
||||||
|
->andReturn(stdClass::class);
|
||||||
|
|
||||||
|
$service = payGatewayService();
|
||||||
|
$logger = new Tests\Helpers\TestLogger();
|
||||||
|
$service->getDi()['logger'] = $logger;
|
||||||
|
$serviceMock->setDi($service->getDi());
|
||||||
|
|
||||||
|
expect($serviceMock->getAdapterConfig($payGateway))->toBe([])
|
||||||
|
->and($logger->calls)->toContain([
|
||||||
|
'method' => 'error',
|
||||||
|
'params' => ['Payment stdClass gateway does not have getConfig method'],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
test('throws exception when adapter class does not exist', function (): void {
|
test('throws exception when adapter class does not exist', function (): void {
|
||||||
$payGateway = createEntity(PayGateway::class, ['gateway' => 'Custom']);
|
$payGateway = createEntity(PayGateway::class, ['gateway' => 'Custom']);
|
||||||
$expected = 'Payment_Adapter_ClassDoesNotExists';
|
$expected = 'Payment_Adapter_ClassDoesNotExists';
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ test('converts to api array', function (): void {
|
|||||||
->atLeast()->once()
|
->atLeast()->once()
|
||||||
->andReturn([$invoiceItemModel]);
|
->andReturn([$invoiceItemModel]);
|
||||||
|
|
||||||
$periodMock = Mockery::mock('\Box_Period');
|
$periodMock = Mockery::mock(FOSSBilling\Period::class);
|
||||||
$periodMock->shouldReceive('getUnit');
|
$periodMock->shouldReceive('getUnit');
|
||||||
$periodMock->shouldReceive('getQty');
|
$periodMock->shouldReceive('getQty');
|
||||||
|
|
||||||
@@ -313,7 +313,7 @@ test('to api array self-heals invoice with missing hash', function (): void {
|
|||||||
$em->shouldReceive('flush')
|
$em->shouldReceive('flush')
|
||||||
->atLeast()->once();
|
->atLeast()->once();
|
||||||
|
|
||||||
$periodMock = Mockery::mock('\Box_Period');
|
$periodMock = Mockery::mock(FOSSBilling\Period::class);
|
||||||
$periodMock->shouldReceive('getUnit');
|
$periodMock->shouldReceive('getUnit');
|
||||||
$periodMock->shouldReceive('getQty');
|
$periodMock->shouldReceive('getQty');
|
||||||
|
|
||||||
|
|||||||
@@ -307,6 +307,33 @@ test('preProcessTransaction marks error on a generic exception', function (): vo
|
|||||||
->and($transactionModel->getError())->toBe('Unexpected DB error');
|
->and($transactionModel->getError())->toBe('Unexpected DB error');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('processes the rest of a received transaction batch after a failure', function (): void {
|
||||||
|
$first = createEntity(Transaction::class, ['id' => 1]);
|
||||||
|
$second = createEntity(Transaction::class, ['id' => 2]);
|
||||||
|
|
||||||
|
$transactionRepository = Mockery::mock(TransactionRepository::class);
|
||||||
|
$transactionRepository->shouldReceive('find')->once()->with(1)->andReturn($first);
|
||||||
|
$transactionRepository->shouldReceive('find')->once()->with(2)->andReturn($second);
|
||||||
|
|
||||||
|
$em = Mockery::mock(EntityManagerInterface::class);
|
||||||
|
$em->shouldReceive('getRepository')->with(Transaction::class)->andReturn($transactionRepository);
|
||||||
|
|
||||||
|
$di = container();
|
||||||
|
$di['em'] = $em;
|
||||||
|
$di['logger'] = new Tests\Helpers\TestLogger();
|
||||||
|
|
||||||
|
$service = Mockery::mock(ServiceTransaction::class)->makePartial();
|
||||||
|
$service->shouldReceive('getReceived')->once()->andReturn([
|
||||||
|
['id' => 1],
|
||||||
|
['id' => 2],
|
||||||
|
]);
|
||||||
|
$service->shouldReceive('preProcessTransaction')->once()->with($first)->andThrow(new RuntimeException('First transaction failed'));
|
||||||
|
$service->shouldReceive('preProcessTransaction')->once()->with($second)->andReturnTrue();
|
||||||
|
$service->setDi($di);
|
||||||
|
|
||||||
|
expect($service->processReceivedATransactions())->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
test('claimForProcessing includes error status in claim query', function (): void {
|
test('claimForProcessing includes error status in claim query', function (): void {
|
||||||
$execArgs = [];
|
$execArgs = [];
|
||||||
$connection = Mockery::mock(Doctrine\DBAL\Connection::class);
|
$connection = Mockery::mock(Doctrine\DBAL\Connection::class);
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ class Admin extends \FOSSBilling\Api\AbstractApi
|
|||||||
|
|
||||||
$this->getDi()['em']->flush();
|
$this->getDi()['em']->flush();
|
||||||
|
|
||||||
$this->getDi()['logger']->info('Updated mail message #%s', $model->getId());
|
$this->getDi()['logger']->info('Updated mail message #{model_id}', ['model_id' => $model->getId()]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -140,7 +140,7 @@ Order our services at {{ "order"|url }}
|
|||||||
throw new \FOSSBilling\Exception('Failed to retrieve ID of created mail message.');
|
throw new \FOSSBilling\Exception('Failed to retrieve ID of created mail message.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->getDi()['logger']->info('Created mail message #%s', $id);
|
$this->getDi()['logger']->info('Created mail message #{id}', ['id' => $id]);
|
||||||
|
|
||||||
return $id;
|
return $id;
|
||||||
}
|
}
|
||||||
@@ -161,7 +161,7 @@ Order our services at {{ "order"|url }}
|
|||||||
|
|
||||||
$this->getService()->sendMessage($model, $client_id, true);
|
$this->getService()->sendMessage($model, $client_id, true);
|
||||||
|
|
||||||
$this->getDi()['logger']->info('Sent test mail message #%s to client ', $model->getId());
|
$this->getDi()['logger']->info('Sent test mail message #{model_id} to client ', ['model_id' => $model->getId()]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -188,7 +188,7 @@ Order our services at {{ "order"|url }}
|
|||||||
$model->setSentAt(new \DateTime());
|
$model->setSentAt(new \DateTime());
|
||||||
$this->getDi()['em']->flush();
|
$this->getDi()['em']->flush();
|
||||||
|
|
||||||
$this->getDi()['logger']->info('Added mass mail messages #%s to queue', $model->getId());
|
$this->getDi()['logger']->info('Added mass mail messages #{model_id} to queue', ['model_id' => $model->getId()]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -218,7 +218,7 @@ Order our services at {{ "order"|url }}
|
|||||||
throw new \FOSSBilling\Exception('Failed to retrieve ID of copied mail message.');
|
throw new \FOSSBilling\Exception('Failed to retrieve ID of copied mail message.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->getDi()['logger']->info('Copied mail message #%s to #%s', $model->getId(), $id);
|
$this->getDi()['logger']->info('Copied mail message #{model_id} to #{id}', ['model_id' => $model->getId(), 'id' => $id]);
|
||||||
|
|
||||||
return $id;
|
return $id;
|
||||||
}
|
}
|
||||||
@@ -248,7 +248,7 @@ Order our services at {{ "order"|url }}
|
|||||||
$this->getDi()['em']->remove($model);
|
$this->getDi()['em']->remove($model);
|
||||||
$this->getDi()['em']->flush();
|
$this->getDi()['em']->flush();
|
||||||
|
|
||||||
$this->getDi()['logger']->info('Removed mail message #%s', $id);
|
$this->getDi()['logger']->info('Removed mail message #{id}', ['id' => $id]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ class Service implements \FOSSBilling\InjectionAwareInterface
|
|||||||
if (!Environment::isProduction()) {
|
if (!Environment::isProduction()) {
|
||||||
// @phpstan-ignore if.alwaysFalse
|
// @phpstan-ignore if.alwaysFalse
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
error_log('Skip email sending. Application ENV: ' . Environment::getCurrentEnvironment());
|
$this->di['logger']->debug('Skip email sending. Application ENV: ' . Environment::getCurrentEnvironment());
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ test('update stores normalized filter', function (): void {
|
|||||||
|
|
||||||
$service = new Box\Mod\Massmailer\Service();
|
$service = new Box\Mod\Massmailer\Service();
|
||||||
$di = createMassmailerAdminDi($model);
|
$di = createMassmailerAdminDi($model);
|
||||||
$di['logger'] = new Box_Log();
|
$di['logger'] = new FOSSBilling\Logger();
|
||||||
$service->setDi($di);
|
$service->setDi($di);
|
||||||
|
|
||||||
$api = apiEndpoint(new Box\Mod\Massmailer\Api\Admin());
|
$api = apiEndpoint(new Box\Mod\Massmailer\Api\Admin());
|
||||||
@@ -65,7 +65,7 @@ test('update rejects invalid filter', function (): void {
|
|||||||
|
|
||||||
$service = new Box\Mod\Massmailer\Service();
|
$service = new Box\Mod\Massmailer\Service();
|
||||||
$di = createMassmailerAdminDi($model, false);
|
$di = createMassmailerAdminDi($model, false);
|
||||||
$di['logger'] = new Box_Log();
|
$di['logger'] = new FOSSBilling\Logger();
|
||||||
$service->setDi($di);
|
$service->setDi($di);
|
||||||
|
|
||||||
$api = apiEndpoint(new Box\Mod\Massmailer\Api\Admin());
|
$api = apiEndpoint(new Box\Mod\Massmailer\Api\Admin());
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ class Admin extends \FOSSBilling\Api\AbstractApi
|
|||||||
$this->getDi()['em']->persist($post);
|
$this->getDi()['em']->persist($post);
|
||||||
$this->getDi()['em']->flush();
|
$this->getDi()['em']->flush();
|
||||||
|
|
||||||
$this->getDi()['logger']->info('Updated news item #%s', $post->getId());
|
$this->getDi()['logger']->info('Updated news item #{post_id}', ['post_id' => $post->getId()]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -142,7 +142,7 @@ class Admin extends \FOSSBilling\Api\AbstractApi
|
|||||||
$this->getDi()['em']->persist($post);
|
$this->getDi()['em']->persist($post);
|
||||||
$this->getDi()['em']->flush();
|
$this->getDi()['em']->flush();
|
||||||
|
|
||||||
$this->getDi()['logger']->info('Created news item #%s', $post->getId());
|
$this->getDi()['logger']->info('Created news item #{post_id}', ['post_id' => $post->getId()]);
|
||||||
|
|
||||||
return $post->getId();
|
return $post->getId();
|
||||||
}
|
}
|
||||||
@@ -167,7 +167,7 @@ class Admin extends \FOSSBilling\Api\AbstractApi
|
|||||||
$this->getDi()['em']->remove($post);
|
$this->getDi()['em']->remove($post);
|
||||||
$this->getDi()['em']->flush();
|
$this->getDi()['em']->flush();
|
||||||
|
|
||||||
$this->getDi()['logger']->info('Removed news item #%s', $data['id']);
|
$this->getDi()['logger']->info('Removed news item #{data_id}', ['data_id' => $data['id']]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -185,7 +185,7 @@ class Admin extends \FOSSBilling\Api\AbstractApi
|
|||||||
|
|
||||||
$count = $repo->deleteByIds($data['ids']);
|
$count = $repo->deleteByIds($data['ids']);
|
||||||
|
|
||||||
$this->getDi()['logger']->info('Removed %s news items', $count);
|
$this->getDi()['logger']->info('Removed {count} news items', ['count' => $count]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
+64
-144
@@ -24,6 +24,7 @@ use Box\Mod\Product\Entity\Product;
|
|||||||
use Box\Mod\Staff\Entity\Admin;
|
use Box\Mod\Staff\Entity\Admin;
|
||||||
use FOSSBilling\InformationException;
|
use FOSSBilling\InformationException;
|
||||||
use FOSSBilling\InjectionAwareInterface;
|
use FOSSBilling\InjectionAwareInterface;
|
||||||
|
use FOSSBilling\Logger;
|
||||||
use FOSSBilling\Validation\NonNegativeIntegerValidator;
|
use FOSSBilling\Validation\NonNegativeIntegerValidator;
|
||||||
use FOSSBilling\Validation\PriceValidator;
|
use FOSSBilling\Validation\PriceValidator;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
@@ -185,151 +186,65 @@ class Service implements InjectionAwareInterface
|
|||||||
$emailService = $di['mod_service']('email');
|
$emailService = $di['mod_service']('email');
|
||||||
$emailService->sendTemplate($email);
|
$emailService->sendTemplate($email);
|
||||||
} catch (\Exception $exc) {
|
} catch (\Exception $exc) {
|
||||||
$di['logger']->setChannel('email')->error('Failed to send order activation email', ['exception' => $exc->getMessage(), 'order_id' => $order_id]);
|
$di['logger']->withChannel('email')->error('Failed to send order activation email', ['exception' => $exc, 'order_id' => $order_id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function sendOrderLifecycleEmail(\Box_Event $event, string $templateSuffix, string $logAction, bool $includeService = true): void
|
||||||
|
{
|
||||||
|
$params = $event->getParameters();
|
||||||
|
$orderId = $params['id'];
|
||||||
|
$di = $event->getDi();
|
||||||
|
$orderService = $di['mod_service']('order');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$order = $di['em']->getRepository(Order::class)->find($orderId);
|
||||||
|
if (!$order instanceof Order) {
|
||||||
|
throw new \FOSSBilling\Exception('Order not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
$service = $includeService ? $orderService->getOrderServiceData($order) : null;
|
||||||
|
$orderArr = $orderService->toApiArray($order, true);
|
||||||
|
|
||||||
|
$email = [
|
||||||
|
'to_client' => $orderArr['client']['id'],
|
||||||
|
'code' => sprintf('mod_service%s_%s', $orderArr['service_type'], $templateSuffix),
|
||||||
|
];
|
||||||
|
if ($includeService) {
|
||||||
|
$email['service'] = $service;
|
||||||
|
}
|
||||||
|
$email['order'] = $orderArr;
|
||||||
|
|
||||||
|
$emailService = $di['mod_service']('email');
|
||||||
|
$emailService->sendTemplate($email);
|
||||||
|
} catch (\Exception $exc) {
|
||||||
|
$di['logger']->withChannel('email')->error('Failed to send order {action} email', ['action' => $logAction, 'exception' => $exc]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function onAfterAdminOrderRenew(\Box_Event $event): void
|
public static function onAfterAdminOrderRenew(\Box_Event $event): void
|
||||||
{
|
{
|
||||||
$params = $event->getParameters();
|
self::sendOrderLifecycleEmail($event, 'renewed', 'renewal');
|
||||||
$order_id = $params['id'];
|
|
||||||
$di = $event->getDi();
|
|
||||||
$orderService = $di['mod_service']('order');
|
|
||||||
|
|
||||||
try {
|
|
||||||
$order = $di['em']->getRepository(Order::class)->find($order_id);
|
|
||||||
if (!$order instanceof Order) {
|
|
||||||
throw new \FOSSBilling\Exception('Order not found');
|
|
||||||
}
|
|
||||||
$identity = $di['loggedin_admin'] ?? null;
|
|
||||||
$service = $orderService->getOrderServiceData($order, $identity);
|
|
||||||
$orderArr = $orderService->toApiArray($order, true, $identity);
|
|
||||||
|
|
||||||
$email = [];
|
|
||||||
$email['to_client'] = $orderArr['client']['id'];
|
|
||||||
$email['code'] = sprintf('mod_service%s_renewed', $orderArr['service_type']);
|
|
||||||
$email['service'] = $service;
|
|
||||||
$email['order'] = $orderArr;
|
|
||||||
|
|
||||||
$emailService = $di['mod_service']('email');
|
|
||||||
$emailService->sendTemplate($email);
|
|
||||||
} catch (\Exception $exc) {
|
|
||||||
$di['logger']->setChannel('email')->error('Failed to send order renewal email', ['exception' => $exc->getMessage()]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function onAfterAdminOrderSuspend(\Box_Event $event): void
|
public static function onAfterAdminOrderSuspend(\Box_Event $event): void
|
||||||
{
|
{
|
||||||
$params = $event->getParameters();
|
self::sendOrderLifecycleEmail($event, 'suspended', 'suspension');
|
||||||
$order_id = $params['id'];
|
|
||||||
$di = $event->getDi();
|
|
||||||
$service = $di['mod_service']('order');
|
|
||||||
|
|
||||||
try {
|
|
||||||
$order = $di['em']->getRepository(Order::class)->find($order_id);
|
|
||||||
if (!$order instanceof Order) {
|
|
||||||
throw new \FOSSBilling\Exception('Order not found');
|
|
||||||
}
|
|
||||||
$identity = $di['loggedin_admin'] ?? null;
|
|
||||||
$s = $service->getOrderServiceData($order, $identity);
|
|
||||||
$orderArr = $service->toApiArray($order, true, $identity);
|
|
||||||
|
|
||||||
$email = [];
|
|
||||||
$email['to_client'] = $orderArr['client']['id'];
|
|
||||||
$email['code'] = sprintf('mod_service%s_suspended', $orderArr['service_type']);
|
|
||||||
$email['service'] = $s;
|
|
||||||
$email['order'] = $orderArr;
|
|
||||||
|
|
||||||
$emailService = $di['mod_service']('email');
|
|
||||||
$emailService->sendTemplate($email);
|
|
||||||
} catch (\Exception $exc) {
|
|
||||||
$di['logger']->setChannel('email')->error('Failed to send order suspension email', ['exception' => $exc->getMessage()]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function onAfterAdminOrderUnsuspend(\Box_Event $event): void
|
public static function onAfterAdminOrderUnsuspend(\Box_Event $event): void
|
||||||
{
|
{
|
||||||
$params = $event->getParameters();
|
self::sendOrderLifecycleEmail($event, 'unsuspended', 'unsuspension');
|
||||||
$order_id = $params['id'];
|
|
||||||
$di = $event->getDi();
|
|
||||||
$service = $di['mod_service']('order');
|
|
||||||
|
|
||||||
try {
|
|
||||||
$order = $di['em']->getRepository(Order::class)->find($order_id);
|
|
||||||
if (!$order instanceof Order) {
|
|
||||||
throw new \FOSSBilling\Exception('Order not found');
|
|
||||||
}
|
|
||||||
$identity = $di['loggedin_admin'] ?? null;
|
|
||||||
$s = $service->getOrderServiceData($order, $identity);
|
|
||||||
$orderArr = $service->toApiArray($order, true, $identity);
|
|
||||||
|
|
||||||
$email = [];
|
|
||||||
$email['to_client'] = $orderArr['client']['id'];
|
|
||||||
$email['code'] = sprintf('mod_service%s_unsuspended', $orderArr['service_type']);
|
|
||||||
$email['service'] = $s;
|
|
||||||
$email['order'] = $orderArr;
|
|
||||||
|
|
||||||
$emailService = $di['mod_service']('email');
|
|
||||||
$emailService->sendTemplate($email);
|
|
||||||
} catch (\Exception $exc) {
|
|
||||||
$di['logger']->setChannel('email')->error('Failed to send order unsuspension email', ['exception' => $exc->getMessage()]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function onAfterAdminOrderCancel(\Box_Event $event): void
|
public static function onAfterAdminOrderCancel(\Box_Event $event): void
|
||||||
{
|
{
|
||||||
$params = $event->getParameters();
|
self::sendOrderLifecycleEmail($event, 'canceled', 'cancellation', false);
|
||||||
$order_id = $params['id'];
|
|
||||||
$di = $event->getDi();
|
|
||||||
$service = $di['mod_service']('order');
|
|
||||||
|
|
||||||
try {
|
|
||||||
$order = $di['em']->getRepository(Order::class)->find($order_id);
|
|
||||||
if (!$order instanceof Order) {
|
|
||||||
throw new \FOSSBilling\Exception('Order not found');
|
|
||||||
}
|
|
||||||
$identity = $di['loggedin_admin'] ?? null;
|
|
||||||
$orderArr = $service->toApiArray($order, true, $identity);
|
|
||||||
|
|
||||||
$email = [];
|
|
||||||
$email['to_client'] = $orderArr['client']['id'];
|
|
||||||
$email['code'] = sprintf('mod_service%s_canceled', $orderArr['service_type']);
|
|
||||||
$email['order'] = $orderArr;
|
|
||||||
|
|
||||||
$emailService = $di['mod_service']('email');
|
|
||||||
$emailService->sendTemplate($email);
|
|
||||||
} catch (\Exception $exc) {
|
|
||||||
$di['logger']->setChannel('email')->error('Failed to send order cancellation email', ['exception' => $exc->getMessage()]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function onAfterAdminOrderUncancel(\Box_Event $event): void
|
public static function onAfterAdminOrderUncancel(\Box_Event $event): void
|
||||||
{
|
{
|
||||||
$params = $event->getParameters();
|
self::sendOrderLifecycleEmail($event, 'renewed', 'uncancel');
|
||||||
$order_id = $params['id'];
|
|
||||||
$di = $event->getDi();
|
|
||||||
$service = $di['mod_service']('order');
|
|
||||||
|
|
||||||
try {
|
|
||||||
$order = $di['em']->getRepository(Order::class)->find($order_id);
|
|
||||||
if (!$order instanceof Order) {
|
|
||||||
throw new \FOSSBilling\Exception('Order not found');
|
|
||||||
}
|
|
||||||
$identity = $di['loggedin_admin'] ?? null;
|
|
||||||
$s = $service->getOrderServiceData($order, $identity);
|
|
||||||
$orderArr = $service->toApiArray($order, true, $identity);
|
|
||||||
|
|
||||||
$email = [];
|
|
||||||
$email['to_client'] = $orderArr['client']['id'];
|
|
||||||
$email['code'] = sprintf('mod_service%s_renewed', $orderArr['service_type']);
|
|
||||||
$email['order'] = $orderArr;
|
|
||||||
$email['service'] = $s;
|
|
||||||
|
|
||||||
$emailService = $di['mod_service']('email');
|
|
||||||
$emailService->sendTemplate($email);
|
|
||||||
} catch (\Exception $exc) {
|
|
||||||
$di['logger']->setChannel('email')->error('Failed to send order uncancel email', ['exception' => $exc->getMessage()]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -431,15 +346,14 @@ class Service implements InjectionAwareInterface
|
|||||||
return $order instanceof Order;
|
return $order instanceof Order;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getLogger(Order $order)
|
public function getLogger(Order $order): Logger
|
||||||
{
|
{
|
||||||
$orderId = $this->orderId($order);
|
$orderId = $this->orderId($order);
|
||||||
|
|
||||||
$log = $this->di['logger'];
|
return $this->di['logger']->withContext([
|
||||||
$log->setEventItem('client_order_id', $orderId);
|
'client_order_id' => $orderId,
|
||||||
$log->setEventItem('status', $order->getStatus());
|
'status' => $order->getStatus(),
|
||||||
|
]);
|
||||||
return $log;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1032,7 +946,7 @@ class Service implements InjectionAwareInterface
|
|||||||
|
|
||||||
$this->di['events_manager']->fire(['event' => 'onAfterAdminOrderCreate', 'params' => ['id' => $order->getId()], 'subject' => $this->getProductType($product)]);
|
$this->di['events_manager']->fire(['event' => 'onAfterAdminOrderCreate', 'params' => ['id' => $order->getId()], 'subject' => $this->getProductType($product)]);
|
||||||
|
|
||||||
$this->di['logger']->info('Created order #%s', $id);
|
$this->di['logger']->info('Created order #{id}', ['id' => $id]);
|
||||||
|
|
||||||
// activate immediately on creation
|
// activate immediately on creation
|
||||||
if ($activate) {
|
if ($activate) {
|
||||||
@@ -1111,7 +1025,7 @@ class Service implements InjectionAwareInterface
|
|||||||
|
|
||||||
$this->activateOrderAddons($order);
|
$this->activateOrderAddons($order);
|
||||||
|
|
||||||
$this->di['logger']->info('Activated order #%s', $orderId);
|
$this->di['logger']->info('Activated order #{order_id}', ['order_id' => $orderId]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1360,7 +1274,7 @@ class Service implements InjectionAwareInterface
|
|||||||
|
|
||||||
$this->di['events_manager']->fire(['event' => 'onAfterAdminOrderUpdate', 'params' => ['id' => $orderId]]);
|
$this->di['events_manager']->fire(['event' => 'onAfterAdminOrderUpdate', 'params' => ['id' => $orderId]]);
|
||||||
|
|
||||||
$this->di['logger']->info('Update order #%s', $orderId);
|
$this->di['logger']->info('Update order #{order_id}', ['order_id' => $orderId]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1386,7 +1300,7 @@ class Service implements InjectionAwareInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
$this->di['events_manager']->fire(['event' => 'onAfterAdminOrderRenew', 'params' => ['id' => $orderId]]);
|
$this->di['events_manager']->fire(['event' => 'onAfterAdminOrderRenew', 'params' => ['id' => $orderId]]);
|
||||||
$this->di['logger']->info('Renewed order #%s', $orderId);
|
$this->di['logger']->info('Renewed order #{order_id}', ['order_id' => $orderId]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1466,7 +1380,7 @@ class Service implements InjectionAwareInterface
|
|||||||
$this->di['events_manager']->fire(['event' => 'onAfterAdminOrderSuspend', 'params' => ['id' => $orderId]]);
|
$this->di['events_manager']->fire(['event' => 'onAfterAdminOrderSuspend', 'params' => ['id' => $orderId]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->di['logger']->info('Suspended order #%s', $orderId);
|
$this->di['logger']->info('Suspended order #{order_id}', ['order_id' => $orderId]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1489,7 +1403,7 @@ class Service implements InjectionAwareInterface
|
|||||||
|
|
||||||
$this->di['events_manager']->fire(['event' => 'onAfterAdminOrderUnsuspend', 'params' => ['id' => $orderId]]);
|
$this->di['events_manager']->fire(['event' => 'onAfterAdminOrderUnsuspend', 'params' => ['id' => $orderId]]);
|
||||||
|
|
||||||
$this->di['logger']->info('Unsuspended order #%s', $orderId);
|
$this->di['logger']->info('Unsuspended order #{order_id}', ['order_id' => $orderId]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1516,7 +1430,7 @@ class Service implements InjectionAwareInterface
|
|||||||
$this->di['events_manager']->fire(['event' => 'onAfterAdminOrderCancel', 'params' => ['id' => $orderId]]);
|
$this->di['events_manager']->fire(['event' => 'onAfterAdminOrderCancel', 'params' => ['id' => $orderId]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->di['logger']->info('Canceled order #%s', $orderId);
|
$this->di['logger']->info('Canceled order #{order_id}', ['order_id' => $orderId]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1540,7 +1454,7 @@ class Service implements InjectionAwareInterface
|
|||||||
$order->setUpdatedAt(new \DateTime());
|
$order->setUpdatedAt(new \DateTime());
|
||||||
$this->persistOrder($order);
|
$this->persistOrder($order);
|
||||||
$this->saveStatusChange($order, 'Cancellation scheduled at the end of the current billing period');
|
$this->saveStatusChange($order, 'Cancellation scheduled at the end of the current billing period');
|
||||||
$this->di['logger']->info('Scheduled cancellation for order #%s at the end of the current billing period', $orderId);
|
$this->di['logger']->info('Scheduled cancellation for order #{order_id} at the end of the current billing period', ['order_id' => $orderId]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1620,7 +1534,7 @@ class Service implements InjectionAwareInterface
|
|||||||
|
|
||||||
$this->di['events_manager']->fire(['event' => 'onAfterAdminOrderUncancel', 'params' => ['id' => $orderId]]);
|
$this->di['events_manager']->fire(['event' => 'onAfterAdminOrderUncancel', 'params' => ['id' => $orderId]]);
|
||||||
|
|
||||||
$this->di['logger']->info('Uncanceled order #%s', $orderId);
|
$this->di['logger']->info('Uncanceled order #{order_id}', ['order_id' => $orderId]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1687,7 +1601,7 @@ class Service implements InjectionAwareInterface
|
|||||||
$this->rmOrder($order);
|
$this->rmOrder($order);
|
||||||
|
|
||||||
$this->di['events_manager']->fire(['event' => 'onAfterAdminOrderDelete', 'params' => ['id' => $orderId]]);
|
$this->di['events_manager']->fire(['event' => 'onAfterAdminOrderDelete', 'params' => ['id' => $orderId]]);
|
||||||
$this->di['logger']->info('Deleted order #%s', $orderId);
|
$this->di['logger']->info('Deleted order #{order_id}', ['order_id' => $orderId]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1718,9 +1632,9 @@ class Service implements InjectionAwareInterface
|
|||||||
]);
|
]);
|
||||||
} catch (\Throwable $exception) {
|
} catch (\Throwable $exception) {
|
||||||
$this->releaseSuspensionWarningClaim($order, $candidate['suspension_at']);
|
$this->releaseSuspensionWarningClaim($order, $candidate['suspension_at']);
|
||||||
$this->di['logger']->setChannel('email')->error('Failed to send order suspension warning email', [
|
$this->di['logger']->withChannel('email')->error('Failed to send order suspension warning email', [
|
||||||
'order_id' => $order->getId(),
|
'order_id' => $order->getId(),
|
||||||
'exception' => $exception->getMessage(),
|
'exception' => $exception,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1865,7 +1779,10 @@ class Service implements InjectionAwareInterface
|
|||||||
$this->di['em']->persist($order);
|
$this->di['em']->persist($order);
|
||||||
$this->di['em']->flush();
|
$this->di['em']->flush();
|
||||||
|
|
||||||
$this->di['logger']->info(sprintf("Order #%s config changes:\n%s\n%s", $orderId, $oldConfig, $order->getConfig()));
|
$this->di['logger']->info(
|
||||||
|
"Order #{order_id} config changes:\n{old_config}\n{new_config}",
|
||||||
|
['order_id' => $orderId, 'old_config' => $oldConfig, 'new_config' => $order->getConfig()]
|
||||||
|
);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1944,7 +1861,10 @@ class Service implements InjectionAwareInterface
|
|||||||
$this->di['em']->persist($statusEntry);
|
$this->di['em']->persist($statusEntry);
|
||||||
$this->di['em']->flush();
|
$this->di['em']->flush();
|
||||||
|
|
||||||
$this->di['logger']->info('Added order status history message to order #%s', $statusEntry->getId());
|
$this->di['logger']->info(
|
||||||
|
'Added order status history entry {status_id} for order {order_id}',
|
||||||
|
['status_id' => $statusEntry->getId(), 'order_id' => $order->getId()]
|
||||||
|
);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -214,8 +214,8 @@ test('onAfterAdminOrderRenew fires template without an admin session', function
|
|||||||
|
|
||||||
$serviceMock = Mockery::mock(Service::class)->makePartial();
|
$serviceMock = Mockery::mock(Service::class)->makePartial();
|
||||||
$serviceMock->shouldAllowMockingProtectedMethods();
|
$serviceMock->shouldAllowMockingProtectedMethods();
|
||||||
$serviceMock->shouldReceive('getOrderServiceData')->once()->with($order, null)->andReturn([]);
|
$serviceMock->shouldReceive('getOrderServiceData')->once()->with($order)->andReturn([]);
|
||||||
$serviceMock->shouldReceive('toApiArray')->once()->with($order, true, null)->andReturn($orderArr);
|
$serviceMock->shouldReceive('toApiArray')->once()->with($order, true)->andReturn($orderArr);
|
||||||
|
|
||||||
$di = container();
|
$di = container();
|
||||||
$di['em']->getRepository(Order::class)->shouldReceive('find')->once()->with(1)->andReturn($order);
|
$di['em']->getRepository(Order::class)->shouldReceive('find')->once()->with(1)->andReturn($order);
|
||||||
@@ -339,8 +339,8 @@ test('onAfterAdminOrderSuspend fires template without an admin session', functio
|
|||||||
|
|
||||||
$serviceMock = Mockery::mock(Service::class)->makePartial();
|
$serviceMock = Mockery::mock(Service::class)->makePartial();
|
||||||
$serviceMock->shouldAllowMockingProtectedMethods();
|
$serviceMock->shouldAllowMockingProtectedMethods();
|
||||||
$serviceMock->shouldReceive('getOrderServiceData')->once()->with($order, null)->andReturn([]);
|
$serviceMock->shouldReceive('getOrderServiceData')->once()->with($order)->andReturn([]);
|
||||||
$serviceMock->shouldReceive('toApiArray')->once()->with($order, true, null)->andReturn($orderArr);
|
$serviceMock->shouldReceive('toApiArray')->once()->with($order, true)->andReturn($orderArr);
|
||||||
|
|
||||||
$di = container();
|
$di = container();
|
||||||
$di['em']->getRepository(Order::class)->shouldReceive('find')->once()->with(1)->andReturn($order);
|
$di['em']->getRepository(Order::class)->shouldReceive('find')->once()->with(1)->andReturn($order);
|
||||||
@@ -464,8 +464,8 @@ test('onAfterAdminOrderUnsuspend fires template without an admin session', funct
|
|||||||
|
|
||||||
$serviceMock = Mockery::mock(Service::class)->makePartial();
|
$serviceMock = Mockery::mock(Service::class)->makePartial();
|
||||||
$serviceMock->shouldAllowMockingProtectedMethods();
|
$serviceMock->shouldAllowMockingProtectedMethods();
|
||||||
$serviceMock->shouldReceive('getOrderServiceData')->once()->with($order, null)->andReturn([]);
|
$serviceMock->shouldReceive('getOrderServiceData')->once()->with($order)->andReturn([]);
|
||||||
$serviceMock->shouldReceive('toApiArray')->once()->with($order, true, null)->andReturn($orderArr);
|
$serviceMock->shouldReceive('toApiArray')->once()->with($order, true)->andReturn($orderArr);
|
||||||
|
|
||||||
$di = container();
|
$di = container();
|
||||||
$di['em']->getRepository(Order::class)->shouldReceive('find')->once()->with(1)->andReturn($order);
|
$di['em']->getRepository(Order::class)->shouldReceive('find')->once()->with(1)->andReturn($order);
|
||||||
@@ -587,7 +587,7 @@ test('onAfterAdminOrderCancel fires template without an admin session', function
|
|||||||
|
|
||||||
$serviceMock = Mockery::mock(Service::class)->makePartial();
|
$serviceMock = Mockery::mock(Service::class)->makePartial();
|
||||||
$serviceMock->shouldAllowMockingProtectedMethods();
|
$serviceMock->shouldAllowMockingProtectedMethods();
|
||||||
$serviceMock->shouldReceive('toApiArray')->once()->with($order, true, null)->andReturn($orderArr);
|
$serviceMock->shouldReceive('toApiArray')->once()->with($order, true)->andReturn($orderArr);
|
||||||
|
|
||||||
$di = container();
|
$di = container();
|
||||||
$di['em']->getRepository(Order::class)->shouldReceive('find')->once()->with(1)->andReturn($order);
|
$di['em']->getRepository(Order::class)->shouldReceive('find')->once()->with(1)->andReturn($order);
|
||||||
@@ -710,8 +710,8 @@ test('onAfterAdminOrderUncancel fires template without an admin session', functi
|
|||||||
|
|
||||||
$serviceMock = Mockery::mock(Service::class)->makePartial();
|
$serviceMock = Mockery::mock(Service::class)->makePartial();
|
||||||
$serviceMock->shouldAllowMockingProtectedMethods();
|
$serviceMock->shouldAllowMockingProtectedMethods();
|
||||||
$serviceMock->shouldReceive('getOrderServiceData')->once()->with($order, null)->andReturn([]);
|
$serviceMock->shouldReceive('getOrderServiceData')->once()->with($order)->andReturn([]);
|
||||||
$serviceMock->shouldReceive('toApiArray')->once()->with($order, true, null)->andReturn($orderArr);
|
$serviceMock->shouldReceive('toApiArray')->once()->with($order, true)->andReturn($orderArr);
|
||||||
|
|
||||||
$di = container();
|
$di = container();
|
||||||
$di['em']->getRepository(Order::class)->shouldReceive('find')->once()->with(1)->andReturn($order);
|
$di['em']->getRepository(Order::class)->shouldReceive('find')->once()->with(1)->andReturn($order);
|
||||||
@@ -1371,21 +1371,18 @@ test('getLogger returns logger with event items', function (): void {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$capturedItems = [];
|
$capturedItems = [];
|
||||||
$logger = new class($capturedItems) extends Box_Log {
|
$logger = new class($capturedItems) extends FOSSBilling\Logger {
|
||||||
public function __construct(public array &$capturedItems)
|
public function __construct(public array &$capturedItems)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public function addWriter($writer): static
|
public function withContext(array $context): static
|
||||||
{
|
{
|
||||||
return $this;
|
foreach ($context as $name => $value) {
|
||||||
|
$this->capturedItems[] = [$name, $value];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setEventItem(string $name, mixed $value): static
|
return clone $this;
|
||||||
{
|
|
||||||
$this->capturedItems[] = [$name, $value];
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1397,7 +1394,7 @@ test('getLogger returns logger with event items', function (): void {
|
|||||||
|
|
||||||
$result = $svc->getLogger($model);
|
$result = $svc->getLogger($model);
|
||||||
|
|
||||||
expect($result)->toBeInstanceOf(Box_Log::class);
|
expect($result)->toBeInstanceOf(FOSSBilling\Logger::class);
|
||||||
expect($capturedItems)->toHaveCount(2);
|
expect($capturedItems)->toHaveCount(2);
|
||||||
expect($capturedItems[0])->toEqual(['client_order_id', 5]);
|
expect($capturedItems[0])->toEqual(['client_order_id', 5]);
|
||||||
expect($capturedItems[1])->toEqual(['status', 'active']);
|
expect($capturedItems[1])->toEqual(['status', 'active']);
|
||||||
@@ -1859,7 +1856,7 @@ test('createOrder creates order', function (): void {
|
|||||||
|
|
||||||
$newId = 1;
|
$newId = 1;
|
||||||
|
|
||||||
$periodMock = Mockery::mock(Box_Period::class);
|
$periodMock = Mockery::mock(FOSSBilling\Period::class);
|
||||||
$periodMock->shouldReceive('getCode')->atLeast()->once()->andReturn('1Y');
|
$periodMock->shouldReceive('getCode')->atLeast()->once()->andReturn('1Y');
|
||||||
|
|
||||||
$di = container();
|
$di = container();
|
||||||
@@ -1880,7 +1877,7 @@ test('createOrder creates order', function (): void {
|
|||||||
$di['events_manager'] = $eventMock;
|
$di['events_manager'] = $eventMock;
|
||||||
$di['em'] = $emMock;
|
$di['em'] = $emMock;
|
||||||
$di['period'] = $di->protect(fn (): Mockery\MockInterface => $periodMock);
|
$di['period'] = $di->protect(fn (): Mockery\MockInterface => $periodMock);
|
||||||
$di['logger'] = new Box_Log();
|
$di['logger'] = new FOSSBilling\Logger();
|
||||||
|
|
||||||
$svc = new Service();
|
$svc = new Service();
|
||||||
$svc->setDi($di);
|
$svc->setDi($di);
|
||||||
@@ -1956,7 +1953,7 @@ test('createOrder sets form id from product', function (): void {
|
|||||||
|
|
||||||
$newId = 1;
|
$newId = 1;
|
||||||
|
|
||||||
$periodMock = Mockery::mock(Box_Period::class);
|
$periodMock = Mockery::mock(FOSSBilling\Period::class);
|
||||||
$periodMock->shouldReceive('getCode')->atLeast()->once()->andReturn('1Y');
|
$periodMock->shouldReceive('getCode')->atLeast()->once()->andReturn('1Y');
|
||||||
|
|
||||||
$di = container();
|
$di = container();
|
||||||
@@ -1977,7 +1974,7 @@ test('createOrder sets form id from product', function (): void {
|
|||||||
$di['events_manager'] = $eventMock;
|
$di['events_manager'] = $eventMock;
|
||||||
$di['em'] = $emMock;
|
$di['em'] = $emMock;
|
||||||
$di['period'] = $di->protect(fn (): Mockery\MockInterface => $periodMock);
|
$di['period'] = $di->protect(fn (): Mockery\MockInterface => $periodMock);
|
||||||
$di['logger'] = new Box_Log();
|
$di['logger'] = new FOSSBilling\Logger();
|
||||||
|
|
||||||
$svc = new Service();
|
$svc = new Service();
|
||||||
$svc->setDi($di);
|
$svc->setDi($di);
|
||||||
@@ -2077,7 +2074,7 @@ test('createOrder returns success when invoice follow up fails', function (): vo
|
|||||||
|
|
||||||
$newId = 1;
|
$newId = 1;
|
||||||
|
|
||||||
$periodMock = Mockery::mock(Box_Period::class);
|
$periodMock = Mockery::mock(FOSSBilling\Period::class);
|
||||||
$periodMock->shouldReceive('getCode')->atLeast()->once()->andReturn('1Y');
|
$periodMock->shouldReceive('getCode')->atLeast()->once()->andReturn('1Y');
|
||||||
|
|
||||||
$di = container();
|
$di = container();
|
||||||
@@ -2101,7 +2098,7 @@ test('createOrder returns success when invoice follow up fails', function (): vo
|
|||||||
$di['events_manager'] = $eventMock;
|
$di['events_manager'] = $eventMock;
|
||||||
$di['em'] = $emMock;
|
$di['em'] = $emMock;
|
||||||
$di['period'] = $di->protect(fn (): Mockery\MockInterface => $periodMock);
|
$di['period'] = $di->protect(fn (): Mockery\MockInterface => $periodMock);
|
||||||
$di['logger'] = new Box_Log();
|
$di['logger'] = new FOSSBilling\Logger();
|
||||||
|
|
||||||
$svc = new Service();
|
$svc = new Service();
|
||||||
$svc->setDi($di);
|
$svc->setDi($di);
|
||||||
@@ -2213,7 +2210,7 @@ test('createOrder uses product pricing service for domain orders', function ():
|
|||||||
});
|
});
|
||||||
$di['events_manager'] = $eventMock;
|
$di['events_manager'] = $eventMock;
|
||||||
$di['em'] = $emMock;
|
$di['em'] = $emMock;
|
||||||
$di['logger'] = new Box_Log();
|
$di['logger'] = new FOSSBilling\Logger();
|
||||||
|
|
||||||
$svc = new Service();
|
$svc = new Service();
|
||||||
$svc->setDi($di);
|
$svc->setDi($di);
|
||||||
@@ -2271,7 +2268,7 @@ test('createFromOrder activates the order after successful provisioning', functi
|
|||||||
->andReturn(['username' => 'created']);
|
->andReturn(['username' => 'created']);
|
||||||
$serviceMock->shouldReceive('saveStatusChange')->once()->with($order, 'Order activated');
|
$serviceMock->shouldReceive('saveStatusChange')->once()->with($order, 'Order activated');
|
||||||
|
|
||||||
$periodMock = Mockery::mock(Box_Period::class);
|
$periodMock = Mockery::mock(FOSSBilling\Period::class);
|
||||||
$periodMock->shouldReceive('getExpirationTime')->once()->andReturn(strtotime('2027-01-01 00:00:00'));
|
$periodMock->shouldReceive('getExpirationTime')->once()->andReturn(strtotime('2027-01-01 00:00:00'));
|
||||||
|
|
||||||
// Stock is reserved atomically at order-creation time (see
|
// Stock is reserved atomically at order-creation time (see
|
||||||
@@ -2440,7 +2437,7 @@ test('activateOrder activates pending order', function (): void {
|
|||||||
$di = container();
|
$di = container();
|
||||||
$di['em'] = $emMock;
|
$di['em'] = $emMock;
|
||||||
$di['events_manager'] = $eventMock;
|
$di['events_manager'] = $eventMock;
|
||||||
$di['logger'] = new Box_Log();
|
$di['logger'] = new FOSSBilling\Logger();
|
||||||
|
|
||||||
$serviceMock = Mockery::mock(Service::class)->makePartial();
|
$serviceMock = Mockery::mock(Service::class)->makePartial();
|
||||||
$serviceMock->shouldAllowMockingProtectedMethods();
|
$serviceMock->shouldAllowMockingProtectedMethods();
|
||||||
@@ -2514,7 +2511,7 @@ test('activateOrder force re-activates an already active order', function (): vo
|
|||||||
$di = container();
|
$di = container();
|
||||||
$di['em'] = $emMock;
|
$di['em'] = $emMock;
|
||||||
$di['events_manager'] = $eventMock;
|
$di['events_manager'] = $eventMock;
|
||||||
$di['logger'] = new Box_Log();
|
$di['logger'] = new FOSSBilling\Logger();
|
||||||
|
|
||||||
$serviceMock = Mockery::mock(Service::class)->makePartial();
|
$serviceMock = Mockery::mock(Service::class)->makePartial();
|
||||||
$serviceMock->shouldAllowMockingProtectedMethods();
|
$serviceMock->shouldAllowMockingProtectedMethods();
|
||||||
@@ -2635,7 +2632,7 @@ test('updateOrder updates fields', function (): void {
|
|||||||
$di = container();
|
$di = container();
|
||||||
$di['events_manager'] = $eventMock;
|
$di['events_manager'] = $eventMock;
|
||||||
$di['em'] = Mockery::mock(Doctrine\ORM\EntityManagerInterface::class)->shouldIgnoreMissing();
|
$di['em'] = Mockery::mock(Doctrine\ORM\EntityManagerInterface::class)->shouldIgnoreMissing();
|
||||||
$di['logger'] = new Box_Log();
|
$di['logger'] = new FOSSBilling\Logger();
|
||||||
|
|
||||||
$data = [
|
$data = [
|
||||||
'period' => '1Y',
|
'period' => '1Y',
|
||||||
@@ -2675,7 +2672,7 @@ test('renewOrder renews order', function (): void {
|
|||||||
|
|
||||||
$di = container();
|
$di = container();
|
||||||
$di['events_manager'] = $eventMock;
|
$di['events_manager'] = $eventMock;
|
||||||
$di['logger'] = new Box_Log();
|
$di['logger'] = new FOSSBilling\Logger();
|
||||||
|
|
||||||
$serviceMock = Mockery::mock(Service::class)->makePartial();
|
$serviceMock = Mockery::mock(Service::class)->makePartial();
|
||||||
$serviceMock->shouldAllowMockingProtectedMethods();
|
$serviceMock->shouldAllowMockingProtectedMethods();
|
||||||
@@ -2699,7 +2696,7 @@ test('renewFromOrder extends expiration', function (): void {
|
|||||||
$clientOrderModel->expires_at = '2026-01-01 00:00:00';
|
$clientOrderModel->expires_at = '2026-01-01 00:00:00';
|
||||||
|
|
||||||
$expectedExpiration = strtotime('2027-01-01 00:00:00');
|
$expectedExpiration = strtotime('2027-01-01 00:00:00');
|
||||||
$periodMock = Mockery::mock(Box_Period::class);
|
$periodMock = Mockery::mock(FOSSBilling\Period::class);
|
||||||
$periodMock->shouldReceive('getExpirationTime')
|
$periodMock->shouldReceive('getExpirationTime')
|
||||||
->atLeast()->once()
|
->atLeast()->once()
|
||||||
->with(strtotime('2026-01-01 00:00:00'))
|
->with(strtotime('2026-01-01 00:00:00'))
|
||||||
@@ -2727,7 +2724,7 @@ test('renewFromOrder treats a missing Doctrine expiration as now', function ():
|
|||||||
$serviceMock->shouldReceive('saveStatusChange')->once()->with(Mockery::type(Order::class), 'Order renewed');
|
$serviceMock->shouldReceive('saveStatusChange')->once()->with(Mockery::type(Order::class), 'Order renewed');
|
||||||
|
|
||||||
$order = createEntity(Order::class, ['period' => '1Y']);
|
$order = createEntity(Order::class, ['period' => '1Y']);
|
||||||
$periodMock = Mockery::mock(Box_Period::class);
|
$periodMock = Mockery::mock(FOSSBilling\Period::class);
|
||||||
$periodMock->shouldReceive('getExpirationTime')
|
$periodMock->shouldReceive('getExpirationTime')
|
||||||
->once()
|
->once()
|
||||||
->with(Mockery::on(static fn (int $from): bool => abs(time() - $from) <= 1))
|
->with(Mockery::on(static fn (int $from): bool => abs(time() - $from) <= 1))
|
||||||
@@ -2758,7 +2755,7 @@ test('renewFromOrder extends free first term on first paid renewal', function ()
|
|||||||
->with(Mockery::on(fn ($order): bool => $order === $clientOrderModel), Order::ACTION_RENEW);
|
->with(Mockery::on(fn ($order): bool => $order === $clientOrderModel), Order::ACTION_RENEW);
|
||||||
|
|
||||||
$expectedExpiration = strtotime('2027-01-01 00:00:00');
|
$expectedExpiration = strtotime('2027-01-01 00:00:00');
|
||||||
$periodMock = Mockery::mock(Box_Period::class);
|
$periodMock = Mockery::mock(FOSSBilling\Period::class);
|
||||||
$periodMock->shouldReceive('getExpirationTime')
|
$periodMock->shouldReceive('getExpirationTime')
|
||||||
->once()
|
->once()
|
||||||
->with(strtotime('2026-01-01 00:00:00'))
|
->with(strtotime('2026-01-01 00:00:00'))
|
||||||
@@ -2805,7 +2802,7 @@ test('suspendFromOrder suspends active order', function (): void {
|
|||||||
|
|
||||||
$di = container();
|
$di = container();
|
||||||
$di['events_manager'] = $eventMock;
|
$di['events_manager'] = $eventMock;
|
||||||
$di['logger'] = new Box_Log();
|
$di['logger'] = new FOSSBilling\Logger();
|
||||||
|
|
||||||
$serviceMock = Mockery::mock(Service::class)->makePartial();
|
$serviceMock = Mockery::mock(Service::class)->makePartial();
|
||||||
$serviceMock->shouldAllowMockingProtectedMethods();
|
$serviceMock->shouldAllowMockingProtectedMethods();
|
||||||
@@ -2857,7 +2854,7 @@ test('cancelFromOrder cancels linked subscriptions', function (): void {
|
|||||||
$di = container();
|
$di = container();
|
||||||
$di['em'] = $emMock;
|
$di['em'] = $emMock;
|
||||||
$di['dbal'] = $connectionMock;
|
$di['dbal'] = $connectionMock;
|
||||||
$di['logger'] = new Box_Log();
|
$di['logger'] = new FOSSBilling\Logger();
|
||||||
$di['mod_service'] = $di->protect(function (string $module, string $service = '') use ($productService, $subscriptionService) {
|
$di['mod_service'] = $di->protect(function (string $module, string $service = '') use ($productService, $subscriptionService) {
|
||||||
if ($module === 'Invoice' && $service === 'Subscription') {
|
if ($module === 'Invoice' && $service === 'Subscription') {
|
||||||
return $subscriptionService;
|
return $subscriptionService;
|
||||||
@@ -2892,7 +2889,7 @@ test('scheduleCancellationFromOrder keeps the service active', function (): void
|
|||||||
$subscriptionService->shouldReceive('scheduleCancellationForOrder')->once()->with($order)->andReturn(1);
|
$subscriptionService->shouldReceive('scheduleCancellationForOrder')->once()->with($order)->andReturn(1);
|
||||||
|
|
||||||
$di = container();
|
$di = container();
|
||||||
$di['logger'] = new Box_Log();
|
$di['logger'] = new FOSSBilling\Logger();
|
||||||
$di['mod_service'] = $di->protect(fn () => $subscriptionService);
|
$di['mod_service'] = $di->protect(fn () => $subscriptionService);
|
||||||
|
|
||||||
$service = Mockery::mock(Service::class)->makePartial();
|
$service = Mockery::mock(Service::class)->makePartial();
|
||||||
@@ -3112,7 +3109,7 @@ test('updatePeriod sets period when given', function (): void {
|
|||||||
$period = '1Y';
|
$period = '1Y';
|
||||||
$di = container();
|
$di = container();
|
||||||
|
|
||||||
$periodMock = Mockery::mock(Box_Period::class);
|
$periodMock = Mockery::mock(FOSSBilling\Period::class);
|
||||||
$periodMock->shouldReceive('getCode')->atLeast()->once();
|
$periodMock->shouldReceive('getCode')->atLeast()->once();
|
||||||
$di['period'] = $di->protect(fn (): Mockery\MockInterface => $periodMock);
|
$di['period'] = $di->protect(fn (): Mockery\MockInterface => $periodMock);
|
||||||
|
|
||||||
@@ -3130,7 +3127,7 @@ test('updatePeriod clears period when empty string', function (): void {
|
|||||||
$period = '';
|
$period = '';
|
||||||
$di = container();
|
$di = container();
|
||||||
|
|
||||||
$periodMock = Mockery::mock(Box_Period::class);
|
$periodMock = Mockery::mock(FOSSBilling\Period::class);
|
||||||
$periodMock->shouldReceive('getCode')->never();
|
$periodMock->shouldReceive('getCode')->never();
|
||||||
$di['period'] = $di->protect(fn (): Mockery\MockInterface => $periodMock);
|
$di['period'] = $di->protect(fn (): Mockery\MockInterface => $periodMock);
|
||||||
|
|
||||||
@@ -3148,7 +3145,7 @@ test('updatePeriod does nothing when null', function (): void {
|
|||||||
$period = null;
|
$period = null;
|
||||||
$di = container();
|
$di = container();
|
||||||
|
|
||||||
$periodMock = Mockery::mock(Box_Period::class);
|
$periodMock = Mockery::mock(FOSSBilling\Period::class);
|
||||||
$periodMock->shouldReceive('getCode')->never();
|
$periodMock->shouldReceive('getCode')->never();
|
||||||
$di['period'] = $di->protect(fn (): Mockery\MockInterface => $periodMock);
|
$di['period'] = $di->protect(fn (): Mockery\MockInterface => $periodMock);
|
||||||
|
|
||||||
@@ -3308,7 +3305,7 @@ test('updateOrderMeta clears existing meta', function (): void {
|
|||||||
|
|
||||||
test('updateOrderConfig succeeds when no form id is set', function (): void {
|
test('updateOrderConfig succeeds when no form id is set', function (): void {
|
||||||
$di = container();
|
$di = container();
|
||||||
$di['logger'] = new Box_Log();
|
$di['logger'] = new FOSSBilling\Logger();
|
||||||
|
|
||||||
$svc = new Service();
|
$svc = new Service();
|
||||||
$svc->setDi($di);
|
$svc->setDi($di);
|
||||||
@@ -3475,7 +3472,7 @@ test('updateOrderConfig succeeds with valid form data', function (): void {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
$di['em'] = Mockery::mock(Doctrine\ORM\EntityManagerInterface::class)->shouldIgnoreMissing();
|
$di['em'] = Mockery::mock(Doctrine\ORM\EntityManagerInterface::class)->shouldIgnoreMissing();
|
||||||
$di['logger'] = new Box_Log();
|
$di['logger'] = new FOSSBilling\Logger();
|
||||||
|
|
||||||
$svc = new Service();
|
$svc = new Service();
|
||||||
$svc->setDi($di);
|
$svc->setDi($di);
|
||||||
@@ -3595,7 +3592,7 @@ test('createOrder generates an invoice for a zero-price order with issue-invoice
|
|||||||
|
|
||||||
$newId = 1;
|
$newId = 1;
|
||||||
|
|
||||||
$periodMock = Mockery::mock(Box_Period::class);
|
$periodMock = Mockery::mock(FOSSBilling\Period::class);
|
||||||
$periodMock->shouldReceive('getCode')->atLeast()->once()->andReturn('1Y');
|
$periodMock->shouldReceive('getCode')->atLeast()->once()->andReturn('1Y');
|
||||||
|
|
||||||
$di = container();
|
$di = container();
|
||||||
@@ -3619,7 +3616,7 @@ test('createOrder generates an invoice for a zero-price order with issue-invoice
|
|||||||
$di['events_manager'] = $eventMock;
|
$di['events_manager'] = $eventMock;
|
||||||
$di['em'] = $emMock;
|
$di['em'] = $emMock;
|
||||||
$di['period'] = $di->protect(fn (): Mockery\MockInterface => $periodMock);
|
$di['period'] = $di->protect(fn (): Mockery\MockInterface => $periodMock);
|
||||||
$di['logger'] = new Box_Log();
|
$di['logger'] = new FOSSBilling\Logger();
|
||||||
|
|
||||||
$svc = new Service();
|
$svc = new Service();
|
||||||
$svc->setDi($di);
|
$svc->setDi($di);
|
||||||
@@ -3708,7 +3705,7 @@ test('createOrder does not roll back when invoice generation fails for a negativ
|
|||||||
|
|
||||||
$newId = 1;
|
$newId = 1;
|
||||||
|
|
||||||
$periodMock = Mockery::mock(Box_Period::class);
|
$periodMock = Mockery::mock(FOSSBilling\Period::class);
|
||||||
$periodMock->shouldReceive('getCode')->atLeast()->once()->andReturn('1Y');
|
$periodMock->shouldReceive('getCode')->atLeast()->once()->andReturn('1Y');
|
||||||
|
|
||||||
$di = container();
|
$di = container();
|
||||||
@@ -3732,7 +3729,7 @@ test('createOrder does not roll back when invoice generation fails for a negativ
|
|||||||
$di['events_manager'] = $eventMock;
|
$di['events_manager'] = $eventMock;
|
||||||
$di['em'] = $emMock;
|
$di['em'] = $emMock;
|
||||||
$di['period'] = $di->protect(fn (): Mockery\MockInterface => $periodMock);
|
$di['period'] = $di->protect(fn (): Mockery\MockInterface => $periodMock);
|
||||||
$di['logger'] = new Box_Log();
|
$di['logger'] = new FOSSBilling\Logger();
|
||||||
|
|
||||||
$svc = new Service();
|
$svc = new Service();
|
||||||
$svc->setDi($di);
|
$svc->setDi($di);
|
||||||
@@ -3807,7 +3804,7 @@ test('batchSendSuspensionWarnings claims and queues each warning once', function
|
|||||||
$di = container();
|
$di = container();
|
||||||
$di['em'] = $em;
|
$di['em'] = $em;
|
||||||
$di['events_manager'] = $events;
|
$di['events_manager'] = $events;
|
||||||
$di['logger'] = new Box_Log();
|
$di['logger'] = new FOSSBilling\Logger();
|
||||||
$di['mod_service'] = $di->protect(fn (string $name): Box\Mod\Email\Service => $emailService);
|
$di['mod_service'] = $di->protect(fn (string $name): Box\Mod\Email\Service => $emailService);
|
||||||
$service->setDi($di);
|
$service->setDi($di);
|
||||||
|
|
||||||
@@ -3856,7 +3853,7 @@ test('batchSendSuspensionWarnings releases a failed claim so the warning can be
|
|||||||
$di = container();
|
$di = container();
|
||||||
$di['em'] = $em;
|
$di['em'] = $em;
|
||||||
$di['events_manager'] = $events;
|
$di['events_manager'] = $events;
|
||||||
$di['logger'] = new Box_Log();
|
$di['logger'] = new FOSSBilling\Logger();
|
||||||
$di['mod_service'] = $di->protect(fn (string $name): Box\Mod\Email\Service => $emailService);
|
$di['mod_service'] = $di->protect(fn (string $name): Box\Mod\Email\Service => $emailService);
|
||||||
$service->setDi($di);
|
$service->setDi($di);
|
||||||
|
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ class Admin extends \FOSSBilling\Api\AbstractApi
|
|||||||
* Update product settings.
|
* Update product settings.
|
||||||
*
|
*
|
||||||
* @optional array $pricing - product pricing configuration. Shape: {type: "free"|"once"|"recurrent", once: {price, setup}, recurrent: {"<PERIOD_CODE>": {price, setup, enabled}, ...}}.
|
* @optional array $pricing - product pricing configuration. Shape: {type: "free"|"once"|"recurrent", once: {price, setup}, recurrent: {"<PERIOD_CODE>": {price, setup, enabled}, ...}}.
|
||||||
* Each recurrent key is a Box_Period code (quantity + unit letter, e.g. "1M", "3Y", "45D"; D 1-90, W 1-52, M 1-24, Y 1-5).
|
* Each recurrent key is a billing period code (quantity + unit letter, e.g. "1M", "3Y", "45D"; D 1-90, W 1-52, M 1-24, Y 1-5).
|
||||||
* Submitting recurrent replaces the product's full set of billing periods - omitted codes are removed, at least one must remain.
|
* Submitting recurrent replaces the product's full set of billing periods - omitted codes are removed, at least one must remain.
|
||||||
* @optional array $config - product configuration options depending on type
|
* @optional array $config - product configuration options depending on type
|
||||||
* @optional array $upgrades - array of upgradable products
|
* @optional array $upgrades - array of upgradable products
|
||||||
@@ -242,7 +242,7 @@ class Admin extends \FOSSBilling\Api\AbstractApi
|
|||||||
* Addon update.
|
* Addon update.
|
||||||
*
|
*
|
||||||
* @optional array $pricing - product pricing configuration. Shape: {type: "free"|"once"|"recurrent", once: {price, setup}, recurrent: {"<PERIOD_CODE>": {price, setup, enabled}, ...}}.
|
* @optional array $pricing - product pricing configuration. Shape: {type: "free"|"once"|"recurrent", once: {price, setup}, recurrent: {"<PERIOD_CODE>": {price, setup, enabled}, ...}}.
|
||||||
* Each recurrent key is a Box_Period code (quantity + unit letter, e.g. "1M", "3Y", "45D"; D 1-90, W 1-52, M 1-24, Y 1-5).
|
* Each recurrent key is a billing period code (quantity + unit letter, e.g. "1M", "3Y", "45D"; D 1-90, W 1-52, M 1-24, Y 1-5).
|
||||||
* Submitting recurrent replaces the product's full set of billing periods - omitted codes are removed, at least one must remain.
|
* Submitting recurrent replaces the product's full set of billing periods - omitted codes are removed, at least one must remain.
|
||||||
* @optional array $config - product configuration options depending on type
|
* @optional array $config - product configuration options depending on type
|
||||||
* @optional array $upgrades - array of upgradable products
|
* @optional array $upgrades - array of upgradable products
|
||||||
@@ -272,7 +272,7 @@ class Admin extends \FOSSBilling\Api\AbstractApi
|
|||||||
if (!$model instanceof Product || !$model->isAddon()) {
|
if (!$model instanceof Product || !$model->isAddon()) {
|
||||||
throw new \FOSSBilling\InformationException('Addon not found');
|
throw new \FOSSBilling\InformationException('Addon not found');
|
||||||
}
|
}
|
||||||
$this->di['logger']->info('Updated addon #%s', $model->getId());
|
$this->di['logger']->info('Updated addon #{model_id}', ['model_id' => $model->getId()]);
|
||||||
|
|
||||||
return $this->update($data);
|
return $this->update($data);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ class ProductPayment
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds a configured billing period by its Box_Period code (e.g. "1M", "3Y", "45D").
|
* Finds a configured billing period by its code (e.g. "1M", "3Y", "45D").
|
||||||
*/
|
*/
|
||||||
public function getPeriod(string $code): ?ProductPaymentPeriod
|
public function getPeriod(string $code): ?ProductPaymentPeriod
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ declare(strict_types=1);
|
|||||||
namespace Box\Mod\Product\Entity;
|
namespace Box\Mod\Product\Entity;
|
||||||
|
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use FOSSBilling\Period;
|
||||||
|
|
||||||
#[ORM\Entity]
|
#[ORM\Entity]
|
||||||
#[ORM\Table(name: 'product_payment_period')]
|
#[ORM\Table(name: 'product_payment_period')]
|
||||||
@@ -121,10 +122,10 @@ class ProductPaymentPeriod
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses this period's code (e.g. "1M", "3Y", "45D") into a Box_Period value object.
|
* Parses this period's code (e.g. "1M", "3Y", "45D") into a billing period value object.
|
||||||
*/
|
*/
|
||||||
public function getPeriod(): \Box_Period
|
public function getPeriod(): Period
|
||||||
{
|
{
|
||||||
return new \Box_Period($this->code);
|
return new Period($this->code);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -31,6 +31,7 @@ use Box\Mod\Staff\Entity\Admin;
|
|||||||
use Doctrine\ORM\QueryBuilder;
|
use Doctrine\ORM\QueryBuilder;
|
||||||
use FOSSBilling\InjectionAwareInterface;
|
use FOSSBilling\InjectionAwareInterface;
|
||||||
use FOSSBilling\PaginationOptions;
|
use FOSSBilling\PaginationOptions;
|
||||||
|
use FOSSBilling\Period;
|
||||||
use FOSSBilling\Validation\NonNegativeIntegerValidator;
|
use FOSSBilling\Validation\NonNegativeIntegerValidator;
|
||||||
|
|
||||||
class Service implements InjectionAwareInterface
|
class Service implements InjectionAwareInterface
|
||||||
@@ -438,7 +439,7 @@ class Service implements InjectionAwareInterface
|
|||||||
$this->di['em']->persist($model);
|
$this->di['em']->persist($model);
|
||||||
$this->di['em']->flush();
|
$this->di['em']->flush();
|
||||||
$productId = $model->getId();
|
$productId = $model->getId();
|
||||||
$this->di['logger']->info('Created new product #%s', $model->getId());
|
$this->di['logger']->info('Created new product #{model_id}', ['model_id' => $model->getId()]);
|
||||||
|
|
||||||
return (int) $productId;
|
return (int) $productId;
|
||||||
}
|
}
|
||||||
@@ -512,7 +513,7 @@ class Service implements InjectionAwareInterface
|
|||||||
|
|
||||||
$this->di['em']->flush();
|
$this->di['em']->flush();
|
||||||
|
|
||||||
$this->di['logger']->info('Updated product #%s configuration', $model->getId());
|
$this->di['logger']->info('Updated product #{model_id} configuration', ['model_id' => $model->getId()]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -562,7 +563,7 @@ class Service implements InjectionAwareInterface
|
|||||||
$model->setUpdatedAt(new \DateTime());
|
$model->setUpdatedAt(new \DateTime());
|
||||||
$this->di['em']->flush();
|
$this->di['em']->flush();
|
||||||
|
|
||||||
$this->di['logger']->info('Updated product #%s configuration', $model->getId());
|
$this->di['logger']->info('Updated product #{model_id} configuration', ['model_id' => $model->getId()]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -598,7 +599,7 @@ class Service implements InjectionAwareInterface
|
|||||||
$this->di['em']->flush();
|
$this->di['em']->flush();
|
||||||
$productId = $model->getId();
|
$productId = $model->getId();
|
||||||
|
|
||||||
$this->di['logger']->info('Created new addon #%s', $productId);
|
$this->di['logger']->info('Created new addon #{product_id}', ['product_id' => $productId]);
|
||||||
|
|
||||||
return $productId;
|
return $productId;
|
||||||
}
|
}
|
||||||
@@ -612,7 +613,7 @@ class Service implements InjectionAwareInterface
|
|||||||
$id = $product->getId();
|
$id = $product->getId();
|
||||||
$this->di['em']->remove($product);
|
$this->di['em']->remove($product);
|
||||||
$this->di['em']->flush();
|
$this->di['em']->flush();
|
||||||
$this->di['logger']->info('Deleted product #%s', $id);
|
$this->di['logger']->info('Deleted product #{id}', ['id' => $id]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -642,7 +643,7 @@ class Service implements InjectionAwareInterface
|
|||||||
->setDescription($description);
|
->setDescription($description);
|
||||||
$this->di['em']->flush();
|
$this->di['em']->flush();
|
||||||
|
|
||||||
$this->di['logger']->info('Updated product category #%s', $productCategory->getId());
|
$this->di['logger']->info('Updated product category #{category_id}', ['category_id' => $productCategory->getId()]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -657,7 +658,7 @@ class Service implements InjectionAwareInterface
|
|||||||
$this->di['em']->flush();
|
$this->di['em']->flush();
|
||||||
$id = $model->getId();
|
$id = $model->getId();
|
||||||
|
|
||||||
$this->di['logger']->info('Created new product category #%s', $id);
|
$this->di['logger']->info('Created new product category #{id}', ['id' => $id]);
|
||||||
|
|
||||||
return $id;
|
return $id;
|
||||||
}
|
}
|
||||||
@@ -671,7 +672,7 @@ class Service implements InjectionAwareInterface
|
|||||||
$this->di['em']->remove($category);
|
$this->di['em']->remove($category);
|
||||||
$this->di['em']->flush();
|
$this->di['em']->flush();
|
||||||
|
|
||||||
$this->di['logger']->info('Deleted product category #%s', $id);
|
$this->di['logger']->info('Deleted product category #{id}', ['id' => $id]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -914,7 +915,7 @@ class Service implements InjectionAwareInterface
|
|||||||
|
|
||||||
$addon = $this->getAddonById($id);
|
$addon = $this->getAddonById($id);
|
||||||
if (!$addon instanceof Product) {
|
if (!$addon instanceof Product) {
|
||||||
$this->di['logger']->warning('Addon not found by id %s', $id);
|
$this->di['logger']->warning('Addon not found by id {id}', ['id' => $id]);
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1044,7 +1045,7 @@ class Service implements InjectionAwareInterface
|
|||||||
$this->di['em']->refresh($resolvedProduct);
|
$this->di['em']->refresh($resolvedProduct);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->di['logger']->info('Released stock reservation for order #%s (%s)', $orderId, $reason);
|
$this->di['logger']->info('Released stock reservation for order #{order_id} ({reason})', ['order_id' => $orderId, 'reason' => $reason]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1252,7 +1253,7 @@ class Service implements InjectionAwareInterface
|
|||||||
$this->di['em']->flush();
|
$this->di['em']->flush();
|
||||||
$promoId = (int) $promo->getId();
|
$promoId = (int) $promo->getId();
|
||||||
|
|
||||||
$this->di['logger']->info('Created new promotion code %s', $promo->getCode());
|
$this->di['logger']->info('Created new promotion code {promo_code}', ['promo_code' => $promo->getCode()]);
|
||||||
|
|
||||||
return $promoId;
|
return $promoId;
|
||||||
}
|
}
|
||||||
@@ -1281,7 +1282,7 @@ class Service implements InjectionAwareInterface
|
|||||||
$this->di['em']->flush();
|
$this->di['em']->flush();
|
||||||
$promoId = (int) $promo->getId();
|
$promoId = (int) $promo->getId();
|
||||||
|
|
||||||
$this->di['logger']->info('Duplicated promotion code %s into new promotion code %s', $model->getCode(), $promo->getCode());
|
$this->di['logger']->info('Duplicated promotion code {model_code} into new promotion code {promo_code}', ['model_code' => $model->getCode(), 'promo_code' => $promo->getCode()]);
|
||||||
|
|
||||||
return $promoId;
|
return $promoId;
|
||||||
}
|
}
|
||||||
@@ -1443,6 +1444,54 @@ class Service implements InjectionAwareInterface
|
|||||||
$this->di['em']->flush();
|
$this->di['em']->flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Release promo reservations left behind by a failed checkout.
|
||||||
|
*
|
||||||
|
* A normal checkout runs through the shared Doctrine transaction, so a
|
||||||
|
* rollback removes these rows before this method finds them. The cleanup
|
||||||
|
* remains useful when an older integration persists a reservation outside
|
||||||
|
* that transaction. Only active checkout reservations are eligible; if none
|
||||||
|
* remain, there is no safe usage counter adjustment to make.
|
||||||
|
*
|
||||||
|
* @param list<int> $orderIds order IDs from the failed checkout
|
||||||
|
* @param int $reservedCount number of successful reservations
|
||||||
|
*/
|
||||||
|
public function compensateCheckoutPromoFailure(Promo $promo, array $orderIds, int $reservedCount): void
|
||||||
|
{
|
||||||
|
if ($reservedCount <= 0 || $orderIds === []) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$promoId = (int) ($this->getPromoSourceArray($promo)['id'] ?? 0);
|
||||||
|
if ($promoId <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$redemptionRepository = $this->getPromoRedemptionRepository();
|
||||||
|
$promoRepository = $this->getPromoRepository();
|
||||||
|
$this->di['em']->wrapInTransaction(function () use ($redemptionRepository, $promoRepository, $promo, $orderIds, $promoId): void {
|
||||||
|
$redemptions = $redemptionRepository->findBy([
|
||||||
|
'promo' => $promo,
|
||||||
|
'clientOrderId' => $orderIds,
|
||||||
|
'phase' => PromoRedemption::PHASE_CHECKOUT,
|
||||||
|
'status' => PromoRedemption::STATUS_RESERVED,
|
||||||
|
]);
|
||||||
|
if ($redemptions === []) {
|
||||||
|
// The checkout transaction may already have rolled back these
|
||||||
|
// rows. Do not decrement from the caller's count when there is
|
||||||
|
// nothing left to remove.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($redemptions as $redemption) {
|
||||||
|
$this->di['em']->remove($redemption);
|
||||||
|
}
|
||||||
|
$this->di['em']->flush();
|
||||||
|
|
||||||
|
$promoRepository->decrementUsage($promoId, count($redemptions), new \DateTimeImmutable());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array{
|
* @return array{
|
||||||
* promo: Promo,
|
* promo: Promo,
|
||||||
@@ -1659,7 +1708,7 @@ class Service implements InjectionAwareInterface
|
|||||||
$this->applyPromoDataToEntity($promo, $data);
|
$this->applyPromoDataToEntity($promo, $data);
|
||||||
$this->di['em']->flush();
|
$this->di['em']->flush();
|
||||||
|
|
||||||
$this->di['logger']->info('Update promo code %s', $promo->getCode());
|
$this->di['logger']->info('Update promo code {promo_code}', ['promo_code' => $promo->getCode()]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1675,7 +1724,7 @@ class Service implements InjectionAwareInterface
|
|||||||
$this->di['em']->remove($promo);
|
$this->di['em']->remove($promo);
|
||||||
$this->di['em']->flush();
|
$this->di['em']->flush();
|
||||||
|
|
||||||
$this->di['logger']->info('Removed promo code %s', $promo->getCode());
|
$this->di['logger']->info('Removed promo code {promo_code}', ['promo_code' => $promo->getCode()]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1782,7 +1831,7 @@ class Service implements InjectionAwareInterface
|
|||||||
{
|
{
|
||||||
// Validate the code shape/range up front so a malformed period gives a clear error.
|
// Validate the code shape/range up front so a malformed period gives a clear error.
|
||||||
try {
|
try {
|
||||||
$code = (new \Box_Period($code))->getCode();
|
$code = (new Period($code))->getCode();
|
||||||
} catch (\FOSSBilling\Exception) {
|
} catch (\FOSSBilling\Exception) {
|
||||||
throw new \FOSSBilling\InformationException('Selected billing period is not available for this product');
|
throw new \FOSSBilling\InformationException('Selected billing period is not available for this product');
|
||||||
}
|
}
|
||||||
@@ -1876,7 +1925,7 @@ class Service implements InjectionAwareInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$code = (new \Box_Period((string) $rawCode))->getCode();
|
$code = (new Period((string) $rawCode))->getCode();
|
||||||
} catch (\FOSSBilling\Exception) {
|
} catch (\FOSSBilling\Exception) {
|
||||||
throw new \FOSSBilling\InformationException('Invalid billing period :period', [':period' => (string) $rawCode]);
|
throw new \FOSSBilling\InformationException('Invalid billing period :period', [':period' => (string) $rawCode]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -212,7 +212,7 @@ test('updates an addon', function (): void {
|
|||||||
$serviceMock->shouldReceive('findProductById')->once()->with(1)->andReturn($model);
|
$serviceMock->shouldReceive('findProductById')->once()->with(1)->andReturn($model);
|
||||||
|
|
||||||
$di = container();
|
$di = container();
|
||||||
$di['logger'] = new Box_Log();
|
$di['logger'] = new FOSSBilling\Logger();
|
||||||
|
|
||||||
$apiMock->setService($serviceMock);
|
$apiMock->setService($serviceMock);
|
||||||
$apiMock->setDi($di);
|
$apiMock->setDi($di);
|
||||||
|
|||||||
@@ -572,7 +572,7 @@ test('releaseReservedStockForOrder restores stock and clears the reservation', f
|
|||||||
'Order' => $orderServiceMock,
|
'Order' => $orderServiceMock,
|
||||||
default => throw new RuntimeException("Unexpected module service {$module}"),
|
default => throw new RuntimeException("Unexpected module service {$module}"),
|
||||||
});
|
});
|
||||||
$di['logger'] = new Box_Log();
|
$di['logger'] = new FOSSBilling\Logger();
|
||||||
$service->setDi($di);
|
$service->setDi($di);
|
||||||
|
|
||||||
$service->releaseReservedStockForOrder($order, 'order_canceled');
|
$service->releaseReservedStockForOrder($order, 'order_canceled');
|
||||||
@@ -750,7 +750,7 @@ test('get related product discount uses domain pricing implementation', function
|
|||||||
$tldService = productTestCreateDomainTldServiceMock($tld);
|
$tldService = productTestCreateDomainTldServiceMock($tld);
|
||||||
|
|
||||||
$di = container();
|
$di = container();
|
||||||
$di['period'] = $di->protect(fn (string $period): Box_Period => new Box_Period($period));
|
$di['period'] = $di->protect(fn (string $period): FOSSBilling\Period => new FOSSBilling\Period($period));
|
||||||
$di['mod_service'] = $di->protect(function (string $serviceName, ?string $sub = null) use ($tldService) {
|
$di['mod_service'] = $di->protect(function (string $serviceName, ?string $sub = null) use ($tldService) {
|
||||||
if ($serviceName === 'servicedomain' && $sub === 'Tld') {
|
if ($serviceName === 'servicedomain' && $sub === 'Tld') {
|
||||||
return $tldService;
|
return $tldService;
|
||||||
@@ -837,7 +837,7 @@ test('get product order line config uses domain pricing implementation', functio
|
|||||||
$tldService = productTestCreateDomainTldServiceMock($tld);
|
$tldService = productTestCreateDomainTldServiceMock($tld);
|
||||||
|
|
||||||
$di = container();
|
$di = container();
|
||||||
$di['period'] = $di->protect(fn (string $period): Box_Period => new Box_Period($period));
|
$di['period'] = $di->protect(fn (string $period): FOSSBilling\Period => new FOSSBilling\Period($period));
|
||||||
$di['mod_service'] = $di->protect(function (string $serviceName, ?string $sub = null) use ($tldService) {
|
$di['mod_service'] = $di->protect(function (string $serviceName, ?string $sub = null) use ($tldService) {
|
||||||
if ($serviceName === 'servicedomain' && $sub === 'Tld') {
|
if ($serviceName === 'servicedomain' && $sub === 'Tld') {
|
||||||
return $tldService;
|
return $tldService;
|
||||||
@@ -871,7 +871,7 @@ test('get product renewal line config uses domain pricing implementation', funct
|
|||||||
$tldService = productTestCreateDomainTldServiceMock($tld);
|
$tldService = productTestCreateDomainTldServiceMock($tld);
|
||||||
|
|
||||||
$di = container();
|
$di = container();
|
||||||
$di['period'] = $di->protect(fn (string $period): Box_Period => new Box_Period($period));
|
$di['period'] = $di->protect(fn (string $period): FOSSBilling\Period => new FOSSBilling\Period($period));
|
||||||
$di['mod_service'] = $di->protect(function (string $serviceName, ?string $sub = null) use ($tldService) {
|
$di['mod_service'] = $di->protect(function (string $serviceName, ?string $sub = null) use ($tldService) {
|
||||||
if ($serviceName === 'servicedomain' && $sub === 'Tld') {
|
if ($serviceName === 'servicedomain' && $sub === 'Tld') {
|
||||||
return $tldService;
|
return $tldService;
|
||||||
@@ -937,7 +937,7 @@ test('create product', function (): void {
|
|||||||
$di = container();
|
$di = container();
|
||||||
$di['em'] = productTestCreateEntityManagerWithRepositories($productRepo, null, productTestCreateProductEntity($newProductId), productTestCreateProductPaymentEntity(1));
|
$di['em'] = productTestCreateEntityManagerWithRepositories($productRepo, null, productTestCreateProductEntity($newProductId), productTestCreateProductPaymentEntity(1));
|
||||||
$di['tools'] = $toolMock;
|
$di['tools'] = $toolMock;
|
||||||
$di['logger'] = new Box_Log();
|
$di['logger'] = new FOSSBilling\Logger();
|
||||||
|
|
||||||
$service->setDi($di);
|
$service->setDi($di);
|
||||||
$result = $service->createProduct('title', 'domain');
|
$result = $service->createProduct('title', 'domain');
|
||||||
@@ -1014,7 +1014,7 @@ test('update product', function (): void {
|
|||||||
|
|
||||||
$di = container();
|
$di = container();
|
||||||
$di['em'] = productTestCreateEntityManagerWithRepositories(null, null, null, null, $categoryRepo);
|
$di['em'] = productTestCreateEntityManagerWithRepositories(null, null, null, null, $categoryRepo);
|
||||||
$di['logger'] = new Box_Log();
|
$di['logger'] = new FOSSBilling\Logger();
|
||||||
|
|
||||||
$serviceMock->setDi($di);
|
$serviceMock->setDi($di);
|
||||||
|
|
||||||
@@ -1051,7 +1051,7 @@ test('update priority', function (): void {
|
|||||||
|
|
||||||
$di = container();
|
$di = container();
|
||||||
$di['em'] = productTestCreateEntityManagerWithRepositories($productRepo);
|
$di['em'] = productTestCreateEntityManagerWithRepositories($productRepo);
|
||||||
$di['logger'] = new Box_Log();
|
$di['logger'] = new FOSSBilling\Logger();
|
||||||
|
|
||||||
$service->setDi($di);
|
$service->setDi($di);
|
||||||
|
|
||||||
@@ -1074,7 +1074,7 @@ test('update config', function (): void {
|
|||||||
|
|
||||||
$di = container();
|
$di = container();
|
||||||
$di['em'] = productTestCreateEntityManagerWithRepositories();
|
$di['em'] = productTestCreateEntityManagerWithRepositories();
|
||||||
$di['logger'] = new Box_Log();
|
$di['logger'] = new FOSSBilling\Logger();
|
||||||
|
|
||||||
$service->setDi($di);
|
$service->setDi($di);
|
||||||
|
|
||||||
@@ -1093,7 +1093,7 @@ test('get addons', function (): void {
|
|||||||
|
|
||||||
$di = container();
|
$di = container();
|
||||||
$di['em'] = productTestCreateEntityManagerWithRepositories($productRepo);
|
$di['em'] = productTestCreateEntityManagerWithRepositories($productRepo);
|
||||||
$di['logger'] = new Box_Log();
|
$di['logger'] = new FOSSBilling\Logger();
|
||||||
|
|
||||||
$service->setDi($di);
|
$service->setDi($di);
|
||||||
|
|
||||||
@@ -1114,7 +1114,7 @@ test('create addon', function (): void {
|
|||||||
|
|
||||||
$di = container();
|
$di = container();
|
||||||
$di['em'] = productTestCreateEntityManagerWithRepositories($productRepo, null, productTestCreateProductEntity($newProductId), productTestCreateProductPaymentEntity(1));
|
$di['em'] = productTestCreateEntityManagerWithRepositories($productRepo, null, productTestCreateProductEntity($newProductId), productTestCreateProductPaymentEntity(1));
|
||||||
$di['logger'] = new Box_Log();
|
$di['logger'] = new FOSSBilling\Logger();
|
||||||
$di['tools'] = $toolMock;
|
$di['tools'] = $toolMock;
|
||||||
|
|
||||||
$service->setDi($di);
|
$service->setDi($di);
|
||||||
@@ -1164,7 +1164,7 @@ test('update category', function (): void {
|
|||||||
|
|
||||||
$di = container();
|
$di = container();
|
||||||
$di['em'] = productTestCreateEntityManagerWithRepositories();
|
$di['em'] = productTestCreateEntityManagerWithRepositories();
|
||||||
$di['logger'] = new Box_Log();
|
$di['logger'] = new FOSSBilling\Logger();
|
||||||
|
|
||||||
$service->setDi($di);
|
$service->setDi($di);
|
||||||
|
|
||||||
@@ -1179,7 +1179,7 @@ test('create category', function (): void {
|
|||||||
|
|
||||||
$di = container();
|
$di = container();
|
||||||
$di['em'] = productTestCreateEntityManagerWithRepositories(null, null, null, null, null, productTestCreateProductCategoryEntity($newCategoryId));
|
$di['em'] = productTestCreateEntityManagerWithRepositories(null, null, null, null, null, productTestCreateProductCategoryEntity($newCategoryId));
|
||||||
$di['logger'] = new Box_Log();
|
$di['logger'] = new FOSSBilling\Logger();
|
||||||
|
|
||||||
$service->setDi($di);
|
$service->setDi($di);
|
||||||
|
|
||||||
@@ -1212,7 +1212,7 @@ test('remove product category', function (): void {
|
|||||||
|
|
||||||
$di = container();
|
$di = container();
|
||||||
$di['em'] = productTestCreateEntityManagerWithRepositories($productRepository);
|
$di['em'] = productTestCreateEntityManagerWithRepositories($productRepository);
|
||||||
$di['logger'] = new Box_Log();
|
$di['logger'] = new FOSSBilling\Logger();
|
||||||
|
|
||||||
$service->setDi($di);
|
$service->setDi($di);
|
||||||
|
|
||||||
@@ -1249,7 +1249,7 @@ test('create promo', function (): void {
|
|||||||
};
|
};
|
||||||
|
|
||||||
$di = container();
|
$di = container();
|
||||||
$di['logger'] = new Box_Log();
|
$di['logger'] = new FOSSBilling\Logger();
|
||||||
$di['em'] = $emMock;
|
$di['em'] = $emMock;
|
||||||
|
|
||||||
$service->setDi($di);
|
$service->setDi($di);
|
||||||
@@ -1306,7 +1306,7 @@ test('duplicate promo', function (): void {
|
|||||||
};
|
};
|
||||||
|
|
||||||
$di = container();
|
$di = container();
|
||||||
$di['logger'] = new Box_Log();
|
$di['logger'] = new FOSSBilling\Logger();
|
||||||
$di['em'] = $emMock;
|
$di['em'] = $emMock;
|
||||||
|
|
||||||
$service->setDi($di);
|
$service->setDi($di);
|
||||||
@@ -1363,7 +1363,7 @@ test('duplicate promo generates alternate code when copy code already exists', f
|
|||||||
};
|
};
|
||||||
|
|
||||||
$di = container();
|
$di = container();
|
||||||
$di['logger'] = new Box_Log();
|
$di['logger'] = new FOSSBilling\Logger();
|
||||||
$di['em'] = $emMock;
|
$di['em'] = $emMock;
|
||||||
|
|
||||||
$service->setDi($di);
|
$service->setDi($di);
|
||||||
@@ -1922,6 +1922,124 @@ test('release reserved promo redemptions for invoice releases reservations and d
|
|||||||
expect($di['em']->flushCalls)->toBe(1);
|
expect($di['em']->flushCalls)->toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('failed checkout compensation removes only active checkout reservations atomically', function (): void {
|
||||||
|
$service = new Service();
|
||||||
|
$promo = productTestCreatePromoEntity(7);
|
||||||
|
$activeRedemption = (new PromoRedemption())
|
||||||
|
->setPromo($promo)
|
||||||
|
->setPhase(PromoRedemption::PHASE_CHECKOUT)
|
||||||
|
->setStatus(PromoRedemption::STATUS_RESERVED);
|
||||||
|
|
||||||
|
$redemptionRepository = Mockery::mock(PromoRedemptionRepository::class);
|
||||||
|
$redemptionRepository->shouldReceive('findBy')->once()->with([
|
||||||
|
'promo' => $promo,
|
||||||
|
'clientOrderId' => [11, 12],
|
||||||
|
'phase' => PromoRedemption::PHASE_CHECKOUT,
|
||||||
|
'status' => PromoRedemption::STATUS_RESERVED,
|
||||||
|
])->andReturn([$activeRedemption]);
|
||||||
|
|
||||||
|
$promoRepository = Mockery::mock(PromoRepository::class);
|
||||||
|
$promoRepository->shouldReceive('decrementUsage')
|
||||||
|
->once()
|
||||||
|
->with(7, 1, Mockery::type(DateTimeInterface::class));
|
||||||
|
|
||||||
|
$entityManager = new class($promoRepository, $redemptionRepository) {
|
||||||
|
public array $removed = [];
|
||||||
|
public int $flushCalls = 0;
|
||||||
|
public int $transactionCalls = 0;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly object $promoRepository,
|
||||||
|
private readonly object $redemptionRepository,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRepository(string $class): object
|
||||||
|
{
|
||||||
|
return $class === Promo::class ? $this->promoRepository : $this->redemptionRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function remove(object $entity): void
|
||||||
|
{
|
||||||
|
$this->removed[] = $entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function flush(): void
|
||||||
|
{
|
||||||
|
++$this->flushCalls;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function wrapInTransaction(callable $callback): mixed
|
||||||
|
{
|
||||||
|
++$this->transactionCalls;
|
||||||
|
|
||||||
|
return $callback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$di = container();
|
||||||
|
$di['em'] = $entityManager;
|
||||||
|
$service->setDi($di);
|
||||||
|
|
||||||
|
$service->compensateCheckoutPromoFailure($promo, [11, 12], 1);
|
||||||
|
|
||||||
|
expect($entityManager->removed)->toBe([$activeRedemption])
|
||||||
|
->and($entityManager->flushCalls)->toBe(1)
|
||||||
|
->and($entityManager->transactionCalls)->toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('failed checkout compensation does nothing when no active reservation remains', function (): void {
|
||||||
|
$service = new Service();
|
||||||
|
$promo = productTestCreatePromoEntity(7);
|
||||||
|
$redemptionRepository = Mockery::mock(PromoRedemptionRepository::class);
|
||||||
|
$redemptionRepository->shouldReceive('findBy')->once()->with([
|
||||||
|
'promo' => $promo,
|
||||||
|
'clientOrderId' => [11],
|
||||||
|
'phase' => PromoRedemption::PHASE_CHECKOUT,
|
||||||
|
'status' => PromoRedemption::STATUS_RESERVED,
|
||||||
|
])->andReturn([]);
|
||||||
|
|
||||||
|
$promoRepository = Mockery::mock(PromoRepository::class);
|
||||||
|
$promoRepository->shouldNotReceive('decrementUsage');
|
||||||
|
|
||||||
|
$entityManager = new class($promoRepository, $redemptionRepository) {
|
||||||
|
public int $removeCalls = 0;
|
||||||
|
public int $transactionCalls = 0;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly object $promoRepository,
|
||||||
|
private readonly object $redemptionRepository,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRepository(string $class): object
|
||||||
|
{
|
||||||
|
return $class === Promo::class ? $this->promoRepository : $this->redemptionRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function remove(object $entity): void
|
||||||
|
{
|
||||||
|
++$this->removeCalls;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function wrapInTransaction(callable $callback): mixed
|
||||||
|
{
|
||||||
|
++$this->transactionCalls;
|
||||||
|
|
||||||
|
return $callback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$di = container();
|
||||||
|
$di['em'] = $entityManager;
|
||||||
|
$service->setDi($di);
|
||||||
|
|
||||||
|
$service->compensateCheckoutPromoFailure($promo, [11], 1);
|
||||||
|
|
||||||
|
expect($entityManager->removeCalls)->toBe(0)
|
||||||
|
->and($entityManager->transactionCalls)->toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
test('update promo', function (): void {
|
test('update promo', function (): void {
|
||||||
$service = new Service();
|
$service = new Service();
|
||||||
$data = [
|
$data = [
|
||||||
@@ -1964,7 +2082,7 @@ test('update promo', function (): void {
|
|||||||
};
|
};
|
||||||
|
|
||||||
$di = container();
|
$di = container();
|
||||||
$di['logger'] = new Box_Log();
|
$di['logger'] = new FOSSBilling\Logger();
|
||||||
$di['em'] = $emMock;
|
$di['em'] = $emMock;
|
||||||
|
|
||||||
$service->setDi($di);
|
$service->setDi($di);
|
||||||
@@ -2005,7 +2123,7 @@ test('delete promo', function (): void {
|
|||||||
};
|
};
|
||||||
|
|
||||||
$di = container();
|
$di = container();
|
||||||
$di['logger'] = new Box_Log();
|
$di['logger'] = new FOSSBilling\Logger();
|
||||||
$di['em'] = $emMock;
|
$di['em'] = $emMock;
|
||||||
|
|
||||||
$service->setDi($di);
|
$service->setDi($di);
|
||||||
@@ -2452,7 +2570,7 @@ test('update product accepts a custom recurring period and drops periods no long
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$di = container();
|
$di = container();
|
||||||
$di['logger'] = new Box_Log();
|
$di['logger'] = new FOSSBilling\Logger();
|
||||||
$serviceMock->setDi($di);
|
$serviceMock->setDi($di);
|
||||||
|
|
||||||
$data = [
|
$data = [
|
||||||
@@ -2486,7 +2604,7 @@ test('update product rejects an invalid custom period code', function (): void {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$di = container();
|
$di = container();
|
||||||
$di['logger'] = new Box_Log();
|
$di['logger'] = new FOSSBilling\Logger();
|
||||||
$serviceMock->setDi($di);
|
$serviceMock->setDi($di);
|
||||||
|
|
||||||
$data = [
|
$data = [
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ class Admin extends \FOSSBilling\Api\AbstractApi
|
|||||||
(string) $data['target']
|
(string) $data['target']
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->getDi()['logger']->info('Created new redirect #%s', $id);
|
$this->getDi()['logger']->info('Created new redirect #{id}', ['id' => $id]);
|
||||||
|
|
||||||
return $id;
|
return $id;
|
||||||
}
|
}
|
||||||
@@ -76,7 +76,7 @@ class Admin extends \FOSSBilling\Api\AbstractApi
|
|||||||
|
|
||||||
$this->getService()->update($this->getService()->get((int) $data['id']), $data);
|
$this->getService()->update($this->getService()->get((int) $data['id']), $data);
|
||||||
|
|
||||||
$this->getDi()['logger']->info('Updated redirect #%s', $data['id']);
|
$this->getDi()['logger']->info('Updated redirect #{data_id}', ['data_id' => $data['id']]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -93,7 +93,7 @@ class Admin extends \FOSSBilling\Api\AbstractApi
|
|||||||
|
|
||||||
$this->getService()->delete($this->getService()->get((int) $data['id']));
|
$this->getService()->delete($this->getService()->get((int) $data['id']));
|
||||||
|
|
||||||
$this->getDi()['logger']->info('Removed redirect #%s', $data['id']);
|
$this->getDi()['logger']->info('Removed redirect #{data_id}', ['data_id' => $data['id']]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ test('create delegates to service', function (): void {
|
|||||||
$staffService->shouldReceive('checkPermissionsAndThrowException')->with('redirect', 'create_and_edit', Mockery::any(), Mockery::any())->once();
|
$staffService->shouldReceive('checkPermissionsAndThrowException')->with('redirect', 'create_and_edit', Mockery::any(), Mockery::any())->once();
|
||||||
|
|
||||||
$di = new Pimple\Container();
|
$di = new Pimple\Container();
|
||||||
$di['logger'] = new Box_Log();
|
$di['logger'] = new FOSSBilling\Logger();
|
||||||
$di['mod_service'] = $di->protect(fn (string $name): object => match (strtolower($name)) {
|
$di['mod_service'] = $di->protect(fn (string $name): object => match (strtolower($name)) {
|
||||||
'staff' => $staffService,
|
'staff' => $staffService,
|
||||||
default => throw new RuntimeException("Unexpected mod service: $name"),
|
default => throw new RuntimeException("Unexpected mod service: $name"),
|
||||||
@@ -55,7 +55,7 @@ test('delete delegates to service entity', function (): void {
|
|||||||
$staffService->shouldReceive('checkPermissionsAndThrowException')->with('redirect', 'delete', Mockery::any(), Mockery::any())->once();
|
$staffService->shouldReceive('checkPermissionsAndThrowException')->with('redirect', 'delete', Mockery::any(), Mockery::any())->once();
|
||||||
|
|
||||||
$di = new Pimple\Container();
|
$di = new Pimple\Container();
|
||||||
$di['logger'] = new Box_Log();
|
$di['logger'] = new FOSSBilling\Logger();
|
||||||
$di['mod_service'] = $di->protect(fn (string $name): object => match (strtolower($name)) {
|
$di['mod_service'] = $di->protect(fn (string $name): object => match (strtolower($name)) {
|
||||||
'staff' => $staffService,
|
'staff' => $staffService,
|
||||||
default => throw new RuntimeException("Unexpected mod service: $name"),
|
default => throw new RuntimeException("Unexpected mod service: $name"),
|
||||||
|
|||||||
Loaded 100 of 148 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user