Automatic push 4.5.0

This commit is contained in:
chevereto
2026-04-08 17:03:15 +00:00
parent e0f06f36eb
commit 84d9cf6187
40 changed files with 4154 additions and 765 deletions

2
.github/test.yml vendored
View File

@@ -13,7 +13,7 @@ jobs:
strategy:
matrix:
operating-system: [ubuntu-22.04]
php-versions: ["8.0", "8.1"]
php-versions: ["8.2"]
env:
extensions: pcov, imagick
tools: composer

View File

@@ -14,7 +14,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-22.04]
php: ["8.1"]
php: ["8.2"]
env:
tools: composer
ini-values: default_charset='UTF-8'

View File

@@ -11,7 +11,7 @@ jobs:
strategy:
matrix:
operating-system: [ubuntu-latest]
php-versions: ["8.1"]
php-versions: ["8.2"]
env:
tools: composer
ini-values: default_charset='UTF-8'

View File

@@ -1,7 +0,0 @@
Chevereto 4.4.2 (2026-01-06)
- Fixed bug with "Upgrade now" button display
- Fixed bug in /settings/api route display
- Fixed bug in delete URL not working
- Fixed bug in Free edition missing Follow class
- Fixed API documentation links

28
.package/4.5.0.txt Normal file
View File

@@ -0,0 +1,28 @@
Chevereto 4.5.0 (2026-04-08)
- Added /_/api/4/auth/verify route
- Added /_/api/4/config/traefik internal HTTP provider route
- Added /_/api/4/tenants/{id}/user-password-reset route
- Added CHEVERETO_ENABLE_GUESTS env for controlling guest interactions
- Added CHEVERETO_SERVICE_NAME env for specifying the service name
- Added CHEVERETO_TRIAL_ENABLE_* keys support for controlling features enabled during trial
- Added CHEVERETO_TRIAL_MAX_* keys support for controlling max limits during trial
- Added CHEVERETO_TRIAL env for controlling trial mode
- Added envTrialAware helper function for accessing trial-aware env variables
- Added version-installed command
- Added login_providers tenant stats
- Added password parameter for password-reset command
- Added port 8080 to the list of allowed ports
- Added support for more email providers: AhaSend, Amazon SES, Azure, Brevo, Infobip, MailerSend, Mailgun, Mailjet, Mailomat, MailPace, Mailtrap, Mandrill, Microsoft Graph, Postal, Postmark, Resend, Scaleway, SendGrid and Sweego
- Bumped minimum PHP version to 8.2
- Fixed "Powered by" message
- Fixed bug affecting homepage (free edition)
- Fixed bug on /_/api/4/* routes missing error responses
- Fixed bug on Tenants jobs:worker command when passing tenant id
- Fixed bug on Tenants caching system
- Fixed bug on tenants CLI database-migrate command
- Fixed bug preventing Tenant installation
- Fixed missing custom semantics parsing for image route description
- Improved "Something went wrong" error page for both SaaS and self-hosted contexts
- Improved album dropdown options on uploader
- Renamed env variable CHEVERETO_JOBS_WORKER_INTERVAL to CHEVERETO_SCHEDULER_INTERVAL

View File

@@ -246,6 +246,7 @@ if($missingOptions ?? false) {
$envDefault = require dirname(__DIR__, 1) . '/env-default.php';
$envVar = array_merge($envDefault, ENV, getCheveretoEnv());
$envVar['CHEVERETO_DB_TABLE_ROOT_PREFIX'] = $envVar['CHEVERETO_DB_TABLE_PREFIX'];
$envVar['CHEVERETO_CACHE_KEY_ROOT_PREFIX'] = $envVar['CHEVERETO_CACHE_KEY_PREFIX'];
$envVar['CHEVERETO_DB_TABLE_PREFIX'] .= '_'; // chv__
$envVar['CHEVERETO_CACHE_KEY_PREFIX'] .= '_:'; // chv:_:
new EnvVar($envVar);
@@ -273,6 +274,7 @@ $redis->connect(env()['CHEVERETO_CACHE_HOST'], (int) env()['CHEVERETO_CACHE_PORT
if (env()['CHEVERETO_CACHE_PASSWORD'] !== '') {
$redis->auth(env()['CHEVERETO_CACHE_PASSWORD']);
}
$redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP);
new EncryptionInstance(
new Encryption(
new Key(env()['CHEVERETO_ENCRYPTION_KEY'])
@@ -494,7 +496,7 @@ $databaseMigrate = function() use ($tenants, $opts, $logger): void {
}
foreach ($tenantsUpdate as $tenant) {
runAppCommand(
command: ['-C', 'database-update'],
command: ['-C', 'database-migrate'],
env: array_merge($_ENV, [
'CHEVERETO_TENANT' => $tenant['id'],
]),
@@ -527,7 +529,7 @@ $jobsWorker = function() use ($tenants, $opts, $logger): void {
while (true) {
$now = time();
if($opts['id'] ?? null) {
$tenantsId = $opts['id'];
$tenantsId = [$opts['id']];
} else {
$tenantsId = $tenants->getTenantsIds(
['is_enabled' => true],
@@ -539,7 +541,7 @@ $jobsWorker = function() use ($tenants, $opts, $logger): void {
}
foreach ($tenantsId as $tenantId) {
$tenant = $tenants->getTenant($tenantId);
$intervalSec = (int) ($tenant->env['CHEVERETO_JOBS_WORKER_INTERVAL'] ?? 300);
$intervalSec = (int) ($tenant->env['CHEVERETO_SCHEDULER_INTERVAL'] ?? 300);
$mustRun = $tenant->lastJobAt === null
|| (strtotime($tenant->lastJobAt) + $intervalSec) <= $now;
if ($mustRun) {
@@ -547,11 +549,27 @@ $jobsWorker = function() use ($tenants, $opts, $logger): void {
tenantId: $tenantId,
lastJobAt: datetimegmt()
);
$commandEnv = array_merge($_ENV, [
'CHEVERETO_TENANT' => $tenantId,
]);
$commandExit = runAppCommand(
command: ['-C', 'version-installed'],
env: $commandEnv,
isVerbose: $isVerbose,
logger: $logger
);
if($commandExit === 255) {
$logger->write(
<<<PLAIN
Tenant {$tenantId} app is not installed, skipping
PLAIN
);
continue;
}
runAppCommand(
command: ['-C', 'cron'],
env: array_merge($_ENV, [
'CHEVERETO_TENANT' => $tenantId,
]),
env: $commandEnv,
isVerbose: $isVerbose,
logger: $logger
);

View File

@@ -16,29 +16,27 @@
"composer/package-versions-deprecated": true
},
"platform": {
"php": "8.1.28"
"php": "8.2"
}
},
"require": {
"php": "^8.1",
"php": "^8.2",
"intervention/image": "^2.6",
"jeroendesloovere/xmp-metadata-extractor": "^2.0",
"guzzlehttp/psr7": "^1.7||^2",
"aws/aws-sdk-php": "^3.336.15",
"phpmailer/phpmailer": "^6.5",
"psr/cache": "^1",
"psr/cache": "^3.0",
"psr/log": "^1||^2||^3",
"phpseclib/phpseclib": "^3.0.37",
"mobiledetect/mobiledetectlib": "^2.8",
"mlocati/ip-lib": "^1.17",
"composer/ca-bundle": "^1.2",
"chevere/throwable-handler": "^1.0.2",
"pragmarx/google2fa": "^8.0",
"pragmarx/google2fa-qrcode": "^3.0",
"phpseclib/bcmath_compat": "^2.0",
"chillerlan/php-qrcode": "^4.3",
"firebase/php-jwt": "^6.3",
"lychee-org/php-exif": "1.0.2",
"firebase/php-jwt": "^7.0",
"lychee-org/php-exif": "^1.0.2",
"p3k/emoji-detector": "^1.0",
"php-ffmpeg/php-ffmpeg": "^1.2",
"psy/psysh": "^0.11.8",
@@ -46,11 +44,32 @@
"chevere/var-dump": "^2.0.7",
"chevere/var-support": "^1.0",
"matthiasmullie/scrapbook": "^1.0",
"xrdebug/php": "^3.0",
"donatj/phpuseragentparser": "^1.11",
"chevere/router": "^0.9.1",
"chevere/http": "^0.7.1",
"laminas/laminas-httphandlerrunner": "^2.13"
"laminas/laminas-httphandlerrunner": "^2.13",
"chevere/throwable-handler": "^1.0",
"symfony/mailer": "^7.4",
"symfony/http-client": "^7.4",
"symfony/amazon-mailer": "^7.4",
"symfony/aha-send-mailer": "^7.4",
"symfony/azure-mailer": "^7.4",
"symfony/brevo-mailer": "^7.4",
"symfony/infobip-mailer": "^7.4",
"symfony/mailgun-mailer": "^7.4",
"symfony/mailjet-mailer": "^7.4",
"symfony/mailomat-mailer": "^7.4",
"symfony/mail-pace-mailer": "^7.4",
"symfony/mailer-send-mailer": "^7.4",
"symfony/mailtrap-mailer": "^7.4",
"symfony/mailchimp-mailer": "^7.4",
"symfony/microsoft-graph-mailer": "^7.4",
"symfony/postal-mailer": "^7.4",
"symfony/postmark-mailer": "^7.4",
"symfony/resend-mailer": "^7.4",
"symfony/scaleway-mailer": "^7.4",
"symfony/sendgrid-mailer": "^7.4",
"symfony/sweego-mailer": "^7.4"
},
"autoload": {
"files": [

3210
app/composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -90,7 +90,6 @@ return [
'CHEVERETO_HTTPS' => '1',
'CHEVERETO_IMAGE_FORMATS_AVAILABLE' => '["AVIF","JPEG","PNG","BMP","GIF","WEBP"]',
'CHEVERETO_IMAGE_LIBRARY' => 'imagick',
'CHEVERETO_JOBS_WORKER_INTERVAL' => '300',
'CHEVERETO_MAX_ADMINS' => '0',
'CHEVERETO_MAX_ALBUMS' => '0',
'CHEVERETO_MAX_CACHE_TTL' => '86400',
@@ -115,6 +114,8 @@ return [
'CHEVERETO_MIN_STORAGES_ACTIVE' => '0',
'CHEVERETO_PROVIDER_NAME' => 'Self-hosted Chevereto',
'CHEVERETO_PROVIDER_URL' => '',
'CHEVERETO_SCHEDULER_INTERVAL' => '300',
'CHEVERETO_SERVICE_NAME' => 'app',
'CHEVERETO_SERVICING' => 'server',
'CHEVERETO_SESSION_SAVE_HANDLER' => 'files',
'CHEVERETO_SESSION_SAVE_PATH' => '/tmp',
@@ -123,6 +124,7 @@ return [
'CHEVERETO_TENANTS_API_ALLOW_LIST' => '',
'CHEVERETO_TENANTS_API_KEY_SECRET' => '',
'CHEVERETO_TENANTS_API_REQUEST_SECRET' => '',
'CHEVERETO_TRIAL' => '0',
'CHEVERETO_XRDEBUG_HOST' => 'localhost',
'CHEVERETO_XRDEBUG_HTTPS' => '0',
'CHEVERETO_XRDEBUG_KEY' => '',

View File

@@ -10,16 +10,22 @@
*/
use Chevereto\Legacy\Classes\Login;
use Chevereto\Legacy\Classes\Settings;
use Chevereto\Legacy\Classes\User;
use function Chevere\Standard\randomString;
$opts = getopt('C:u:') ?: [];
$opts = getopt('C:u:x:') ?: [];
$missing = [];
if (! isset($opts['u'])) {
echo '[Error] Missing username' . "\n";
exit(255);
}
$password = randomString(24);
$password = $opts['x'] ?? randomString(24);
if (! preg_match('/' . Settings::USER_PASSWORD_PATTERN . '/', $password)) {
echo '[Error] Invalid password' . "\n";
exit(255);
}
$user = User::getSingle($opts['u'], 'username');
if ($user === []) {
echo '[Error] User not found' . "\n";

View File

@@ -9,11 +9,11 @@
* file that was distributed with this source code.
*/
namespace Chevereto\Legacy\Classes;
use function Chevereto\Legacy\cheveretoVersionInstalled;
use PHPMailer\PHPMailer\PHPMailer;
class Mailer extends PHPMailer
{
public $XMailer = ' ';
$version = cheveretoVersionInstalled();
if ($version === '') {
echo 'Chevereto is not installed' . PHP_EOL;
exit(255);
}
echo $version . PHP_EOL;

View File

@@ -41,6 +41,7 @@ $options = [
'setting-update',
'database-migrate',
'version',
'version-installed',
'stats',
'stats-rebuild',
];

View File

@@ -59,6 +59,16 @@ if (cheveretoVersionInstalled() !== ''
throw new LogicException(message('Request denied. You must be an admin to be here.'), 403);
}
if ((env()['CHEVERETO_TENANT'] ?? '') !== '') {
if (env()['CHEVERETO_DB_TABLE_PREFIX'] === env()['CHEVERETO_DB_TABLE_ROOT_PREFIX']
|| env()['CHEVERETO_CACHE_KEY_PREFIX'] === env()['CHEVERETO_CACHE_KEY_ROOT_PREFIX']
) {
throw new LogicException(
'Tenant namespace collision detected. Refusing install/update with root prefixes.',
600
);
}
}
if (function_exists('opcache_reset')) {
try {
opcache_reset();
@@ -121,7 +131,7 @@ $settings_updates = [
// 'google' => 0, // Deprecated in 4.0.0-beta.11
// 'google_client_id' => '',
// 'google_client_secret' => '',
'guest_uploads' => 1,
'guest_uploads' => intval(env()['CHEVERETO_CONTEXT'] !== 'saas'),
'listing_items_per_page' => '24',
'maintenance' => 0,
'captcha' => 0, //recaptcha
@@ -660,6 +670,36 @@ $settings_updates = [
'4.4.0' => null,
'4.4.1' => null,
'4.4.2' => null,
'4.5.0' => [
'email_ahasend_api_key' => '',
'email_ses_access_key' => '',
'email_ses_secret_key' => '',
'email_azure_resource_name' => '',
'email_azure_key' => '',
'email_brevo_api_key' => '',
'email_infobip_api_key' => '',
'email_infobip_base_url' => '',
'email_mailgun_api_key' => '',
'email_mailgun_domain' => '',
'email_mailjet_access_key' => '',
'email_mailjet_secret_key' => '',
'email_mailomat_api_key' => '',
'email_mailpace_api_token' => '',
'email_mailersend_api_key' => '',
'email_mailtrap_api_token' => '',
'email_mandrill_api_key' => '',
'email_microsoftgraph_client_id' => '',
'email_microsoftgraph_client_secret' => '',
'email_microsoftgraph_tenant_id' => '',
'email_postal_api_key' => '',
'email_postal_base_url' => '',
'email_postmark_api_token' => '',
'email_resend_api_key' => '',
'email_scaleway_project_id' => '',
'email_scaleway_api_key' => '',
'email_sendgrid_api_key' => '',
'email_sweego_api_key' => '',
],
];
/**

View File

@@ -9,5 +9,5 @@
* file that was distributed with this source code.
*/
const APP_VERSION = '4.4.2';
const APP_VERSION_AKA = 'vivaracho';
const APP_VERSION = '4.5.0';
const APP_VERSION_AKA = 'elevado';

View File

@@ -93,21 +93,49 @@ set_exception_handler(function (Throwable $throwable) {
$internalHandler = $publicHandler->withIsDebug(true);
$doDebug = in_array($debugLevel, [2, 3], true) || isDebug();
if ($doDebug === false) {
$publicHandler = $publicHandler
->withIsDebug($doDebug)
->withPutExtra(
'Why am I seeing this?',
$incidentId = $publicHandler->id();
$providerName = getenv('CHEVERETO_PROVIDER_NAME') ?: '<provider name>';
$providerUrl = getenv('CHEVERETO_PROVIDER_URL') ?: '#';
if (getenv('CHEVERETO_CONTEXT') === 'saas') {
$title = 'Service temporarily unavailable';
$message = "We're already on it! Our team has been automatically notified and is working to resolve this issue.";
$ownerGuide = strtr(
<<<HTML
For security reasons, detailed error information is not shown. This incident has been logged and will be reviewed by the system administrator.
<p>Our team is already investigating this incident. For immediate assistance, contact %providerLink% support with incident ID %id%.</p>
HTML,
[
'%providerLink%' => <<<HTML
<a href="{$providerUrl}" target="_blank">{$providerName}</a>
HTML,
'%id%' => $incidentId,
]
);
} else {
$title = 'Something went wrong';
$message = 'Please try again later. If the error persists you may need to contact the website owner.';
$ownerGuide = <<<HTML
<p>This error has been logged with ID {$incidentId}. To diagnose the issue:</p>
<ol>
<li>Check your server error logs for this ID</li>
<li>Review the <a href="https://v4-docs.chevereto.com/developer/how-to/debug" target="_blank">debugging guide</a> in the documentation</li>
<li>Need help? Check our <a href="https://chevereto.com/support" target="_blank">support</a> alternatives</li>
</ol>
HTML;
}
$publicHandler = $publicHandler
->withTitle($title)
->withIsDebug($doDebug)
->withMessage($message)
->withPutExtra(
'What happened?',
<<<HTML
<p>A technical error has occurred. The incident has been logged for investigation.</p>
HTML
)
->withPutExtra(
'Administrator guide',
'Are you the owner of this website?',
<<<HTML
<ul>
<li>Refer to the <a href="https://v4-docs.chevereto.com/developer/how-to/debug" target="_blank">Chevereto documentation</a> to understand how to debug this error.</li>
<li>Need help? Visit <a href="https://chevereto.com/support" target="_blank">Chevereto support</a> to open a ticket.</li>
</ul>
{$ownerGuide}
<style>.administrator-guide ul{margin:0;padding-left:1.5em}</style>
HTML
);

View File

@@ -10,9 +10,11 @@
*/
use Chevere\Http\Exceptions\ControllerException;
use Chevere\Http\Exceptions\MethodNotAllowedException;
use Chevere\Parameter\Interfaces\TypeInterface;
use Chevere\Parameter\Type;
use Chevere\Router\Container;
use Chevere\Router\Exceptions\NotFoundException;
use Chevere\Writer\NullWriter;
use Chevereto\Config\Config;
use Chevereto\Legacy\Classes\Cache;
@@ -125,11 +127,32 @@ if ($isTenantsApiRouting) {
foreach ($headers as $name => $value) {
$serverRequest = $serverRequest->withHeader($name, $value);
}
$router = router(require dirname(__DIR__, 2) . '/routes/tenants-api-v4.php');
$routerPath = dirname(__DIR__, 2) . '/routes/';
$routes = [
require $routerPath . 'tenants-api-v4.php',
];
if (in_array(server()['SERVER_NAME'], ['localhost', '127.0.0.1', '::1', 'app'])) {
$routes[] = require $routerPath . 'tenants-internal-api-v4.php';
}
$router = router(...$routes);
$container = $container->withAutoInject(
$router->dependencies(),
);
$routed = $router->getRouted($serverRequest, $psr17Factory, container: $container);
try {
$routed = $router->getRouted($serverRequest, $psr17Factory, container: $container);
} catch (NotFoundException|MethodNotAllowedException $e) {
http_response_code($e->getCode());
header('Content-Type: application/json');
echo json_encode([
'error' => [
'code' => $e->getCode(),
'message' => $e->getMessage(),
'list' => explode("\n", $e->getMessage()),
],
], JSON_PRETTY_PRINT);
exit();
}
if ($routed->hasThrowable()
&& ! ($routed->throwable() instanceof ControllerException)
) {

View File

@@ -21,7 +21,6 @@ use Chevereto\Legacy\Classes\Image;
use Chevereto\Legacy\Classes\L10n;
use Chevereto\Legacy\Classes\Listing;
use Chevereto\Legacy\Classes\Login;
use Chevereto\Legacy\Classes\Mailer;
use Chevereto\Legacy\Classes\Page;
use Chevereto\Legacy\Classes\ProjectArachnid;
use Chevereto\Legacy\Classes\Settings;
@@ -34,7 +33,8 @@ use Chevereto\Legacy\G\Handler;
use FFMpeg\FFMpeg;
use FFMpeg\FFProbe;
use Intervention\Image\ImageManagerStatic;
use PHPMailer\PHPMailer\SMTP;
use Symfony\Component\Mailer\Transport;
use Symfony\Component\Mailer\Transport\Smtp\SmtpTransport;
use function Chevere\Standard\randomString;
use function Chevereto\Encryption\hasEncryption;
use function Chevereto\Legacy\badgePaid;
@@ -73,9 +73,11 @@ use function Chevereto\Legacy\getSystemNotices;
use function Chevereto\Legacy\getVariable;
use function Chevereto\Legacy\headersNoCache;
use function Chevereto\Legacy\strip_tags_content;
use function Chevereto\Legacy\trialAwareLabel;
use function Chevereto\Legacy\updateCheveretoNews;
use function Chevereto\Legacy\upload_to_content_images;
use function Chevereto\Vars\env;
use function Chevereto\Vars\envTrialAware;
use function Chevereto\Vars\files;
use function Chevereto\Vars\get;
use function Chevereto\Vars\post;
@@ -916,13 +918,13 @@ return function (Handler $handler) {
$settings_pages['title'] = _s('Add page');
$settings_pages['doing'] = 'add';
$pagesCount = Page::countAll();
$maxPages = (int) env()['CHEVERETO_MAX_PAGES'];
$maxPages = (int) envTrialAware()['CHEVERETO_MAX_PAGES'];
if ($maxPages !== 0) {
if ($pagesCount >= $maxPages) {
$is_error = true;
$error_title = _s('Quota limit reached');
$error_message = _s(
'Maximum number of pages allowed reached (limit %s).',
'Maximum number of pages allowed reached (limit %s).' . trialAwareLabel(),
$maxPages
);
$handler::setVar('error_title', $error_title);
@@ -1168,7 +1170,28 @@ return function (Handler $handler) {
// 'page_file_path_absolute' => $POST['page_file_path_absolute'],
]);
}
$mailApis = ['smtp'];
$mailApis = [
'smtp',
'ahasend',
'ses',
'azure',
'brevo',
'infobip',
'mailgun',
'mailjet',
'mailomat',
'mailpace',
'mailersend',
'mailtrap',
'mandrill',
'microsoftgraph',
'postal',
'postmark',
'resend',
'scaleway',
'sendgrid',
'sweego',
];
if (env()['CHEVERETO_SERVICING'] !== 'docker') {
$mailApis[] = 'mail';
}
@@ -1262,14 +1285,6 @@ return function (Handler $handler) {
'validate' => isset($POST['email_mode']) && in_array($POST['email_mode'], $mailApis, true),
'error_msg' => _s('Invalid email mode'),
],
'email_smtp_server_port' => [
'validate' => isset($POST['email_smtp_server_port']) && $POST['email_smtp_server_port'] > 0 && $POST['email_smtp_server_port'] < 65536,
'error_msg' => _s('Invalid SMTP port'),
],
'email_smtp_server_security' => [
'validate' => isset($POST['email_smtp_server_security']) && in_array($POST['email_smtp_server_security'], ['tls', 'ssl', 'unsecured'], true),
'error_msg' => _s('Invalid SMTP security'),
],
'website_mode' => [
'validate' => isset($POST['website_mode']) && in_array($POST['website_mode'], ['community', 'personal'], true),
'error_msg' => _s('Invalid website mode'),
@@ -1635,52 +1650,116 @@ return function (Handler $handler) {
}
}
if (isset($POST['email_mode']) && $POST['email_mode'] === 'smtp') {
$email_smtp_validate = [
'email_smtp_server' => _s('Invalid SMTP server'),
// 'email_smtp_server_username' => _s('Invalid SMTP username'),
$emailMode = $POST['email_mode'] ?? '';
$emailApiRequiredFields = [
'smtp' => ['email_smtp_server', 'email_smtp_server_port', 'email_smtp_server_security'],
'ahasend' => ['email_ahasend_api_key'],
'ses' => ['email_ses_access_key', 'email_ses_secret_key'],
'azure' => ['email_azure_resource_name', 'email_azure_key'],
'brevo' => ['email_brevo_api_key'],
'infobip' => ['email_infobip_api_key', 'email_infobip_base_url'],
'mailersend' => ['email_mailersend_api_key'],
'mailgun' => ['email_mailgun_api_key', 'email_mailgun_domain'],
'mailjet' => ['email_mailjet_access_key', 'email_mailjet_secret_key'],
'mailomat' => ['email_mailomat_api_key'],
'mailpace' => ['email_mailpace_api_token'],
'mailtrap' => ['email_mailtrap_api_token'],
'mandrill' => ['email_mandrill_api_key'],
'microsoftgraph' => ['email_microsoftgraph_client_id', 'email_microsoftgraph_client_secret', 'email_microsoftgraph_tenant_id'],
'postal' => ['email_postal_api_key', 'email_postal_base_url'],
'postmark' => ['email_postmark_api_token'],
'resend' => ['email_resend_api_key'],
'scaleway' => ['email_scaleway_project_id', 'email_scaleway_api_key'],
'sendgrid' => ['email_sendgrid_api_key'],
'sweego' => ['email_sweego_api_key'],
];
if ((env()['CHEVERETO_SERVICING'] !== 'server' && $emailMode === 'mail')
|| (env()['CHEVERETO_CONTEXT'] === 'saas' && in_array($emailMode, ['smtp', 'mail'], true))
) {
$validations['email_mode'] = [
'validate' => false,
'error_msg' => _s('The %s API is not available in this context', $emailMode),
];
foreach ($email_smtp_validate as $k => $v) {
$validations[$k] = [
'validate' => (bool) $POST[$k],
'error_msg' => $v,
}
if ($validations === [] && isset($emailApiRequiredFields[$emailMode])) {
foreach ($emailApiRequiredFields[$emailMode] as $field) {
$validations[$field] = [
'validate' => ! empty($POST[$field]),
'error_msg' => _s('Invalid value'),
];
}
$email_validate = [
'email_smtp_server',
'email_smtp_server_port',
// 'email_smtp_server_username',
// 'email_smtp_server_password',
'email_smtp_server_security',
];
$email_error = false;
foreach ($email_validate as $k) {
if (! $validations[$k]['validate']) {
$email_error = true;
}
$emailFieldsValid = array_reduce(
$emailApiRequiredFields[$emailMode],
fn (bool $carry, string $field) => $carry && ($validations[$field]['validate'] ?? false),
true
);
if ($emailMode === 'smtp') {
$validations['email_smtp_server'] = [
'validate' => (bool) ($POST['email_smtp_server'] ?? ''),
'error_msg' => _s('Invalid SMTP server'),
];
$validations['email_smtp_server_port'] = [
'validate' => isset($POST['email_smtp_server_port']) && $POST['email_smtp_server_port'] > 0 && $POST['email_smtp_server_port'] < 65536,
'error_msg' => _s('Invalid SMTP port'),
];
$validations['email_smtp_server_security'] = [
'validate' => isset($POST['email_smtp_server_security']) && in_array($POST['email_smtp_server_security'], ['tls', 'ssl', 'unsecured'], true),
'error_msg' => _s('Invalid SMTP security'),
];
$emailFieldsValid = $validations['email_smtp_server']['validate']
&& $validations['email_smtp_server_port']['validate']
&& $validations['email_smtp_server_security']['validate'];
}
if (! $email_error) {
if ($emailFieldsValid) {
try {
$mail = new Mailer(true);
$mail->Username = $POST['email_smtp_server_username'] ?? '';
$mail->Password = $POST['email_smtp_server_password'] ?? '';
$mail->SMTPAuth = $mail->Username !== '' || $mail->Password !== '';
$mail->SMTPSecure = in_array($POST['email_smtp_server_security'], ['ssl', 'tls'])
? $POST['email_smtp_server_security']
: '';
$mail->SMTPAutoTLS = in_array($POST['email_smtp_server_security'], ['ssl', 'tls']);
$mail->Host = (string) $POST['email_smtp_server'];
$mail->Port = (int) $POST['email_smtp_server_port'];
$mail->SMTPDebug = SMTP::DEBUG_SERVER;
$GLOBALS['SMTPDebug'] = '';
$mail->Debugoutput = function ($str) {
$GLOBALS['SMTPDebug'] .= "{$str}\n";
$dsn = match ($emailMode) {
'smtp' => (function () use ($POST): string {
$username = urlencode($POST['email_smtp_server_username'] ?? '');
$password = urlencode($POST['email_smtp_server_password'] ?? '');
$host = (string) $POST['email_smtp_server'];
$port = (int) $POST['email_smtp_server_port'];
$security = $POST['email_smtp_server_security'];
$scheme = $security === 'ssl' ? 'smtps' : 'smtp';
$auth = ($username !== '' || $password !== '') ? "{$username}:{$password}@" : '';
$dsn = "{$scheme}://{$auth}{$host}:{$port}";
if ($security === 'tls') {
$dsn .= '?encryption=tls';
} elseif (! in_array($security, ['ssl', 'tls'], true)) {
$dsn .= '?verify_peer=false';
}
return $dsn;
})(),
'ahasend' => 'ahasend+api://' . urlencode($POST['email_ahasend_api_key']) . '@default',
'ses' => 'ses+api://' . urlencode($POST['email_ses_access_key']) . ':' . urlencode($POST['email_ses_secret_key']) . '@default',
'azure' => 'azure+api://' . urlencode($POST['email_azure_resource_name']) . ':' . urlencode($POST['email_azure_key']) . '@default',
'brevo' => 'brevo+api://' . urlencode($POST['email_brevo_api_key']) . '@default',
'infobip' => 'infobip+api://' . urlencode($POST['email_infobip_api_key']) . '@' . urlencode($POST['email_infobip_base_url']),
'mailersend' => 'mailersend+api://' . urlencode($POST['email_mailersend_api_key']) . '@default',
'mailgun' => 'mailgun+api://' . urlencode($POST['email_mailgun_api_key']) . ':' . urlencode($POST['email_mailgun_domain']) . '@default',
'mailjet' => 'mailjet+api://' . urlencode($POST['email_mailjet_access_key']) . ':' . urlencode($POST['email_mailjet_secret_key']) . '@default',
'mailomat' => 'mailomat+api://' . urlencode($POST['email_mailomat_api_key']) . '@default',
'mailpace' => 'mailpace+api://' . urlencode($POST['email_mailpace_api_token']) . '@default',
'mailtrap' => 'mailtrap+api://' . urlencode($POST['email_mailtrap_api_token']) . '@default',
'mandrill' => 'mandrill+api://' . urlencode($POST['email_mandrill_api_key']) . '@default',
'microsoftgraph' => 'microsoftgraph+api://' . urlencode($POST['email_microsoftgraph_client_id']) . ':' . urlencode($POST['email_microsoftgraph_client_secret']) . '@default?tenantId=' . urlencode($POST['email_microsoftgraph_tenant_id']),
'postal' => 'postal+api://' . urlencode($POST['email_postal_api_key']) . '@' . urlencode($POST['email_postal_base_url']),
'postmark' => 'postmark+api://' . urlencode($POST['email_postmark_api_token']) . '@default',
'resend' => 'resend+api://' . urlencode($POST['email_resend_api_key']) . '@default',
'scaleway' => 'scaleway+api://' . urlencode($POST['email_scaleway_project_id']) . ':' . urlencode($POST['email_scaleway_api_key']) . '@default',
'sendgrid' => 'sendgrid+api://' . urlencode($POST['email_sendgrid_api_key']) . '@default',
'sweego' => 'sweego+api://' . urlencode($POST['email_sweego_api_key']) . '@default',
};
$GLOBALS['SMTPDebug'] = "SMTP Debug>>\n" . $GLOBALS['SMTPDebug'];
$mail->SmtpConnect();
} catch (Exception $e) {
$GLOBALS['SMTPDebug'] = "SMTP Exception>>\n" . ($mail->ErrorInfo ?: $e->getMessage());
$transport = Transport::fromDsn($dsn);
if ($transport instanceof SmtpTransport) {
$GLOBALS['SMTPDebug'] = '';
$transport->start();
$GLOBALS['SMTPDebug'] = "SMTP Debug>>\nConnected successfully";
} else {
$GLOBALS['SMTPDebug'] = "Transport configured: {$emailMode}";
}
} catch (Throwable $e) {
$GLOBALS['SMTPDebug'] = "Error>>\n" . $e->getMessage();
}
}
}

View File

@@ -292,6 +292,7 @@ return function (Handler $handler) {
$meta_description = $image['description'];
} else {
$image_tr = [
'%s' => _s(mb_ucfirst($image['type'])),
'%i' => $image[$image['title'] === null ? 'filename' : 'title'],
'%a' => $image['album']['name'] ?? '',
'%w' => getSetting('website_name'),
@@ -301,11 +302,11 @@ return function (Handler $handler) {
|| (
! ((bool) ($image['user']['is_private'] ?? false)) && isset($image['album']['name'])
)) {
$meta_description = _s('Image %i in %a album', $image_tr);
$meta_description = _s('%s %i in %a album', $image_tr);
} elseif (isset($image['category']['id'])) {
$meta_description = _s('Image %i in %c category', $image_tr);
$meta_description = _s('%s %i in %c category', $image_tr);
} else {
$meta_description = _s('Image %i hosted in %w', $image_tr);
$meta_description = _s('%s %i hosted in %w', $image_tr);
}
}
$handler::setVar('meta_description', $meta_description ?? '');

View File

@@ -16,10 +16,12 @@ use Chevereto\Http\Controllers\Api\V4\TenantPatch;
use Chevereto\Http\Controllers\Api\V4\TenantPlanDelete;
use Chevereto\Http\Controllers\Api\V4\TenantPlanGet;
use Chevereto\Http\Controllers\Api\V4\TenantPlanPatch;
use Chevereto\Http\Controllers\Api\V4\TenantsAuthVerifyPost;
use Chevereto\Http\Controllers\Api\V4\TenantsGet;
use Chevereto\Http\Controllers\Api\V4\TenantsPlansGet;
use Chevereto\Http\Controllers\Api\V4\TenantsPlansPost;
use Chevereto\Http\Controllers\Api\V4\TenantsPost;
use Chevereto\Http\Controllers\Api\V4\TenantUserPasswordResetPatch;
use Chevereto\Http\Middlewares\RestrictIpAccess;
use Chevereto\Http\Middlewares\SignedRequest;
use Chevereto\Http\Middlewares\TenantsApiKeyAuthorization;
@@ -28,6 +30,10 @@ use function Chevere\Router\routes;
use function Chevereto\Vars\env;
return routes(
route(
'/_/api/4/auth/verify',
POST: TenantsAuthVerifyPost::class,
),
route(
'/_/api/4/tenants',
POST: TenantsPost::class,
@@ -43,6 +49,10 @@ return routes(
'/_/api/4/tenants/{id}/install',
POST: TenantInstallPost::class,
),
route(
'/_/api/4/tenants/{id}/user-password-reset',
PATCH: TenantUserPasswordResetPatch::class,
),
route(
'/_/api/4/tenants-plans',
POST: TenantsPlansPost::class,

View File

@@ -0,0 +1,29 @@
<?php
/*
* This file is part of Chevereto.
*
* (c) Rodolfo Berrios <rodolfo@chevereto.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
use Chevereto\Http\Controllers\Api\V4\TenantsConfigTraefikGet;
use Chevereto\Http\Middlewares\RestrictIpAccess;
use Chevereto\Http\Middlewares\TenantsApiKeyAuthorization;
use function Chevere\Router\route;
use function Chevere\Router\routes;
return routes(
route(
'/_/api/4/config/traefik',
GET: TenantsConfigTraefikGet::class,
),
)
->withAppendMiddleware(
RestrictIpAccess::with(
allowList: '127.0.0.1,::1,172.16.0.0/12,192.168.65.0/24',
),
TenantsApiKeyAuthorization::class,
);

View File

@@ -17,6 +17,7 @@ CREATE TABLE `%table_root_prefix%tenants_stats` (
`pages` INT UNSIGNED NOT NULL DEFAULT '0',
`storages` INT UNSIGNED NOT NULL DEFAULT '0',
`categories` INT UNSIGNED NOT NULL DEFAULT '0',
`login_providers` INT UNSIGNED NOT NULL DEFAULT '0',
PRIMARY KEY (`tenant_id`),
KEY `updated_at` (`updated_at`),
FOREIGN KEY (tenant_id) REFERENCES `%table_root_prefix%tenants` (id) ON DELETE CASCADE

View File

@@ -33,6 +33,7 @@ class TenantPlanPatch extends Controller
public function __invoke(string $id): void
{
try {
$this->tenants->getPlan($id);
$this->tenants->editPlan(
planId: $id,
limits: $this->bodyParsed()->optional('limits')?->array(),

View File

@@ -0,0 +1,95 @@
<?php
/*
* This file is part of Chevereto.
*
* (c) Rodolfo Berrios <rodolfo@chevereto.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Chevereto\Http\Controllers\Api\V4;
use Chevere\Http\Attributes\Response;
use Chevere\Http\Controller;
use Chevere\Http\Exceptions\ControllerException;
use Chevere\Http\Status;
use Chevere\Parameter\Interfaces\ParameterInterface;
use Chevere\Writer\StreamWriter;
use Chevereto\Exceptions\NotFoundException;
use Chevereto\PCRE;
use Chevereto\Tenants\Tenants;
use function Chevere\Parameter\arrayp;
use function Chevere\Parameter\string;
use function Chevere\Standard\randomString;
use function Chevere\Writer\streamTemp;
use function Chevereto\Legacy\G\str_replace_last;
use function Chevereto\Legacy\runAppCommand;
use function Chevereto\Vars\env;
#[Response(
new Status(201, fail: 404, conflict: 409),
)]
class TenantUserPasswordResetPatch extends Controller
{
public function __construct(
private Tenants $tenants,
) {
}
public function __invoke(string $id): string
{
try {
$tenant = $this->tenants->getTenant(tenantId: $id);
} catch (NotFoundException) {
throw new ControllerException(
'Tenant not found',
404
);
}
$password = $this->bodyParsed()->optional('password')?->string()
?? randomString(16);
$logger = new StreamWriter(streamTemp());
$exit = runAppCommand(
command: [
'-C', 'password-reset',
'-u', $this->bodyParsed()->required('username')->string(),
'-x', $password,
],
env: array_merge(env(), [
'CHEVERETO_TENANT' => $tenant->id,
'CHEVERETO_CACHE_KEY_PREFIX' => str_replace_last(
'_:',
'',
env()['CHEVERETO_CACHE_KEY_PREFIX']
),
'CHEVERETO_DB_TABLE_PREFIX' => str_replace_last(
'_',
'',
env()['CHEVERETO_DB_TABLE_PREFIX']
),
]),
isVerbose: true,
logger: $logger
);
xr($exit);
if ($exit === 0) {
return $password;
}
throw new ControllerException(
'Password reset failed',
500
);
}
public static function acceptBody(): ParameterInterface
{
return arrayp(
username: string(PCRE::USER_USERNAME->value),
)->withOptional(
password: string(PCRE::USER_PASSWORD->value)
);
}
}

View File

@@ -0,0 +1,28 @@
<?php
/*
* This file is part of Chevereto.
*
* (c) Rodolfo Berrios <rodolfo@chevereto.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Chevereto\Http\Controllers\Api\V4;
use Chevere\Http\Attributes\Response;
use Chevere\Http\Controller;
use Chevere\Http\Header;
use Chevere\Http\Status;
#[Response(
new Status(200),
new Header('Content-Type', 'application/json'),
)]
class TenantsAuthVerifyPost extends Controller
{
public function __invoke(): void
{
}
}

View File

@@ -0,0 +1,43 @@
<?php
/*
* This file is part of Chevereto.
*
* (c) Rodolfo Berrios <rodolfo@chevereto.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Chevereto\Http\Controllers\Api\V4;
use Chevere\Http\Attributes\Response;
use Chevere\Http\Controller;
use Chevere\Http\Header;
use Chevere\Http\Status;
use Chevereto\Tenants\Tenants;
use Chevereto\Tenants\TenantsConfig;
use function Chevereto\Vars\env;
#[Response(
new Status(200),
new Header('Content-Type', 'application/json'),
)]
class TenantsConfigTraefikGet extends Controller
{
public function __construct(
private Tenants $tenants,
private TenantsConfig $tenantsConfig,
) {
}
public function __invoke(): array
{
return $this->tenantsConfig->getConfig(
tenants: $this->tenants,
service: env()['CHEVERETO_SERVICE_NAME'],
port: 80,
middleware: ['cf-only']
);
}
}

View File

@@ -118,7 +118,9 @@ class DB
];
$this->pdo_options = $this->pdo_default_attrs + $this->pdoAttrs;
$this->pdo_options[PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION;
$this->pdo_options[PDO::MYSQL_ATTR_INIT_COMMAND] = "SET time_zone = '+00:00', NAMES 'utf8mb4'";
$attrInitOption = class_exists('Pdo\\Mysql') ? \Pdo\Mysql::ATTR_INIT_COMMAND : PDO::MYSQL_ATTR_INIT_COMMAND;
$this->pdo_options[] = "SET time_zone = '+00:00', NAMES 'utf8mb4'";
$this->pdo_options[$attrInitOption] = "SET time_zone = '+00:00', NAMES 'utf8mb4'";
self::$dbh = new PDO($pdo_connect, $this->user, $this->pass, $this->pdo_options);
self::$dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);
self::$instance = $this;

View File

@@ -32,6 +32,34 @@ class Settings
'email_smtp_server_password',
'email_smtp_server_port',
'email_smtp_server_username',
'email_ahasend_api_key',
'email_ses_access_key',
'email_ses_secret_key',
'email_azure_resource_name',
'email_azure_key',
'email_brevo_api_key',
'email_infobip_api_key',
'email_infobip_base_url',
'email_mailgun_api_key',
'email_mailgun_domain',
'email_mailjet_access_key',
'email_mailjet_secret_key',
'email_mailomat_api_key',
'email_mailpace_api_token',
'email_mailersend_api_key',
'email_mailtrap_api_token',
'email_mandrill_api_key',
'email_microsoftgraph_client_id',
'email_microsoftgraph_client_secret',
'email_microsoftgraph_tenant_id',
'email_postal_api_key',
'email_postal_base_url',
'email_postmark_api_token',
'email_resend_api_key',
'email_scaleway_project_id',
'email_scaleway_api_key',
'email_sendgrid_api_key',
'email_sweego_api_key',
'captcha_secret',
'disqus_secret_key',
'akismet_api_key',
@@ -312,6 +340,13 @@ class Settings
'upload_image_path' => 'images',
],
],
'CHEVERETO_ENABLE_GUESTS' => ['0',
[
'enable_api_guest' => false,
'guest_uploads' => false,
'guest_albums' => false,
],
],
];
public const STOCK = [
@@ -421,6 +456,34 @@ class Settings
'theme_palette_user_select' => true,
'enable_api_user' => true,
'enable_api_guest' => false,
'email_ahasend_api_key' => '',
'email_ses_access_key' => '',
'email_ses_secret_key' => '',
'email_azure_resource_name' => '',
'email_azure_key' => '',
'email_brevo_api_key' => '',
'email_infobip_api_key' => '',
'email_infobip_base_url' => '',
'email_mailgun_api_key' => '',
'email_mailgun_domain' => '',
'email_mailjet_access_key' => '',
'email_mailjet_secret_key' => '',
'email_mailomat_api_key' => '',
'email_mailpace_api_token' => '',
'email_mailersend_api_key' => '',
'email_mailtrap_api_token' => '',
'email_mandrill_api_key' => '',
'email_microsoftgraph_client_id' => '',
'email_microsoftgraph_client_secret' => '',
'email_microsoftgraph_tenant_id' => '',
'email_postal_api_key' => '',
'email_postal_base_url' => '',
'email_postmark_api_token' => '',
'email_resend_api_key' => '',
'email_scaleway_project_id' => '',
'email_scaleway_api_key' => '',
'email_sendgrid_api_key' => '',
'email_sweego_api_key' => '',
];
public const USERNAME_MIN_LENGTH = 3;
@@ -804,7 +867,6 @@ class Settings
foreach ($binds as $bindK => $bindV) {
$db->bind($bindK, $bindV);
}
$return = $db->exec();
if ($return) {
self::cache();

View File

@@ -17,7 +17,9 @@ use LogicException;
use OverflowException;
use function Chevere\Message\message;
use function Chevereto\Legacy\G\datetimegmt;
use function Chevereto\Legacy\trialAwareLabel;
use function Chevereto\Vars\env;
use function Chevereto\Vars\envTrialAware;
class Stat
{
@@ -127,7 +129,7 @@ class Stat
600
);
}
$maxLimit = (int) (env()[$env] ?? 0);
$maxLimit = (int) (envTrialAware()[$env] ?? 0);
if ($maxLimit === 0) {
return;
}
@@ -135,7 +137,7 @@ class Stat
if (($count + $add) > $maxLimit) {
throw new OverflowException(
message(
'Maximum %t% reached (limit %s%).',
'Maximum %t% reached (limit %s%).' . trialAwareLabel(),
t: $env,
s: strval($maxLimit),
),
@@ -449,6 +451,7 @@ class Stat
'file_likes' => 's.stat_image_likes',
'album_likes' => 's.stat_album_likes',
'storage_used' => 's.stat_disk_used',
'login_providers' => 'u.login_providers',
'admins' => 'u.admins',
'managers' => 'u.managers',
'pages' => 'u.pages',
@@ -469,9 +472,7 @@ class Stat
$selectColumns = implode(',', $pairs);
$select = match ($asJsonColumn) {
true => <<<SQL
SELECT JSON_OBJECT(
{$selectColumns}
) AS stats
SELECT JSON_OBJECT({$selectColumns}) AS stats
SQL,
false => <<<SQL
SELECT {$selectColumns}
@@ -487,7 +488,8 @@ class Stat
SUM(CASE WHEN `user_is_manager` = 1 THEN 1 ELSE 0 END) AS managers,
(SELECT COUNT(*) FROM `{$tablePrefix}pages`) AS pages,
(SELECT COUNT(*) FROM `{$tablePrefix}storages` WHERE storage_deleted_at IS NULL) AS storages,
(SELECT COUNT(*) FROM `{$tablePrefix}categories`) AS categories
(SELECT COUNT(*) FROM `{$tablePrefix}categories`) AS categories,
(SELECT COUNT(*) FROM `{$tablePrefix}login_providers` WHERE login_provider_is_enabled = 1) AS login_providers
FROM `{$tablePrefix}users`
) u
WHERE s.stat_type = "total"

View File

@@ -18,6 +18,7 @@ use function Chevereto\Legacy\assertNotStopWords;
use function Chevereto\Legacy\G\get_base_url;
use function Chevereto\Legacy\G\safe_html;
use function Chevereto\Vars\env;
use function Chevereto\Vars\envTrialAware;
/**
* Tags on the database are "as is" without any encoding
@@ -176,7 +177,7 @@ final class Tag
if ($tag === []) {
return;
}
$maxTags = (int) env()['CHEVERETO_MAX_TAGS'];
$maxTags = (int) envTrialAware()['CHEVERETO_MAX_TAGS'];
if ($maxTags > 0) {
$currentTotalTags = Stat::getTotals()['tags'] ?? 0;
if ($currentTotalTags >= $maxTags) {

View File

@@ -37,7 +37,9 @@ use function Chevereto\Legacy\getSetting;
use function Chevereto\Legacy\headersNoCache;
use function Chevereto\Legacy\linkify_redirector;
use function Chevereto\Legacy\system_notification_email;
use function Chevereto\Legacy\trialAwareLabel;
use function Chevereto\Vars\env;
use function Chevereto\Vars\envTrialAware;
class User
{
@@ -134,37 +136,40 @@ class User
} else {
$userAlbums = [];
$user_stream = self::getStreamAlbum($var);
if ($user_stream === null || $user_stream['user_album_count'] === 0) {
if ($user_stream === null) {
return [];
}
$userAlbumsCount = $user_stream['user_album_count'];
unset($user_stream['user_album_count']);
$userAlbums['stream'] = $user_stream;
$map = [];
$children = [];
$columns = [
'album_id',
'album_name',
'album_privacy',
'album_parent_id',
'album_image_count',
'album_cover_id',
];
$columnsString = implode(', ', $columns);
$tableAlbums = DB::getTable('albums');
$db = DB::getInstance();
$db->query(
<<<MySQL
SELECT {$columnsString}
FROM {$tableAlbums}
WHERE album_user_id=:image_user_id
ORDER BY album_parent_id ASC, album_name ASC LIMIT :limit
MySQL
);
$db->bind(':limit', intval(env()['CHEVERETO_MAX_USER_ALBUMS_LIST']));
$db->bind(':image_user_id', $id);
$user_albums_db = $db->fetchAll();
if ($user_albums_db) {
$userAlbums += $user_albums_db;
if ($userAlbumsCount > 0) {
$columns = [
'album_id',
'album_name',
'album_privacy',
'album_parent_id',
'album_image_count',
'album_cover_id',
];
$columnsString = implode(', ', $columns);
$tableAlbums = DB::getTable('albums');
$db = DB::getInstance();
$db->query(
<<<MySQL
SELECT {$columnsString}
FROM {$tableAlbums}
WHERE album_user_id=:image_user_id
ORDER BY album_parent_id ASC, album_name ASC LIMIT :limit
MySQL
);
$db->bind(':limit', intval(env()['CHEVERETO_MAX_USER_ALBUMS_LIST']));
$db->bind(':image_user_id', $id);
$user_albums_db = $db->fetchAll();
if ($user_albums_db) {
$userAlbums += $user_albums_db;
}
}
foreach ($userAlbums as $k => &$v) {
$album_id = isset($v['album_id'])
@@ -343,7 +348,7 @@ class User
if (! array_key_exists($role, $roles)) {
throw new Exception('Invalid role', 600);
}
$maxLimit = (int) env()[$roleHandle] ?? 0;
$maxLimit = (int) envTrialAware()[$roleHandle] ?? 0;
if ($maxLimit === 0) {
return;
}
@@ -358,7 +363,7 @@ class User
if (($count + 1) > $maxLimit) {
throw new OverflowException(
message(
'Maximum %u% for role %r% reached (limit %c%)',
'Maximum %u% for role %r% reached (limit %c%)' . trialAwareLabel(),
u: _n('user', 'users', 20),
c: strval($maxLimit),
r: mb_strtolower($roleLabel),

View File

@@ -31,7 +31,6 @@ use Chevereto\Legacy\Classes\KeyValue;
use Chevereto\Legacy\Classes\KeyValueNull;
use Chevereto\Legacy\Classes\L10n;
use Chevereto\Legacy\Classes\Login;
use Chevereto\Legacy\Classes\Mailer;
use Chevereto\Legacy\Classes\Settings;
use Chevereto\Legacy\Classes\StorageApis;
use Chevereto\Legacy\Classes\Upload;
@@ -56,9 +55,12 @@ use LogicException;
use OutOfBoundsException;
use OverflowException;
use PDO;
use PHPMailer\PHPMailer\SMTP;
use Redis;
use RuntimeException;
use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Mailer\Transport;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email;
use Symfony\Component\Process\Process;
use Throwable;
use function Chevere\Filesystem\filePhpForPath;
@@ -94,6 +96,7 @@ use function Chevereto\Legacy\G\starts_with;
use function Chevereto\Legacy\G\unlinkIfExists;
use function Chevereto\Vars\cookie;
use function Chevereto\Vars\env;
use function Chevereto\Vars\envTrialAware;
use function Chevereto\Vars\post;
use function Chevereto\Vars\server;
use function Chevereto\Vars\session;
@@ -195,55 +198,76 @@ function send_mail($to, $subject, $body): bool
if (! filter_var($to, FILTER_VALIDATE_EMAIL)) {
throw new Exception('Invalid to email', 100);
}
$writer = new StreamWriter(streamFor('php://temp', 'r+'));
$body = trim($body);
$mail = new Mailer();
$mail->SMTPDebug = SMTP::DEBUG_SERVER;
$mail->Debugoutput = function ($str, $level) use ($writer) {
$writer->write("{$str} \n");
};
$alt_body = $mail->html2text($body);
$mail->CharSet = 'UTF-8';
if (getSetting('email_mode') === 'smtp') {
$mail->isSMTP();
$mail->Username = getSetting('email_smtp_server_username') ?? '';
$mail->Password = getSetting('email_smtp_server_password') ?? '';
$mail->SMTPAuth = $mail->Username !== '' || $mail->Password !== '';
$mail->SMTPSecure = in_array(getSetting('email_smtp_server_security'), ['ssl', 'tls'], true)
? getSetting('email_smtp_server_security')
: '';
$mail->SMTPAutoTLS = in_array(getSetting('email_smtp_server_security'), ['ssl', 'tls'], true);
$mail->Port = getSetting('email_smtp_server_port');
$mail->Host = getSetting('email_smtp_server');
}
$mail->Timeout = 30;
$mail->Subject = $subject;
if ($body !== $alt_body) {
$mail->IsHTML(true);
$mail->Body = $mail->normalizeBreaks($body);
$mail->AltBody = $mail->normalizeBreaks($alt_body);
} else {
$mail->Body = $body;
}
$mail->addAddress($to);
$alt_body = strip_tags($body);
$email = new Email();
$email->subject($subject);
$email->to($to);
$email->from(new Address($from[0], $from[1]));
if ($reply_to && is_array($reply_to)) {
foreach ($reply_to as $v) {
$mail->addReplyTo($v);
$email->addReplyTo($v);
}
}
$mail->setFrom($from[0], $from[1]);
if ($mail->Send()) {
return true;
if ($body !== $alt_body) {
$email->html($body);
$email->text($alt_body);
} else {
$email->text($body);
}
$mailerWrap = "\n----------- MAILER DEBUG -----------\n\n";
$error = str_replace('-', '>', $mailerWrap)
. $writer->__toString()
. str_replace('-', '<', $mailerWrap);
writers()->error()
->write($error);
xr(mailer: $error, to: $to, subject: $subject, body: $body);
$emailMode = getSetting('email_mode') ?? 'mail';
$dsn = match ($emailMode) {
'smtp' => (function (): string {
$username = urlencode(getSetting('email_smtp_server_username') ?? '');
$password = urlencode(getSetting('email_smtp_server_password') ?? '');
$host = getSetting('email_smtp_server') ?? 'localhost';
$port = getSetting('email_smtp_server_port') ?? 25;
$security = getSetting('email_smtp_server_security');
$scheme = $security === 'ssl' ? 'smtps' : 'smtp';
$auth = ($username !== '' || $password !== '') ? "{$username}:{$password}@" : '';
$dsn = "{$scheme}://{$auth}{$host}:{$port}";
if ($security === 'tls') {
$dsn .= '?encryption=tls';
} elseif (! in_array($security, ['ssl', 'tls'], true)) {
$dsn .= '?verify_peer=false';
}
throw new Exception($mail->ErrorInfo, 606);
return $dsn;
})(),
'ahasend' => 'ahasend+api://' . urlencode(getSetting('email_ahasend_api_key') ?? '') . '@default',
'ses' => 'ses+api://' . urlencode(getSetting('email_ses_access_key') ?? '') . ':' . urlencode(getSetting('email_ses_secret_key') ?? '') . '@default',
'azure' => 'azure+api://' . urlencode(getSetting('email_azure_resource_name') ?? '') . ':' . urlencode(getSetting('email_azure_key') ?? '') . '@default',
'brevo' => 'brevo+api://' . urlencode(getSetting('email_brevo_api_key') ?? '') . '@default',
'infobip' => 'infobip+api://' . urlencode(getSetting('email_infobip_api_key') ?? '') . '@' . urlencode(getSetting('email_infobip_base_url') ?? 'default'),
'mailgun' => 'mailgun+api://' . urlencode(getSetting('email_mailgun_api_key') ?? '') . ':' . urlencode(getSetting('email_mailgun_domain') ?? '') . '@default',
'mailjet' => 'mailjet+api://' . urlencode(getSetting('email_mailjet_access_key') ?? '') . ':' . urlencode(getSetting('email_mailjet_secret_key') ?? '') . '@default',
'mailomat' => 'mailomat+api://' . urlencode(getSetting('email_mailomat_api_key') ?? '') . '@default',
'mailpace' => 'mailpace+api://' . urlencode(getSetting('email_mailpace_api_token') ?? '') . '@default',
'mailersend' => 'mailersend+api://' . urlencode(getSetting('email_mailersend_api_key') ?? '') . '@default',
'mailtrap' => 'mailtrap+api://' . urlencode(getSetting('email_mailtrap_api_token') ?? '') . '@default',
'mandrill' => 'mandrill+api://' . urlencode(getSetting('email_mandrill_api_key') ?? '') . '@default',
'microsoftgraph' => 'microsoftgraph+api://' . urlencode(getSetting('email_microsoftgraph_client_id') ?? '') . ':' . urlencode(getSetting('email_microsoftgraph_client_secret') ?? '') . '@default?tenantId=' . urlencode(getSetting('email_microsoftgraph_tenant_id') ?? ''),
'postal' => 'postal+api://' . urlencode(getSetting('email_postal_api_key') ?? '') . '@' . urlencode(getSetting('email_postal_base_url') ?? 'default'),
'postmark' => 'postmark+api://' . urlencode(getSetting('email_postmark_api_token') ?? '') . '@default',
'resend' => 'resend+api://' . urlencode(getSetting('email_resend_api_key') ?? '') . '@default',
'scaleway' => 'scaleway+api://' . urlencode(getSetting('email_scaleway_project_id') ?? '') . ':' . urlencode(getSetting('email_scaleway_api_key') ?? '') . '@default',
'sendgrid' => 'sendgrid+api://' . urlencode(getSetting('email_sendgrid_api_key') ?? '') . '@default',
'sweego' => 'sweego+api://' . urlencode(getSetting('email_sweego_api_key') ?? '') . '@default',
default => 'sendmail://default',
};
try {
$transport = Transport::fromDsn($dsn);
$mailer = new Mailer($transport);
$mailer->send($email);
} catch (\Throwable $e) {
writers()->error()->write($e->getMessage());
xr(mailer: $e->getMessage(), to: $to, subject: $subject, body: $body);
throw new Exception($e->getMessage(), 606);
}
return true;
}
function get_chevereto_version(bool $full = true): string
@@ -1179,12 +1203,27 @@ function loaderHandler(
?? '';
$envVar['CHEVERETO_TENANT_HANDLE'] = '';
$envVar['CHEVERETO_DB_TABLE_ROOT_PREFIX'] = $envVar['CHEVERETO_DB_TABLE_PREFIX'];
if ($envVar['CHEVERETO_ENABLE_TENANTS'] === '1') {
$envVar['CHEVERETO_CACHE_KEY_ROOT_PREFIX'] = $envVar['CHEVERETO_CACHE_KEY_PREFIX'];
// try {
// $xrArguments = [
// 'isEnabled' => true,
// 'isHttps' => false,
// 'host' => 'host.docker.internal',
// 'port' => 27420,
// ];
// new XrInstance(new Xr(...$xrArguments));
// } catch (Throwable) {
// // Silent failover
// }
$isTenantsApi = false;
if ($envVar['CHEVERETO_ENABLE_TENANTS'] === '1' || $envVar['CHEVERETO_TENANT'] !== '') {
$redis = new Redis();
$redis->connect($envVar['CHEVERETO_CACHE_HOST'], (int) $envVar['CHEVERETO_CACHE_PORT']);
if ($envVar['CHEVERETO_CACHE_PASSWORD'] !== '') {
$redis->auth($envVar['CHEVERETO_CACHE_PASSWORD']);
}
$redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP);
$lookupNamespace = $envVar['CHEVERETO_CACHE_KEY_PREFIX'] . '_:';
if (PHP_SAPI === 'cli') {
$websiteId = $envVar['CHEVERETO_TENANT'];
@@ -1195,13 +1234,19 @@ function loaderHandler(
PLAIN;
exit(255);
}
$websiteOptions = $redis->get($lookupNamespace . 'tenant:' . $websiteId);
$websiteOptions = unserialize($websiteOptions);
$hostname = $websiteOptions['hostname'];
/** @var array $websiteOptions */
$websiteOptions = $redis->get($lookupNamespace . 'tenant:' . $websiteId) ?: [];
$hostname = $websiteOptions['hostname'] ?? '';
} else {
$hostname = $_server['SERVER_NAME'];
if (($envVar['CHEVERETO_SERVICING'] ?? '') === 'docker'
&& $hostname === 'host.docker.internal'
) {
$hostname = $envVar['CHEVERETO_HOSTNAME'];
}
$isRootHostname = hash_equals($envVar['CHEVERETO_HOSTNAME'], $hostname);
$isTenantsApi = $isRootHostname
$isLocalhost = in_array($hostname, ['localhost', '127.0.0.1', '::1', $envVar['CHEVERETO_SERVICE_NAME']], true);
$isTenantsApi = ($isRootHostname || $isLocalhost)
&& str_starts_with(
$_server['REQUEST_URI'],
$envVar['CHEVERETO_HOSTNAME_PATH']
@@ -1220,17 +1265,15 @@ function loaderHandler(
} else {
$websiteId = $redis->get($lookupNamespace . 'hostname:' . $hostname);
if ($websiteId === false) {
if ($isRootHostname) {
redirect($envVar['CHEVERETO_PROVIDER'] ?? 'https://chevereto.com');
}
http_response_code(404);
echo <<<PLAIN
No website defined
PLAIN;
exit(255);
}
$websiteOptions = $redis->get($lookupNamespace . 'tenant:' . $websiteId);
$websiteOptions = unserialize($websiteOptions);
/** @var array $websiteOptions */
$websiteOptions = $redis->get($lookupNamespace . 'tenant:' . $websiteId) ?: [];
}
}
if ($websiteOptions === []) {
@@ -1268,8 +1311,8 @@ function loaderHandler(
$envVar['CHEVERETO_TENANT_HANDLE'] = "{$websiteId}_";
$envVar['CHEVERETO_HOSTNAME'] = $hostname;
if ($websiteId !== '') {
$envVar['CHEVERETO_CACHE_KEY_PREFIX'] .= "{$websiteId}:"; // chv:ABC:
$envVar['CHEVERETO_DB_TABLE_PREFIX'] .= "{$websiteId}_"; // chv_ABC_
$envVar['CHEVERETO_CACHE_KEY_PREFIX'] .= "{$websiteId}:"; // chv:ABC: (tenant)
$envVar['CHEVERETO_DB_TABLE_PREFIX'] .= "{$websiteId}_"; // chv_ABC_ (tenant)
} else {
$envVar['CHEVERETO_CACHE_KEY_PREFIX'] .= '_:'; // chv:_: (global)
$envVar['CHEVERETO_DB_TABLE_PREFIX'] .= '_'; // chv__ (global)
@@ -1423,6 +1466,7 @@ function loaderHandler(
if (env()['CHEVERETO_CACHE_PASSWORD'] !== '') {
$redis->auth(env()['CHEVERETO_CACHE_PASSWORD']);
}
$redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP);
$keyValue = new KeyValue(
$redis,
env()['CHEVERETO_CACHE_KEY_PREFIX'],
@@ -1435,7 +1479,13 @@ function loaderHandler(
);
}
new Cache($keyValue);
if ($_session === []
$isAPI = str_starts_with(
server()['REQUEST_URI'] ?? '',
env()['CHEVERETO_HOSTNAME_PATH']
. '/api/',
);
if (! ($isAPI || $isTenantsApi)
&& $_session === []
&& session_status() === PHP_SESSION_NONE
&& ACCESS === 'web'
) {
@@ -1489,7 +1539,8 @@ function loaderHandler(
);
}
define('HTTP_APP_PROTOCOL', Config::host()->isHttps() ? 'https' : 'http');
$httpPort = ! in_array(server()['SERVER_PORT'] ?? '80', ['80', '443'], false)
// TODO: Enable ENV to force using the port?
$httpPort = ! in_array(server()['SERVER_PORT'] ?? '80', ['80', '8080', '443'], false)
? ':' . server()['SERVER_PORT']
: '';
define('URL_APP_PUBLIC', HTTP_APP_PROTOCOL . '://' . Config::host()->hostname() . $httpPort . Config::host()->hostnamePath());
@@ -1851,6 +1902,14 @@ function getCounts(string ...$table): array
return DB::queryFetchSingle($query);
}
function trialAwareLabel(): string
{
return match ('1') {
env()['CHEVERETO_TRIAL'] => ' [trial]',
default => '',
};
}
function assertMaxCount(string $table): void
{
$tablesToEnv = [
@@ -1867,7 +1926,7 @@ function assertMaxCount(string $table): void
code: 400
);
}
$maxLimit = (int) (env()[$tablesToEnv[$table]] ?? 0);
$maxLimit = (int) (envTrialAware()[$tablesToEnv[$table]] ?? 0);
if ($maxLimit === 0) {
return;
}
@@ -1875,7 +1934,7 @@ function assertMaxCount(string $table): void
if (($count + 1) > $maxLimit) {
throw new OverflowException(
message(
'Maximum number of %t% reached (limit %s%).',
'Maximum number of %t% reached (limit %s%).' . trialAwareLabel(),
t: $table,
s: strval($maxLimit),
),
@@ -1959,20 +2018,41 @@ function hashString(string $string): string
function getPoweredByRemarks(): array
{
$responsible = match (env()['CHEVERETO_CONTEXT']) {
'saas' => _s('operator'),
default => _s('owner'),
};
$termsLink = '<a href="'
. get_base_url(Handler::var('page_tos')['url'] ?? '')
. '">Terms of Service</a>';
$softwareLicenseLink = '<a href="https://chevereto.com/license">Chevereto License</a>';
if (env()['CHEVERETO_EDITION'] === 'free') {
$softwareLicenseLink = '<a href="https://www.gnu.org/licenses/agpl-3.0.en.html#license-text">AGPL-3.0 license</a>';
$softwareLicenseLink = match (env()['CHEVERETO_EDITION']) {
'free' => '<a href="https://www.gnu.org/licenses/agpl-3.0.en.html#license-text">AGPL-3.0 license</a>',
default => '<a href="https://chevereto.com/license">Chevereto License</a>',
};
$websiteName = getSetting('website_name');
if (strtolower($websiteName) === 'chevereto') {
$websiteName = 'This website';
}
$providerLink = '<a href="' . env()['CHEVERETO_PROVIDER_URL'] . '">' . env()['CHEVERETO_PROVIDER_NAME'] . '</a>';
$provider = match (env()['CHEVERETO_CONTEXT']) {
'saas' => [
'url' => env()['CHEVERETO_PROVIDER_URL'],
'label' => env()['CHEVERETO_PROVIDER_NAME'],
],
default => [
'url' => get_public_url(),
'label' => $websiteName,
],
};
$providerLink = message(
'<a href="{{ url }}">{{ label }}</a>',
...$provider
);
$about = _s('This service is based on Chevereto %edition edition software licensed under the %license.', [
'%edition' => ucfirst(env()['CHEVERETO_EDITION']),
'%license' => $softwareLicenseLink,
]);
$liability = _s("This website is hosted in a service layer not provided by Chevereto Software, which hereby declare to do not have any control nor access to the management layer of this website and it won't be responsible for this service neither the damages that this service may cause.");
$content = _s('File uploads are stored and served from storage facilities provided by %s and managed by The Service Operator.', $providerLink);
$content = _s('File uploads are stored and served from storage facilities provided and managed by the %s of this website.', $responsible);
if (env()['CHEVERETO_CONTEXT'] === 'saas') {
$about = _s('This service operates using Chevereto %edition edition software licensed under the %license.', [
'%edition' => ucfirst(env()['CHEVERETO_EDITION']),
@@ -1989,7 +2069,7 @@ function getPoweredByRemarks(): array
$liability = _s('This website is hosted on a service layer provided by %s. Chevereto Software is not responsible for the operation of this service, nor for any damages that may result from its use.', $providerLink);
}
if (env()['CHEVERETO_ENABLE_LOCAL_STORAGE'] === '0') {
$content = _s('File uploads are stored and served using external storage providers configured by The Service Operator.')
$content = _s('File uploads are stored and served using external storage providers configured by the %s of this website.', $responsible)
. ' '
. _s('%s only hosts the database and application service layer.', $providerLink)
. ' '
@@ -2029,8 +2109,7 @@ function runAppCommand(
}
$logger->write(
<<<PLAIN
{$commandLine}
{$exit}
{$exit}: {$commandLine}
PLAIN
);

View File

@@ -17,6 +17,7 @@ use Chevereto\Encryption\Encryption;
use Chevereto\Exceptions\NotFoundException;
use Chevereto\Legacy\Classes\DB;
use Chevereto\Legacy\Classes\Stat;
use ErrorException;
use PDO;
use Redis;
use function Chevereto\Legacy\G\datetimegmt;
@@ -32,6 +33,8 @@ class Tenants
'tenants_variables',
];
private string $cacheRootPrefix;
private string $cachePrefix;
private string $tableRootPrefix;
@@ -42,6 +45,7 @@ class Tenants
private Encryption $encryption,
private WriterInterface $logger = new NullWriter(),
) {
$this->cacheRootPrefix = env()['CHEVERETO_CACHE_KEY_ROOT_PREFIX']; // chv:
$this->cachePrefix = env()['CHEVERETO_CACHE_KEY_PREFIX']; // chv:<websiteId>:
$this->tableRootPrefix = env()['CHEVERETO_DB_TABLE_ROOT_PREFIX']; // chv_<websiteId>_
}
@@ -226,7 +230,7 @@ class Tenants
$tenantKey = $this->getCacheKey('tenant', $tenantId);
$tenantCache = $this->redis->get($tenantKey);
if ($tenantCache && $tenant === null) {
$tenant = unserialize($tenantCache);
$tenant = $tenantCache;
}
$hostname = $tenant['hostname'] ?? null;
if ($hostname !== null) {
@@ -250,6 +254,24 @@ class Tenants
PLAIN
);
}
$tenantCachePattern = $this->cacheRootPrefix . $tenantId . ':*';
$iterator = null;
while ($iterator !== 0) {
$scan = $this->redis->scan($iterator, "{$tenantCachePattern}");
foreach ($scan as $key) {
$result = (bool) $this->redis->del($key);
$status = 'DELETE';
if ($result === false && ! $this->redis->get($key)) {
$status = ' 404';
}
$this->logger->write(
<<<PLAIN
* {$status} > {$key}
PLAIN
);
}
}
if ($dropTables) {
$likePattern = "{$this->tableRootPrefix}{$tenantId}_%";
$this->db->query(
@@ -371,7 +393,8 @@ class Tenants
'managers', s.managers,
'pages', s.pages,
'storages', s.storages,
'categories', s.categories
'categories', s.categories,
'login_providers', s.login_providers
) AS stats
FROM `{$tableTenantsStats}` AS s
WHERE s.tenant_id = t.id
@@ -464,12 +487,14 @@ class Tenants
$this->mergeTenantCacheable($tenant);
$cached = $this->redis->get($tenantKey);
if ($cached) {
/** @var array $current */
$current = unserialize($cached);
$currentHostname = $current['hostname'];
if ($currentHostname !== $tenant['hostname']) {
$currentKey = $this->getCacheKey('hostname', $tenant['hostname']);
$this->redis->del($currentKey);
try {
/** @var array $cached */
$cachedHostname = $cached['hostname'];
if ($cachedHostname !== $tenant['hostname']) {
$cachedKey = $this->getCacheKey('hostname', $cachedHostname);
$this->redis->del($cachedKey);
}
} catch (ErrorException) {
}
}
$this->cacheTenantArray($tenant);
@@ -700,8 +725,6 @@ class Tenants
$tableTenants = $this->db::getTable('tenants');
$tableTenantsPlans = $this->db::getTable('tenants_plans');
$tableTenantsStats = $this->db::getTable('tenants_stats');
$tablePrefixTenant = $this->tableRootPrefix . $tenantId . '_';
$statQuery = Stat::getStatQuery($tablePrefixTenant);
$this->db->query(
<<<SQL
SELECT
@@ -737,7 +760,8 @@ class Tenants
'managers', s.managers,
'pages', s.pages,
'storages', s.storages,
'categories', s.categories
'categories', s.categories,
'login_providers', s.login_providers
) AS stats
FROM `{$tableTenantsStats}` AS s
WHERE s.tenant_id = t.id
@@ -767,7 +791,7 @@ class Tenants
$this->redis->mset(
[
$hostnameKey => $tenant['id'],
$tenantKey => serialize($tenant),
$tenantKey => $tenant,
]
);
}

View File

@@ -0,0 +1,146 @@
<?php
/*
* This file is part of Chevereto.
*
* (c) Rodolfo Berrios <rodolfo@chevereto.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Chevereto\Tenants;
use Redis;
use stdClass;
use Throwable;
/**
* Generates Traefik dynamic configuration for all tenants.
*/
final class TenantsConfig
{
public const CF_RANGES = [
'173.245.48.0/20',
'103.21.244.0/22',
'103.22.200.0/22',
'103.31.4.0/22',
'141.101.64.0/18',
'108.162.192.0/18',
'190.93.240.0/20',
'188.114.96.0/20',
'197.234.240.0/22',
'198.41.128.0/17',
'162.158.0.0/15',
'104.16.0.0/13',
'104.24.0.0/14',
'172.64.0.0/13',
'131.0.72.0/22',
'2400:cb00::/32',
'2606:4700::/32',
'2803:f800::/32',
'2405:b500::/32',
'2405:8100::/32',
'2a06:98c0::/29',
'2c0f:f248::/32',
];
public function __construct(
public readonly Redis $redis,
) {
}
public function getConfig(
Tenants $tenants,
string $service,
int $port,
array $middleware
): array {
$middlewares = [];
if (in_array('cf-only', $middleware)) {
$middlewares['cf-only'] = [
'ipAllowList' => [
'sourceRange' => $this->getCFRanges(),
],
];
}
$rows = $tenants->getTenantsRows();
$routers = [];
foreach ($rows as $row) {
$row = array_merge($row, [
'limits' => [],
'env' => [],
]);
$tenant = Tenant::fromRow($row);
$routers[$tenant->id] = $this->getTenantRouter(
$tenant,
$service,
array_keys($middlewares)
);
}
return [
'http' => [
'routers' => $routers,
'services' => [
$service => [
'loadBalancer' => [
'servers' => [
[
'url' => sprintf(
'http://%s:%d',
$service,
$port
),
],
],
'passHostHeader' => true,
],
],
],
'middlewares' => $middlewares,
],
];
}
public function getTenantRouter(Tenant $tenant, string $service, array $middlewares): array
{
return [
'rule' => "Host(`{$tenant->hostname}`)",
'service' => $service,
'entryPoints' => ['websecure'],
'tls' => new stdClass(),
'middlewares' => $middlewares,
];
}
public function getCFRanges(): array
{
$cacheKey = 'cf:ip-ranges';
$cached = $this->redis->get($cacheKey);
if ($cached !== false) {
return json_decode($cached, true);
}
try {
$v4 = file_get_contents('https://www.cloudflare.com/ips-v4');
$v6 = file_get_contents('https://www.cloudflare.com/ips-v6');
$ranges = array_values(
array_filter(
array_map(
'trim',
array_merge(
explode("\n", $v4),
explode("\n", $v6),
)
)
)
);
} catch (Throwable) {
return self::CF_RANGES;
}
$this->redis->setex($cacheKey, 3600, json_encode($ranges));
return $ranges;
}
}

View File

@@ -28,6 +28,55 @@ function env(): array
return $cache;
}
/**
* Returns the ENV array but limited when CHEVERETO_TRIAL='1' by
* * `CHEVERETO_TRIAL_MAX_*` (numeric limits) and
* * `CHEVERETO_TRIAL_ENABLE_*` (boolean flags).
*
* The trial variables may only be used to *decrease* the value of the
* corresponding default. In other words, if the trial value is more
* permissive than the default, the default value will always be returned.
* This keeps the SaaS trial from accidentally granting greater privileges
* than the shipped product.
*/
function envTrialAware(): array
{
if (env()['CHEVERETO_TRIAL'] !== '1') {
return env();
}
static $cache;
if (! isset($cache)) {
$cache = [];
/** @var string $value */
foreach (env() as $key => $value) {
$defaultValue = $value;
if (str_starts_with($key, 'CHEVERETO_MAX_')) {
$trialMax = 'CHEVERETO_TRIAL_MAX_' . substr($key, strlen('CHEVERETO_MAX_'));
/** @var string $trialValue */
$trialValue = array_key_exists($trialMax, env())
? env()[$trialMax]
: '0';
$cache[$key] = intval($trialValue) > intval($defaultValue)
? $defaultValue
: $trialValue;
} elseif (str_starts_with($key, 'CHEVERETO_ENABLE_')) {
$trialEnable = 'CHEVERETO_TRIAL_ENABLE_' . substr($key, strlen('CHEVERETO_ENABLE_'));
/** @var string $trialValue */
$trialValue = array_key_exists($trialEnable, env())
? env()[$trialEnable]
: '0';
$cache[$key] = intval($trialValue) > intval($defaultValue)
? $defaultValue
: $trialValue;
} else {
$cache[$key] = $value;
}
}
}
return $cache;
}
function request(): array
{
static $cache;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 453 KiB

After

Width:  |  Height:  |  Size: 344 KiB

View File

@@ -3353,6 +3353,11 @@ body.landing .top-btn-text {
padding: 10px 0;
}
.panel-thumb-list a {
display: block;
line-height: 0;
}
.panel-thumb-list img {
display: block;
width: 47px;
@@ -4527,6 +4532,7 @@ a.top-user-avatar {
.header-content .user-image img,
.header .user-image img {
border-radius: 50%;
-o-object-fit: cover;
object-fit: cover;
}

File diff suppressed because one or more lines are too long

View File

@@ -19,13 +19,11 @@ if (!defined('ACCESS') || !ACCESS) {
}
if (Login::isLoggedUser()) {
$user_albums = [];
if (Login::getUser()['album_count'] > 0) {
$user_albums = Handler::cond('owner')
&& Handler::var('user_items_editor') !== null
&& isset(Handler::var('user_items_editor')['user_albums'])
? Handler::var('user_items_editor')['user_albums']
: User::getAlbums(Login::getUser());
}
$user_albums = Handler::cond('owner')
&& Handler::var('user_items_editor') !== null
&& isset(Handler::var('user_items_editor')['user_albums'])
? Handler::var('user_items_editor')['user_albums']
: User::getAlbums(Login::getUser());
}
?>
<div id="anywhere-upload" class="no-select upload-box upload-box--fixed upload-box--hidden queueEmpty" data-queue-size="0">
@@ -164,16 +162,23 @@ if (Login::isLoggedUser()) {
</div>
<?php
}
if (Login::isLoggedUser() && Login::getUser()['album_count'] > 0) {
if (Login::isLoggedUser()) {
$isSelectedAlbum = Handler::var('album') !== [] && isset(Handler::var('album')['id_encoded']);
?>
<div class="input-label upload-input-col center-box text-align-left">
<label for="upload-album-id"><?php _ne('Album', 'Albums', 1); ?></label>
<select name="upload-album-id" id="upload-album-id" class="text-input">
<option value="_" disabled<?php echo !$isSelectedAlbum ? ' selected' : ''; ?>><?php _se('Select %s', _s('album')); ?></option>
<option value><?php _se('Create or move to %s after upload', _s('album')); ?></option>
<?php
$user_album_options_html = [];
foreach ($user_albums as $album) {
if(!$album['id_encoded']) {
continue;
}
$user_album_options_html[] = strtr('<option value="%id"%selected>%name</option>', [
'%selected' => (Handler::var('album') !== [] && isset(Handler::var('album')['id_encoded']) && Handler::var('album')['id_encoded'] == $album['id_encoded']) ? ' selected' : '',
'%selected' => ($isSelectedAlbum && isset(Handler::var('album')['id_encoded']) && Handler::var('album')['id_encoded'] == $album['id_encoded']) ? ' selected' : '',
'%id' => $album['id_encoded'],
'%name' => $album['indent_string'] . $album['name_with_privacy_readable_html']
]);

View File

@@ -38,31 +38,52 @@ echo read_the_docs_settings('email', _s('Email')); ?>
<?php
$mailOptions = [
'smtp' => _s('SMTP'),
'mail' => _s('PHP mail() func.'),
'ahasend' => 'AhaSend',
'ses' => 'Amazon SES',
'azure' => 'Azure',
'brevo' => 'Brevo',
'infobip' => 'Infobip',
'mailersend' => 'MailerSend',
'mailgun' => 'Mailgun',
'mailjet' => 'Mailjet',
'mailomat' => 'Mailomat',
'mailpace' => 'MailPace',
'mailtrap' => 'Mailtrap',
'mandrill' => 'Mandrill',
'microsoftgraph' => 'Microsoft Graph',
'postal' => 'Postal',
'postmark' => 'Postmark',
'resend' => 'Resend',
'scaleway' => 'Scaleway',
'sendgrid' => 'SendGrid',
'sweego' => 'Sweego',
];
if(env()['CHEVERETO_SERVICING'] === 'server') {
$mailOptions['mail'] = _s('PHP mail() func.');
if (env()['CHEVERETO_SERVICING'] !== 'server') {
unset($mailOptions['mail']);
}
$mailComboClass = '';
if (count($mailOptions) == 2 && (Handler::var('safe_post')
? Handler::var('safe_post')['email_mode']
: Settings::get('email_mode')) !== 'smtp'
) {
$mailComboClass = ' soft-hidden';
if (env()['CHEVERETO_CONTEXT'] === 'saas') {
unset($mailOptions['smtp'], $mailOptions['mail']);
}
$currentEmailMode = Handler::var('safe_post') ? Handler::var('safe_post')['email_mode'] : Settings::get('email_mode');
if(!array_key_exists($currentEmailMode, $mailOptions)) {
$currentEmailMode = array_key_first($mailOptions);
}
?>
<div class="input-label">
<label for="email_mode"><?php _se('Email mode'); ?></label>
<label for="email_mode"><?php _se('Email %s', 'API'); ?></label>
<div class="c5 phablet-c1"><select type="text" name="email_mode" id="email_mode" class="text-input" data-combo="mail-combo">
<?php echo get_select_options_html($mailOptions, Handler::var('safe_post') ? Handler::var('safe_post')['email_mode'] : Settings::get('email_mode')); ?>
<?php echo get_select_options_html($mailOptions, $currentEmailMode); ?>
</select></div>
<div class="input-below input-warning red-warning clear-both"><?php echo Handler::var('input_errors')['email_mode'] ?? ''; ?></div>
</div>
<div id="mail-combo">
<?php
if (isset($GLOBALS['SMTPDebug'])) {
echo '<p class="highlight padding-5 c9 phablet-c1">' . nl2br($GLOBALS['SMTPDebug']) . '</p>';
} ?>
<div data-combo-value="smtp" class="switch-combo c9 phablet-c1<?php echo $mailComboClass; ?>">
<?php if (isset($GLOBALS['SMTPDebug'])) {
echo '<p class="highlight padding-5 c9 phablet-c1 margin-bottom-10">' . nl2br($GLOBALS['SMTPDebug']) . '</p>';
} ?>
<div data-combo-value="smtp" class="switch-combo c9 phablet-c1<?php if ($currentEmailMode !== 'smtp') {
echo ' soft-hidden';
} ?>">
<div class="input-label">
<label for="email_smtp_server"><?php _se('SMTP server and port'); ?></label>
<div class="overflow-auto">
@@ -89,10 +110,225 @@ if (count($mailOptions) == 2 && (Handler::var('safe_post')
<div class="input-label c5">
<label for="email_smtp_server_security"><?php _se('SMTP security'); ?></label>
<select type="text" name="email_smtp_server_security" id="email_smtp_server_security" class="text-input">
<?php
echo get_select_options_html(['tls' => 'TLS', 'ssl' => 'SSL', 'unsecured' => _s('Unsecured')], Handler::var('safe_post') ? Handler::var('safe_post')['email_smtp_server_security'] : Settings::get('email_smtp_server_security')); ?>
<?php echo get_select_options_html(['tls' => 'TLS', 'ssl' => 'SSL', 'unsecured' => _s('Unsecured')], Handler::var('safe_post') ? Handler::var('safe_post')['email_smtp_server_security'] : Settings::get('email_smtp_server_security')); ?>
</select>
<div class="input-below input-warning red-warning clear-both"><?php echo Handler::var('input_errors')['email_smtp_server_security'] ?? ''; ?></div>
</div>
</div>
<div data-combo-value="ahasend" class="switch-combo c9 phablet-c1<?php if ($currentEmailMode !== 'ahasend') {
echo ' soft-hidden';
} ?>">
<div class="input-label">
<label for="email_ahasend_api_key"><?php _se('%s API key', 'AhaSend'); ?></label>
<input type="text" name="email_ahasend_api_key" id="email_ahasend_api_key" class="text-input" value="<?php echo Handler::var('safe_post')['email_ahasend_api_key'] ?? Settings::get('email_ahasend_api_key'); ?>">
<div class="input-warning red-warning"><?php echo Handler::var('input_errors')['email_ahasend_api_key'] ?? ''; ?></div>
</div>
</div>
<div data-combo-value="ses" class="switch-combo c9 phablet-c1<?php if ($currentEmailMode !== 'ses') {
echo ' soft-hidden';
} ?>">
<div class="input-label">
<label for="email_ses_access_key"><?php _se('%s access key', 'Amazon SES'); ?></label>
<input type="text" name="email_ses_access_key" id="email_ses_access_key" class="text-input" value="<?php echo Handler::var('safe_post')['email_ses_access_key'] ?? Settings::get('email_ses_access_key'); ?>">
<div class="input-warning red-warning"><?php echo Handler::var('input_errors')['email_ses_access_key'] ?? ''; ?></div>
</div>
<div class="input-label">
<label for="email_ses_secret_key"><?php _se('%s secret key', 'Amazon SES'); ?></label>
<input type="password" name="email_ses_secret_key" id="email_ses_secret_key" class="text-input" value="<?php echo Handler::var('safe_post')['email_ses_secret_key'] ?? Settings::get('email_ses_secret_key'); ?>">
<div class="input-warning red-warning"><?php echo Handler::var('input_errors')['email_ses_secret_key'] ?? ''; ?></div>
</div>
</div>
<div data-combo-value="azure" class="switch-combo c9 phablet-c1<?php if ($currentEmailMode !== 'azure') {
echo ' soft-hidden';
} ?>">
<div class="input-label">
<label for="email_azure_resource_name"><?php _se('%s resource name', 'Azure'); ?></label>
<input type="text" name="email_azure_resource_name" id="email_azure_resource_name" class="text-input" value="<?php echo Handler::var('safe_post')['email_azure_resource_name'] ?? Settings::get('email_azure_resource_name'); ?>">
<div class="input-warning red-warning"><?php echo Handler::var('input_errors')['email_azure_resource_name'] ?? ''; ?></div>
</div>
<div class="input-label">
<label for="email_azure_key"><?php _se('%s access key', 'Azure'); ?></label>
<input type="password" name="email_azure_key" id="email_azure_key" class="text-input" value="<?php echo Handler::var('safe_post')['email_azure_key'] ?? Settings::get('email_azure_key'); ?>">
<div class="input-warning red-warning"><?php echo Handler::var('input_errors')['email_azure_key'] ?? ''; ?></div>
</div>
</div>
<div data-combo-value="brevo" class="switch-combo c9 phablet-c1<?php if ($currentEmailMode !== 'brevo') {
echo ' soft-hidden';
} ?>">
<div class="input-label">
<label for="email_brevo_api_key"><?php _se('%s API key', 'Brevo'); ?></label>
<input type="text" name="email_brevo_api_key" id="email_brevo_api_key" class="text-input" value="<?php echo Handler::var('safe_post')['email_brevo_api_key'] ?? Settings::get('email_brevo_api_key'); ?>">
<div class="input-warning red-warning"><?php echo Handler::var('input_errors')['email_brevo_api_key'] ?? ''; ?></div>
</div>
</div>
<div data-combo-value="infobip" class="switch-combo c9 phablet-c1<?php if ($currentEmailMode !== 'infobip') {
echo ' soft-hidden';
} ?>">
<div class="input-label">
<label for="email_infobip_api_key"><?php _se('%s API key', 'Infobip'); ?></label>
<input type="text" name="email_infobip_api_key" id="email_infobip_api_key" class="text-input" value="<?php echo Handler::var('safe_post')['email_infobip_api_key'] ?? Settings::get('email_infobip_api_key'); ?>">
<div class="input-warning red-warning"><?php echo Handler::var('input_errors')['email_infobip_api_key'] ?? ''; ?></div>
</div>
<div class="input-label">
<label for="email_infobip_base_url"><?php _se('%s base URL', 'Infobip'); ?></label>
<input type="text" name="email_infobip_base_url" id="email_infobip_base_url" class="text-input" value="<?php echo Handler::var('safe_post')['email_infobip_base_url'] ?? Settings::get('email_infobip_base_url'); ?>" placeholder="e.g. xxxxx.api.infobip.com">
<div class="input-warning red-warning"><?php echo Handler::var('input_errors')['email_infobip_base_url'] ?? ''; ?></div>
</div>
</div>
<div data-combo-value="mailersend" class="switch-combo c9 phablet-c1<?php if ($currentEmailMode !== 'mailersend') {
echo ' soft-hidden';
} ?>">
<div class="input-label">
<label for="email_mailersend_api_key"><?php _se('%s API key', 'MailerSend'); ?></label>
<input type="text" name="email_mailersend_api_key" id="email_mailersend_api_key" class="text-input" value="<?php echo Handler::var('safe_post')['email_mailersend_api_key'] ?? Settings::get('email_mailersend_api_key'); ?>">
<div class="input-warning red-warning"><?php echo Handler::var('input_errors')['email_mailersend_api_key'] ?? ''; ?></div>
</div>
</div>
<div data-combo-value="mailgun" class="switch-combo c9 phablet-c1<?php if ($currentEmailMode !== 'mailgun') {
echo ' soft-hidden';
} ?>">
<div class="input-label">
<label for="email_mailgun_api_key"><?php _se('%s API key', 'Mailgun'); ?></label>
<input type="text" name="email_mailgun_api_key" id="email_mailgun_api_key" class="text-input" value="<?php echo Handler::var('safe_post')['email_mailgun_api_key'] ?? Settings::get('email_mailgun_api_key'); ?>">
<div class="input-warning red-warning"><?php echo Handler::var('input_errors')['email_mailgun_api_key'] ?? ''; ?></div>
</div>
<div class="input-label">
<label for="email_mailgun_domain"><?php _se('%s domain', 'Mailgun'); ?></label>
<input type="text" name="email_mailgun_domain" id="email_mailgun_domain" class="text-input" value="<?php echo Handler::var('safe_post')['email_mailgun_domain'] ?? Settings::get('email_mailgun_domain'); ?>" placeholder="e.g. mg.example.com">
<div class="input-warning red-warning"><?php echo Handler::var('input_errors')['email_mailgun_domain'] ?? ''; ?></div>
</div>
</div>
<div data-combo-value="mailjet" class="switch-combo c9 phablet-c1<?php if ($currentEmailMode !== 'mailjet') {
echo ' soft-hidden';
} ?>">
<div class="input-label">
<label for="email_mailjet_access_key"><?php _se('%s API key', 'Mailjet'); ?></label>
<input type="text" name="email_mailjet_access_key" id="email_mailjet_access_key" class="text-input" value="<?php echo Handler::var('safe_post')['email_mailjet_access_key'] ?? Settings::get('email_mailjet_access_key'); ?>">
<div class="input-warning red-warning"><?php echo Handler::var('input_errors')['email_mailjet_access_key'] ?? ''; ?></div>
</div>
<div class="input-label">
<label for="email_mailjet_secret_key"><?php _se('%s secret key', 'Mailjet'); ?></label>
<input type="password" name="email_mailjet_secret_key" id="email_mailjet_secret_key" class="text-input" value="<?php echo Handler::var('safe_post')['email_mailjet_secret_key'] ?? Settings::get('email_mailjet_secret_key'); ?>">
<div class="input-warning red-warning"><?php echo Handler::var('input_errors')['email_mailjet_secret_key'] ?? ''; ?></div>
</div>
</div>
<div data-combo-value="mailomat" class="switch-combo c9 phablet-c1<?php if ($currentEmailMode !== 'mailomat') {
echo ' soft-hidden';
} ?>">
<div class="input-label">
<label for="email_mailomat_api_key"><?php _se('%s API key', 'Mailomat'); ?></label>
<input type="text" name="email_mailomat_api_key" id="email_mailomat_api_key" class="text-input" value="<?php echo Handler::var('safe_post')['email_mailomat_api_key'] ?? Settings::get('email_mailomat_api_key'); ?>">
<div class="input-warning red-warning"><?php echo Handler::var('input_errors')['email_mailomat_api_key'] ?? ''; ?></div>
</div>
</div>
<div data-combo-value="mailpace" class="switch-combo c9 phablet-c1<?php if ($currentEmailMode !== 'mailpace') {
echo ' soft-hidden';
} ?>">
<div class="input-label">
<label for="email_mailpace_api_token"><?php _se('%s API token', 'MailPace'); ?></label>
<input type="text" name="email_mailpace_api_token" id="email_mailpace_api_token" class="text-input" value="<?php echo Handler::var('safe_post')['email_mailpace_api_token'] ?? Settings::get('email_mailpace_api_token'); ?>">
<div class="input-warning red-warning"><?php echo Handler::var('input_errors')['email_mailpace_api_token'] ?? ''; ?></div>
</div>
</div>
<div data-combo-value="mailtrap" class="switch-combo c9 phablet-c1<?php if ($currentEmailMode !== 'mailtrap') {
echo ' soft-hidden';
} ?>">
<div class="input-label">
<label for="email_mailtrap_api_token"><?php _se('%s API token', 'Mailtrap'); ?></label>
<input type="text" name="email_mailtrap_api_token" id="email_mailtrap_api_token" class="text-input" value="<?php echo Handler::var('safe_post')['email_mailtrap_api_token'] ?? Settings::get('email_mailtrap_api_token'); ?>">
<div class="input-warning red-warning"><?php echo Handler::var('input_errors')['email_mailtrap_api_token'] ?? ''; ?></div>
</div>
</div>
<div data-combo-value="mandrill" class="switch-combo c9 phablet-c1<?php if ($currentEmailMode !== 'mandrill') {
echo ' soft-hidden';
} ?>">
<div class="input-label">
<label for="email_mandrill_api_key"><?php _se('%s API key', 'Mandrill'); ?></label>
<input type="text" name="email_mandrill_api_key" id="email_mandrill_api_key" class="text-input" value="<?php echo Handler::var('safe_post')['email_mandrill_api_key'] ?? Settings::get('email_mandrill_api_key'); ?>">
<div class="input-warning red-warning"><?php echo Handler::var('input_errors')['email_mandrill_api_key'] ?? ''; ?></div>
</div>
</div>
<div data-combo-value="microsoftgraph" class="switch-combo c9 phablet-c1<?php if ($currentEmailMode !== 'microsoftgraph') {
echo ' soft-hidden';
} ?>">
<div class="input-label">
<label for="email_microsoftgraph_client_id"><?php _se('%s client app ID', 'Microsoft Graph'); ?></label>
<input type="text" name="email_microsoftgraph_client_id" id="email_microsoftgraph_client_id" class="text-input" value="<?php echo Handler::var('safe_post')['email_microsoftgraph_client_id'] ?? Settings::get('email_microsoftgraph_client_id'); ?>">
<div class="input-warning red-warning"><?php echo Handler::var('input_errors')['email_microsoftgraph_client_id'] ?? ''; ?></div>
</div>
<div class="input-label">
<label for="email_microsoftgraph_client_secret"><?php _se('%s client secret', 'Microsoft Graph'); ?></label>
<input type="password" name="email_microsoftgraph_client_secret" id="email_microsoftgraph_client_secret" class="text-input" value="<?php echo Handler::var('safe_post')['email_microsoftgraph_client_secret'] ?? Settings::get('email_microsoftgraph_client_secret'); ?>">
<div class="input-warning red-warning"><?php echo Handler::var('input_errors')['email_microsoftgraph_client_secret'] ?? ''; ?></div>
</div>
<div class="input-label">
<label for="email_microsoftgraph_tenant_id"><?php _se('%s tenant ID', 'Microsoft Graph'); ?></label>
<input type="text" name="email_microsoftgraph_tenant_id" id="email_microsoftgraph_tenant_id" class="text-input" value="<?php echo Handler::var('safe_post')['email_microsoftgraph_tenant_id'] ?? Settings::get('email_microsoftgraph_tenant_id'); ?>">
<div class="input-warning red-warning"><?php echo Handler::var('input_errors')['email_microsoftgraph_tenant_id'] ?? ''; ?></div>
</div>
</div>
<div data-combo-value="postal" class="switch-combo c9 phablet-c1<?php if ($currentEmailMode !== 'postal') {
echo ' soft-hidden';
} ?>">
<div class="input-label">
<label for="email_postal_api_key"><?php _se('%s API key', 'Postal'); ?></label>
<input type="text" name="email_postal_api_key" id="email_postal_api_key" class="text-input" value="<?php echo Handler::var('safe_post')['email_postal_api_key'] ?? Settings::get('email_postal_api_key'); ?>">
<div class="input-warning red-warning"><?php echo Handler::var('input_errors')['email_postal_api_key'] ?? ''; ?></div>
</div>
<div class="input-label">
<label for="email_postal_base_url"><?php _se('%s server URL', 'Postal'); ?></label>
<input type="text" name="email_postal_base_url" id="email_postal_base_url" class="text-input" value="<?php echo Handler::var('safe_post')['email_postal_base_url'] ?? Settings::get('email_postal_base_url'); ?>" placeholder="e.g. postal.example.com">
<div class="input-warning red-warning"><?php echo Handler::var('input_errors')['email_postal_base_url'] ?? ''; ?></div>
</div>
</div>
<div data-combo-value="postmark" class="switch-combo c9 phablet-c1<?php if ($currentEmailMode !== 'postmark') {
echo ' soft-hidden';
} ?>">
<div class="input-label">
<label for="email_postmark_api_token"><?php _se('%s server API token', 'Postmark'); ?></label>
<input type="text" name="email_postmark_api_token" id="email_postmark_api_token" class="text-input" value="<?php echo Handler::var('safe_post')['email_postmark_api_token'] ?? Settings::get('email_postmark_api_token'); ?>">
<div class="input-warning red-warning"><?php echo Handler::var('input_errors')['email_postmark_api_token'] ?? ''; ?></div>
</div>
</div>
<div data-combo-value="resend" class="switch-combo c9 phablet-c1<?php if ($currentEmailMode !== 'resend') {
echo ' soft-hidden';
} ?>">
<div class="input-label">
<label for="email_resend_api_key"><?php _se('%s API key', 'Resend'); ?></label>
<input type="text" name="email_resend_api_key" id="email_resend_api_key" class="text-input" value="<?php echo Handler::var('safe_post')['email_resend_api_key'] ?? Settings::get('email_resend_api_key'); ?>">
<div class="input-warning red-warning"><?php echo Handler::var('input_errors')['email_resend_api_key'] ?? ''; ?></div>
</div>
</div>
<div data-combo-value="scaleway" class="switch-combo c9 phablet-c1<?php if ($currentEmailMode !== 'scaleway') {
echo ' soft-hidden';
} ?>">
<div class="input-label">
<label for="email_scaleway_project_id"><?php _se('%s project ID', 'Scaleway'); ?></label>
<input type="text" name="email_scaleway_project_id" id="email_scaleway_project_id" class="text-input" value="<?php echo Handler::var('safe_post')['email_scaleway_project_id'] ?? Settings::get('email_scaleway_project_id'); ?>">
<div class="input-warning red-warning"><?php echo Handler::var('input_errors')['email_scaleway_project_id'] ?? ''; ?></div>
</div>
<div class="input-label">
<label for="email_scaleway_api_key"><?php _se('%s API key', 'Scaleway'); ?></label>
<input type="password" name="email_scaleway_api_key" id="email_scaleway_api_key" class="text-input" value="<?php echo Handler::var('safe_post')['email_scaleway_api_key'] ?? Settings::get('email_scaleway_api_key'); ?>">
<div class="input-warning red-warning"><?php echo Handler::var('input_errors')['email_scaleway_api_key'] ?? ''; ?></div>
</div>
</div>
<div data-combo-value="sendgrid" class="switch-combo c9 phablet-c1<?php if ($currentEmailMode !== 'sendgrid') {
echo ' soft-hidden';
} ?>">
<div class="input-label">
<label for="email_sendgrid_api_key"><?php _se('%s API key', 'SendGrid'); ?></label>
<input type="text" name="email_sendgrid_api_key" id="email_sendgrid_api_key" class="text-input" value="<?php echo Handler::var('safe_post')['email_sendgrid_api_key'] ?? Settings::get('email_sendgrid_api_key'); ?>">
<div class="input-warning red-warning"><?php echo Handler::var('input_errors')['email_sendgrid_api_key'] ?? ''; ?></div>
</div>
</div>
<div data-combo-value="sweego" class="switch-combo c9 phablet-c1<?php if ($currentEmailMode !== 'sweego') {
echo ' soft-hidden';
} ?>">
<div class="input-label">
<label for="email_sweego_api_key"><?php _se('%s API key', 'Sweego'); ?></label>
<input type="text" name="email_sweego_api_key" id="email_sweego_api_key" class="text-input" value="<?php echo Handler::var('safe_post')['email_sweego_api_key'] ?? Settings::get('email_sweego_api_key'); ?>">
<div class="input-warning red-warning"><?php echo Handler::var('input_errors')['email_sweego_api_key'] ?? ''; ?></div>
</div>
</div>
</div>

View File

@@ -0,0 +1,151 @@
<?php
use Chevereto\Legacy\Classes\Login;
use Chevereto\Legacy\Classes\Settings;
use function Chevereto\Legacy\G\get_base_url;
use Chevereto\Legacy\G\Handler;
use function Chevereto\Legacy\G\safe_html;
use function Chevereto\Legacy\get_select_options_html;
use function Chevereto\Legacy\getSetting;
// @phpstan-ignore-next-line
if (!defined('ACCESS') || !ACCESS) {
die('This file cannot be directly accessed.');
}
if (getSetting('website_mode') === 'personal' && getSetting('website_mode_personal_routing') === '/') {
echo '<div class="margin-bottom-10"><span class="icon fas fa-info-circle color-fail"></span> '
. _s('These settings do not have any effect as "%s" is overriding / (root).', [
'%s' => '<b>' . _s('%s routing', _s('Single profile')) . '</b>',
])
. '</div>';
}
echo read_the_docs_settings('homepage', _s('Homepage')); ?>
<div class="input-label">
<label for="homepage_style"><?php _se('Style'); ?></label>
<div class="c5 phablet-c1"><select type="text" name="homepage_style" id="homepage_style" class="text-input" data-combo="home-style-combo">
<?php
echo get_select_options_html([
'landing' => _s('Landing page'),
'split' => _s('Split landing + images'),
'route_explore' => _s('Route %s', _s('explore')),
'route_upload' => _s('Route %s', _s('upload')),
], Settings::get('homepage_style')); ?>
</select></div>
<div class="input-below input-warning red-warning"><?php echo Handler::var('input_errors')['homepage_style'] ?? ''; ?></div>
<div class="input-below"><?php _se('Select the homepage style.'); ?></div>
</div>
<div id="home-style-combo">
<div data-combo-value="landing split" class="switch-combo<?php if (!in_array((Handler::var('safe_post') ? Handler::var('safe_post')['homepage_style'] : Settings::get('homepage_style')), ['split', 'landing'])) {
echo ' soft-hidden';
} ?>">
<?php
foreach (Settings::get('homepage_cover_images') ?? [] as $k => $v) {
$cover_index = $k + 1;
$cover_label = 'homepage_cover_image_' . $k;
$coverName = _s('Cover image') . ' (' . $cover_index . ')'; ?>
<div class="input-label">
<label for="<?php echo $cover_label; ?>"><?php echo $coverName; ?></label>
<div class="transparent-canvas dark margin-bottom-10 col-12-max col-12"><a href="<?php echo $v['url']; ?>" target="_blank"><img class="display-block" width="100%" src="<?php echo $v['url']; ?>"></a></div>
<?php if (count(Settings::get('homepage_cover_images')) > 1) {
?>
<div class="margin-top-10 margin-bottom-10">
<a class="btn btn-small default" data-confirm="<?php _se("Do you really want to delete?"); ?> <?php _se("This can't be undone."); ?>" href="<?php echo get_base_url('dashboard/settings/homepage/?action=delete-cover&cover=' . $cover_index . '&auth_token=' . Handler::getAuthToken()); ?>"><i class="fas fa-trash-alt margin-right-5"></i><?php _se('Delete %s', $coverName); ?></a>
</div>
<?php
} ?>
<div class="c5 phablet-c1">
<input id="<?php echo $cover_label; ?>" name="<?php echo $cover_label; ?>" type="file" accept="image/*">
</div>
<div class="input-below input-warning red-warning"><?php echo Handler::var('input_errors')['homepage_cover_image_' . $k] ?? ''; ?></div>
</div>
<?php
} ?>
<div class="input-label">
<label for="homepage_cover_image_add"><?php _se('Add new cover image'); ?></label>
<div class="c5 phablet-c1">
<input id="homepage_cover_image_add" name="homepage_cover_image_add" type="file" accept="image/*">
</div>
<div class="input-below input-warning red-warning"><?php echo Handler::var('input_errors')['homepage_cover_image_add'] ?? ''; ?></div>
</div>
<hr class="line-separator">
<div class="input-label">
<label for="homepage_title_html"><?php _se('Title'); ?></label>
<div class="c12 phablet-c1"><textarea type="text" name="homepage_title_html" id="homepage_title_html" class="text-input r2 resize-vertical" placeholder="<?php echo safe_html(_s('This will be added inside the homepage %s tag. Leave it blank to use the default contents.', '<h1>')); ?>"><?php echo Settings::get('homepage_title_html'); ?></textarea></div>
</div>
<div class="input-label">
<label for="homepage_paragraph_html"><?php _se('Paragraph'); ?></label>
<div class="c12 phablet-c1"><textarea type="text" name="homepage_paragraph_html" id="homepage_paragraph_html" class="text-input r2 resize-vertical" placeholder="<?php echo safe_html(_s('This will be added inside the homepage %s tag. Leave it blank to use the default contents.', '<p>')); ?>"><?php echo Settings::get('homepage_paragraph_html'); ?></textarea></div>
</div>
<hr class="line-separator">
<div class="input-label">
<label for="homepage_cta_color"><?php _se('Call to action button color'); ?></label>
<div class="c5 phablet-c1"><select type="text" name="homepage_cta_color" id="homepage_cta_color" class="text-input">
<?php
echo get_select_options_html(
[
'accent' => 'Accent',
'blue' => _s('Blue'),
'green' => _s('Green'),
'orange' => _s('Orange'),
'red' => _s('Red'),
'grey' => _s('Grey'),
'black' => _s('Black'),
'white' => _s('White'),
'default' => _s('Default'),
],
Handler::var('safe_post')
? Handler::var('safe_post')['homepage_cta_color']
: Settings::get('homepage_cta_color')
); ?>
</select></div>
<div class="input-below input-warning red-warning clear-both"><?php echo Handler::var('input_errors')['homepage_cta_color'] ?? ''; ?></div>
<div class="input-below"><?php _se('Color of the homepage call to action button.'); ?></div>
</div>
<div class="input-label">
<label for="homepage_cta_outline"><?php _se('Call to action outline style button'); ?></label>
<div class="c5 phablet-c1"><select type="text" name="homepage_cta_outline" id="homepage_cta_outline" class="text-input">
<?php
echo get_select_options_html([1 => _s('Enabled'), 0 => _s('Disabled')], Settings::get('homepage_cta_outline')); ?>
</select></div>
<div class="input-below input-warning red-warning clear-both"><?php echo Handler::var('input_errors')['homepage_cta_outline'] ?? ''; ?></div>
<div class="input-below"><?php _se('Enable this to use outline style for the homepage call to action button.'); ?></div>
</div>
<div class="input-label">
<label for="homepage_cta_fn"><?php _se('Call to action functionality'); ?></label>
<div class="c5 phablet-c1"><select type="text" name="homepage_cta_fn" id="homepage_cta_fn" class="text-input" data-combo="cta-fn-combo">
<?php
echo get_select_options_html([
'cta-upload' => _s('Trigger uploader'),
'cta-link' => _s('Open URL'),
], Settings::get('homepage_cta_fn')); ?>
</select></div>
<div class="input-warning red-warning"><?php echo Handler::var('input_errors')['homepage_cta_fn'] ?? ''; ?></div>
</div>
<div id="cta-fn-combo">
<div data-combo-value="cta-link" class="switch-combo<?php if ((Handler::var('safe_post') ? Handler::var('safe_post')['homepage_cta_fn'] : Settings::get('homepage_cta_fn')) !== 'cta-link') {
echo ' soft-hidden';
} ?>">
<div class="input-label">
<label for="homepage_cta_fn_extra"><?php _se('Call to action URL'); ?></label>
<div class="c9 phablet-c1"><input type="text" name="homepage_cta_fn_extra" id="homepage_cta_fn_extra" class="text-input" value="<?php echo Settings::get('homepage_cta_fn_extra'); ?>" placeholder="<?php _se('Enter an absolute or relative URL'); ?>" <?php echo ((Handler::var('safe_post') ? Handler::var('safe_post')['homepage_cta_fn'] : Settings::get('homepage_cta_fn')) !== 'cta-link') ? 'data-required' : 'required'; ?>></div>
<div class="input-below input-warning red-warning"><?php echo Handler::var('input_errors')['homepage_cta_fn_extra'] ?? ''; ?></div>
<div class="input-below"><?php _se('A relative URL like %r will be mapped to %l', ['%r' => 'page/welcome', '%l' => get_base_url('page/welcome')]); ?></div>
</div>
</div>
</div>
<div class="input-label">
<label for="homepage_cta_html"><?php _se('Call to action HTML'); ?></label>
<div class="c12 phablet-c1"><textarea type="text" name="homepage_cta_html" id="homepage_cta_html" class="text-input r2 resize-vertical" placeholder="<?php echo safe_html(_s('This will be added inside the call to action <a> tag. Leave it blank to use the default contents.')); ?>"><?php echo Settings::get('homepage_cta_html'); ?></textarea></div>
</div>
</div>
<div data-combo-value="split" class="switch-combo<?php if ((Handler::var('safe_post') ? Handler::var('safe_post')['homepage_style'] : Settings::get('homepage_style')) !== 'split') {
echo ' soft-hidden';
} ?>">
<div class="input-label">
<label for="homepage_uids"><?php _se('User IDs'); ?></label>
<div class="c4"><input type="text" name="homepage_uids" id="homepage_uids" class="text-input" value="<?php echo Settings::get('homepage_uids'); ?>" placeholder="<?php _se('Empty'); ?>" rel="tooltip" title="<?php _se('Your user id is: %s', Login::getUser()['id']); ?>" data-tipTip="right"></div>
<div class="input-below input-warning red-warning"><?php echo Handler::var('input_errors')['homepage_uids'] ?? ''; ?></div>
<div class="input-below"><?php _se('Comma-separated list of target user IDs (integers) to show most recent images on homepage. Leave it empty to display trending images.'); ?></div>
</div>
</div>
</div>