Page MenuHomePhorge

No OneTemporary

Authored By
Unknown
Size
32 KB
Referenced Files
None
Subscribers
None
diff --git a/src/app/Http/Theme.php b/src/app/Http/Theme.php
index 65a43b96..5440c5be 100644
--- a/src/app/Http/Theme.php
+++ b/src/app/Http/Theme.php
@@ -1,130 +1,129 @@
<?php
namespace App\Http;
class Theme
{
protected $theme;
protected $meta = [];
public function __construct()
{
$this->theme = \config('app.theme');
$theme_file = resource_path("themes/{$this->theme}/theme.json");
if (file_exists($theme_file)) {
$this->meta = json_decode(file_get_contents($theme_file), true);
if (json_last_error() != \JSON_ERROR_NONE) {
\Log::error("Failed to parse {$theme_file}: " . json_last_error_msg());
$this->meta = [];
}
}
}
/**
* Get FAQ entries from the theme
*
* @param string $page Page name
*/
public function faq(string $page): array
{
$page = mb_strtolower(str_replace('/', '.', $page));
return $this->meta['faq'][$page] ?? [];
}
/**
* Returns list of enabled locales
*
* @return array List of two-letter language codes
*/
public static function locales(): array
{
if ($locales = \env('APP_LOCALES')) {
return preg_split('/\s*,\s*/', strtolower(trim($locales)));
}
return ['en', 'de', 'fr'];
}
/**
* Get menu definition from the theme
*/
public function menu(): array
{
- // TODO: These 2-3 lines could become a utility function somewhere
- $req_domain = preg_replace('/:[0-9]+$/', '', \request()->getHttpHost());
+ $req_domain = \request()->host();
$sys_domain = \config('app.domain');
$isAdmin = $req_domain == "admin.{$sys_domain}";
$filter = static function ($item) use ($isAdmin) {
if ($isAdmin && empty($item['admin'])) {
return false;
}
if (!$isAdmin && !empty($item['admin']) && $item['admin'] === 'only') {
return false;
}
return true;
};
$menu = array_values(array_filter($this->meta['menu'] ?? [], $filter));
// Load localization files for all supported languages
$lang_path = resource_path("themes/{$this->theme}/lang");
$locales = [];
foreach (self::locales() as $lang) {
$file = "{$lang_path}/{$lang}/menu.php";
if (file_exists($file)) {
$locales[$lang] = include $file;
}
}
foreach ($menu as $idx => $item) {
// Handle menu localization
if (!empty($item['label'])) {
$label = $item['label'];
foreach ($locales as $lang => $labels) {
if (!empty($labels[$label])) {
$item["title-{$lang}"] = $labels[$label];
}
}
}
// Unset properties that we don't need on the client side
unset($item['admin']);
$menu[$idx] = $item;
}
return $menu;
}
/**
* Get HTML <meta> definition from the theme
*/
public function meta(): array
{
return $this->meta['meta'] ?? [];
}
/**
* Get theme view name for a specified page (if exists)
*
* @param string $page Page name
*/
public function pageView(string $page): ?string
{
$page = mb_strtolower(str_replace('/', '.', $page));
$file = resource_path("themes/{$this->theme}/pages/{$page}.blade.php");
if (!file_exists($file)) {
return null;
}
return "{$this->theme}.pages.{$page}";
}
}
diff --git a/src/app/Providers/PaymentProvider.php b/src/app/Providers/PaymentProvider.php
index f7fd56ab..075a7132 100644
--- a/src/app/Providers/PaymentProvider.php
+++ b/src/app/Providers/PaymentProvider.php
@@ -1,338 +1,338 @@
<?php
namespace App\Providers;
use App\Payment;
use App\Providers\Payment\Coinbase;
use App\Providers\Payment\Mollie;
use App\Providers\Payment\Stripe;
use App\Utils;
use App\Wallet;
use Illuminate\Support\Facades\Cache;
abstract class PaymentProvider
{
public const METHOD_CREDITCARD = 'creditcard';
public const METHOD_PAYPAL = 'paypal';
public const METHOD_BANKTRANSFER = 'banktransfer';
public const METHOD_DIRECTDEBIT = 'directdebit';
public const METHOD_BITCOIN = 'bitcoin';
public const PROVIDER_MOLLIE = 'mollie';
public const PROVIDER_STRIPE = 'stripe';
public const PROVIDER_COINBASE = 'coinbase';
private static $paymentMethodIcons = [
self::METHOD_CREDITCARD => ['prefix' => 'far', 'name' => 'credit-card'],
self::METHOD_PAYPAL => ['prefix' => 'fab', 'name' => 'paypal'],
self::METHOD_BANKTRANSFER => ['prefix' => 'fas', 'name' => 'building-columns'],
self::METHOD_BITCOIN => ['prefix' => 'fab', 'name' => 'bitcoin'],
];
/**
* Detect the name of the provider
*
* @param Wallet|string|null $provider_or_wallet
*
* @return string The name of the provider
*/
private static function providerName($provider_or_wallet = null): string
{
if ($provider_or_wallet instanceof Wallet) {
$settings = $provider_or_wallet->getSettings(['stripe_id', 'mollie_id']);
if ($settings['stripe_id']) {
$provider = self::PROVIDER_STRIPE;
} elseif ($settings['mollie_id']) {
$provider = self::PROVIDER_MOLLIE;
}
} else {
$provider = $provider_or_wallet;
}
if (empty($provider)) {
$provider = \config('services.payment_provider') ?: self::PROVIDER_MOLLIE;
}
return \strtolower($provider);
}
/**
* Factory method
*
* @param Wallet|string|null $provider_or_wallet
*/
public static function factory($provider_or_wallet = null, $currency = null)
{
if (is_string($currency) && \strtolower($currency) == 'btc') {
return new Coinbase();
}
switch (self::providerName($provider_or_wallet)) {
case self::PROVIDER_STRIPE:
return new Stripe();
case self::PROVIDER_MOLLIE:
return new Mollie();
case self::PROVIDER_COINBASE:
return new Coinbase();
default:
throw new \Exception("Invalid payment provider: {$provider_or_wallet}");
}
}
/**
* Create a new auto-payment mandate for a wallet.
*
* @param Wallet $wallet The wallet
* @param array $payment Payment data:
* - amount: Value in cents (wallet currency)
* - credit_amount: Balance'able base amount in cents (wallet currency)
* - vat_rate_id: VAT rate id
* - currency: The operation currency
* - description: Operation desc.
* - methodId: Payment method
* - redirectUrl: The location to goto after checkout
*
* @return array Provider payment data:
* - id: Operation identifier
* - redirectUrl: the location to redirect to
*/
abstract public function createMandate(Wallet $wallet, array $payment): ?array;
/**
* Revoke the auto-payment mandate for a wallet.
*
* @param Wallet $wallet The wallet
*
* @return bool True on success, False on failure
*/
abstract public function deleteMandate(Wallet $wallet): bool;
/**
* Get a auto-payment mandate for a wallet.
*
* @param Wallet $wallet The wallet
*
* @return array|null Mandate information:
* - id: Mandate identifier
* - method: user-friendly payment method desc.
* - methodId: Payment method
* - isPending: the process didn't complete yet
* - isValid: the mandate is valid
*/
abstract public function getMandate(Wallet $wallet): ?array;
/**
* Get a link to the customer in the provider's control panel
*
* @param Wallet $wallet The wallet
*
* @return string|null The string representing <a> tag
*/
abstract public function customerLink(Wallet $wallet): ?string;
/**
* Get a provider name
*
* @return string Provider name
*/
abstract public function name(): string;
/**
* Create a new payment.
*
* @param Wallet $wallet The wallet
* @param array $payment Payment data:
* - amount: Value in cents (wallet currency)
* - credit_amount: Balance'able base amount in cents (wallet currency)
* - vat_rate_id: Vat rate id
* - currency: The operation currency
* - type: first/oneoff/recurring
* - description: Operation description
* - methodId: Payment method
*
* @return array Provider payment/session data:
* - id: Operation identifier
* - redirectUrl
*/
abstract public function payment(Wallet $wallet, array $payment): ?array;
/**
* Update payment status (and balance).
*
* @return int HTTP response code
*/
abstract public function webhook(): int;
/**
* Create a payment record in DB
*
* @param array $payment Payment information
* @param string $wallet_id Wallet ID
*
* @return Payment Payment object
*/
protected function storePayment(array $payment, $wallet_id): Payment
{
$payment['wallet_id'] = $wallet_id;
$payment['provider'] = $this->name();
return Payment::createFromArray($payment);
}
/**
* Convert a value from $sourceCurrency to $targetCurrency
*
* @param int $amount Amount in cents of $sourceCurrency
* @param string $sourceCurrency Currency from which to convert
* @param string $targetCurrency Currency to convert to
*
* @return int Exchanged amount in cents of $targetCurrency
*/
protected function exchange(int $amount, string $sourceCurrency, string $targetCurrency): int
{
return (int) round($amount * Utils::exchangeRate($sourceCurrency, $targetCurrency));
}
/**
* List supported payment methods from this provider
*
* @param string $type the payment type for which we require a method (oneoff/recurring)
* @param string $currency Currency code
*
* @return array Array of array with available payment methods:
* - id: id of the method
* - name: User readable name of the payment method
* - minimumAmount: Minimum amount to be charged in cents
* - currency: Currency used for the method
* - exchangeRate: The projected exchange rate (actual rate is determined during payment)
* - icon: An icon (icon name) representing the method
*/
abstract public function providerPaymentMethods(string $type, string $currency): array;
/**
* Get a payment.
*
* @param string $paymentId Payment identifier
*
* @return array Payment information:
* - id: Payment identifier
* - status: Payment status
* - isCancelable: The payment can be canceled
* - checkoutUrl: The checkout url to complete the payment or null if none
*/
abstract public function getPayment($paymentId): array;
/**
* Return an array of whitelisted payment methods with override values.
*
* @param string $type the payment type for which we require a method
*
* @return array Array of methods
*/
protected static function paymentMethodsWhitelist($type): array
{
$methods = [];
switch ($type) {
case Payment::TYPE_ONEOFF:
$methods = explode(',', \config('app.payment.methods_oneoff'));
break;
case Payment::TYPE_RECURRING:
$methods = explode(',', \config('app.payment.methods_recurring'));
break;
default:
\Log::error("Unknown payment type: " . $type);
}
$methods = array_map('strtolower', array_map('trim', $methods));
return $methods;
}
/**
* Return an array of whitelisted payment methods with override values.
*
* @param string $type the payment type for which we require a method
*
* @return array<array> Array of methods
*/
private static function applyMethodWhitelist($type, $availableMethods): array
{
$methods = [];
// Use only whitelisted methods, and apply values from whitelist (overriding the backend)
$whitelistMethods = self::paymentMethodsWhitelist($type);
foreach ($whitelistMethods as $id) {
if (array_key_exists($id, $availableMethods)) {
$method = $availableMethods[$id];
$method['icon'] = self::$paymentMethodIcons[$id];
$methods[] = $method;
}
}
return $methods;
}
/**
* List supported payment methods for $wallet
*
* @param Wallet $wallet The wallet
* @param string $type The payment type for which we require a method (oneoff/recurring)
*
* @return array<array{
* // id of the method
* id: string,
* // User readable name of the payment method
* name: string,
* // Minimum amount to be charged in cents
* minimumAmount: float,
* // Currency used for the method
* currency: string,
* // The projected exchange rate (actual rate is determined during payment)
* exchangeRate: float,
* // An icon (icon name) representing the method
* icon: array
* }>
*/
public static function paymentMethods(Wallet $wallet, $type): array
{
$providerName = self::providerName($wallet);
$cacheKey = "methods-{$providerName}-{$type}-{$wallet->currency}";
if ($methods = Cache::get($cacheKey)) {
\Log::debug("Using payment method cache" . var_export($methods, true));
return $methods;
}
$provider = self::factory($providerName);
$methods = $provider->providerPaymentMethods($type, $wallet->currency);
if (!empty(\config('services.coinbase.key'))) {
$coinbaseProvider = self::factory(self::PROVIDER_COINBASE);
$methods = array_merge($methods, $coinbaseProvider->providerPaymentMethods($type, $wallet->currency));
}
$methods = self::applyMethodWhitelist($type, $methods);
\Log::debug("Loaded payment methods " . var_export($methods, true));
Cache::put($cacheKey, $methods, now()->addHours(1));
return $methods;
}
/**
* Returns the full URL for the wallet page, used when returning from an external payment page.
* Depending on the request origin it will return a URL for the User or Reseller UI.
*
* @return string The redirect URL
*/
public static function redirectUrl(): string
{
$url = Utils::serviceUrl('/wallet');
- $domain = preg_replace('/:[0-9]+$/', '', request()->getHttpHost());
+ $domain = \request()->host();
if (str_starts_with($domain, 'reseller.')) {
$url = preg_replace('|^(https?://)([^/]+)|', '\1' . $domain, $url);
}
return $url;
}
}
diff --git a/src/app/Utils.php b/src/app/Utils.php
index 3dfc6d4e..b375666b 100644
--- a/src/app/Utils.php
+++ b/src/app/Utils.php
@@ -1,568 +1,568 @@
<?php
namespace App;
use App\Support\Facades\Theme;
use Carbon\Carbon;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
/**
* Small utility functions for App.
*/
class Utils
{
// Note: Removed '0', 'O', '1', 'I' as problematic with some fonts
public const CHARS = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ';
/**
* Exchange rates for unit tests
*/
private static $testRates;
/**
* Count the number of lines in a file.
*
* Useful for progress bars.
*
* @param string $file the filepath to count the lines of
*
* @return int
*/
public static function countLines($file)
{
$fh = fopen($file, 'r');
$numLines = 0;
while (!feof($fh)) {
$numLines += substr_count(fread($fh, 8192), "\n");
}
fclose($fh);
return $numLines;
}
/**
* Return the country ISO code for an IP address.
*
* @param string $ip IP address
* @param string $fallback Fallback country code
*/
public static function countryForIP($ip, $fallback = 'CH'): string
{
if (!str_contains($ip, ':')) {
// Skip the query if private network
if (str_starts_with($ip, '127.')) {
$net = null;
} else {
$net = IP4Net::getNet($ip);
}
} else {
$net = IP6Net::getNet($ip);
}
return $net && $net->country ? $net->country : $fallback;
}
/**
* Return the country ISO code for the current request.
*/
public static function countryForRequest()
{
$request = \request();
$ip = $request->ip();
return self::countryForIP($ip);
}
/**
* Return the number of days in the month prior to this one.
*
* @return int
*/
public static function daysInLastMonth()
{
$start = new Carbon('first day of last month');
$end = new Carbon('last day of last month');
return (int) $start->diffInDays($end) + 1;
}
/**
* Default route handler
*/
public static function defaultView()
{
// Return standard empty 404 response for non-existing resources and API routes
// TODO: Is there a better way? we'd need access to the vue-router routes here.
if (preg_match('~^(api|themes|js|vendor)/~', request()->path())) {
return response('', 404);
}
$env = self::uiEnv();
return view($env['view'])
->with('env', $env)
->with('meta', Theme::meta());
}
/**
* Download a file from the interwebz and store it locally.
*
* @param string $source The source location
* @param string $target The target location
* @param bool $force Force the download (and overwrite target)
*
* @throws \Exception
*/
public static function downloadFile($source, $target, $force = false): void
{
if (is_file($target) && !$force) {
return;
}
\Log::info("Retrieving {$source}");
Http::sink($target)->get($source)->throwUnlessStatus(200);
}
/**
* Converts an email address to lower case. Keeps the LMTP shared folder
* addresses character case intact.
*
* @param string $email Email address
*
* @return string Email address
*/
public static function emailToLower(string $email): string
{
// For LMTP shared folder address lower case the domain part only
if (str_starts_with($email, 'shared+shared/')) {
$pos = strrpos($email, '@');
$domain = substr($email, $pos + 1);
$local = substr($email, 0, strlen($email) - strlen($domain) - 1);
return $local . '@' . strtolower($domain);
}
return strtolower($email);
}
/**
* Make sure that IMAP folder access rights contains "anyone: p" permission
*
* @param array $acl ACL (in form of "user, permission" records)
*
* @return array ACL list
*/
public static function ensureAclPostPermission(array $acl): array
{
foreach ($acl as $idx => $entry) {
if (str_starts_with($entry, 'anyone,')) {
if (strpos($entry, 'read-only')) {
$acl[$idx] = 'anyone, lrsp';
} elseif (strpos($entry, 'read-write')) {
$acl[$idx] = 'anyone, lrswitednp';
}
return $acl;
}
}
$acl[] = 'anyone, p';
return $acl;
}
/**
* Generate a passphrase. Not intended for use in production, so limited to environments that are not production.
*
* @return string
*/
public static function generatePassphrase()
{
if (\config('app.env') != 'production') {
if (\config('app.passphrase')) {
return \config('app.passphrase');
}
}
$alphaLow = 'abcdefghijklmnopqrstuvwxyz';
$alphaUp = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
$num = '0123456789';
$stdSpecial = '~`!@#$%^&*()-_+=[{]}\|\'";:/?.>,<';
$source = $alphaLow . $alphaUp . $num . $stdSpecial;
$result = '';
for ($x = 0; $x < 16; $x++) {
$result .= substr($source, random_int(0, strlen($source) - 1), 1);
}
return $result;
}
/**
* Retrieve the network ID and Type from a client address
*
* @param string $clientAddress the IPv4 or IPv6 address
*
* @return array an array of ID and class or null and null
*/
public static function getNetFromAddress($clientAddress)
{
if (!str_contains($clientAddress, ':')) {
$net = IP4Net::getNet($clientAddress);
if ($net) {
return [$net->id, IP4Net::class];
}
} else {
$net = IP6Net::getNet($clientAddress);
if ($net) {
return [$net->id, IP6Net::class];
}
}
return [null, null];
}
/**
* Calculate the broadcast address provided a net number and a prefix.
*
* @param string $net a valid IPv6 network number
* @param int $prefix the network prefix
*
* @return string
*/
public static function ip6Broadcast($net, $prefix)
{
$netHex = bin2hex(inet_pton($net));
// Overwriting first address string to make sure notation is optimal
$net = inet_ntop(hex2bin($netHex));
// Calculate the number of 'flexible' bits
$flexbits = 128 - $prefix;
// Build the hexadecimal string of the last address
$lastAddrHex = $netHex;
// We start at the end of the string (which is always 32 characters long)
$pos = 31;
while ($flexbits > 0) {
// Get the character at this position
$orig = substr($lastAddrHex, $pos, 1);
// Convert it to an integer
$origval = hexdec($orig);
// OR it with (2^flexbits)-1, with flexbits limited to 4 at a time
$newval = $origval | (2 ** min(4, $flexbits) - 1);
// Convert it back to a hexadecimal character
$new = dechex($newval);
// And put that character back in the string
$lastAddrHex = substr_replace($lastAddrHex, $new, $pos, 1);
// We processed one nibble, move to previous position
$flexbits -= 4;
$pos--;
}
// Convert the hexadecimal string to a binary string
$lastaddrbin = hex2bin($lastAddrHex);
// And create an IPv6 address from the binary string
$lastaddrstr = inet_ntop($lastaddrbin);
return $lastaddrstr;
}
/**
* Checks that a model is soft-deletable
*
* @param mixed $model Model object or a class name
*/
public static function isSoftDeletable($model): bool
{
if (is_string($model) && !class_exists($model)) {
return false;
}
return method_exists($model, 'restore');
}
/**
* Normalize an email address.
*
* This means to lowercase and strip components separated with recipient delimiters.
*
* @param ?string $address The address to normalize
* @param bool $asArray Return an array with local and domain part
*
* @return string|array Normalized email address as string or array
*/
public static function normalizeAddress(?string $address, bool $asArray = false)
{
if ($address === null || $address === '') {
return $asArray ? ['', ''] : '';
}
$address = self::emailToLower($address);
if (!str_contains($address, '@')) {
return $asArray ? [$address, ''] : $address;
}
[$local, $domain] = explode('@', $address);
if (str_contains($local, '+')) {
$local = explode('+', $local)[0];
}
return $asArray ? [$local, $domain] : "{$local}@{$domain}";
}
/**
* Returns the current user's email address or null.
*/
public static function userEmailOrNull(): ?string
{
$user = Auth::user();
if (!$user) {
return null;
}
return $user->email;
}
/**
* Returns a random string consisting of a quantity of segments of a certain length joined.
*
* Example:
*
* ```php
* $roomName = strtolower(\App\Utils::randStr(3, 3, '-');
* // $roomName == '3qb-7cs-cjj'
* ```
*
* @param int $length The length of each segment
* @param int $qty The quantity of segments
* @param string $join The string to use to join the segments
* @param string $chars The characters to use to build the code
*
* @return string
*/
public static function randStr($length, $qty = 1, $join = '', string $chars = '')
{
if (strlen($chars) == 0) {
$chars = env('SHORTCODE_CHARS', self::CHARS);
}
$randStrs = [];
for ($x = 0; $x < $qty; $x++) {
$string = [];
for ($y = 0; $y < $length; $y++) {
$string[] = $chars[random_int(0, strlen($chars) - 1)];
}
shuffle($string);
$randStrs[$x] = implode('', $string);
}
return implode($join, $randStrs);
}
/**
* Returns a UUID in the form of an integer.
*/
public static function uuidInt(): int
{
$hex = self::uuidStr();
$bin = pack('h*', str_replace('-', '', $hex));
$ids = unpack('L', $bin);
$id = array_shift($ids);
return $id;
}
/**
* Returns a UUID in the form of a string.
*/
public static function uuidStr(): string
{
return (string) Str::uuid();
}
/**
* Create self URL
*
* @param string $route Route/Path/URL
* @param int|null $tenantId Current tenant
*
* @todo Move this to App\Http\Controllers\Controller
*
* @return string Full URL
*/
public static function serviceUrl(string $route, $tenantId = null): string
{
if (preg_match('|^https?://|i', $route)) {
return $route;
}
$url = Tenant::getConfig($tenantId, 'app.public_url');
if (!$url) {
$url = Tenant::getConfig($tenantId, 'app.url');
}
return rtrim(trim($url, '/') . '/' . ltrim($route, '/'), '/');
}
/**
* Create a configuration/environment data to be passed to
* the UI
*
* @todo Move this to App\Http\Controllers\Controller
*
* @return array Configuration data
*/
public static function uiEnv(): array
{
$countries = include resource_path('countries.php');
- $req_domain = preg_replace('/:[0-9]+$/', '', request()->getHttpHost());
+ $req_domain = \request()->host();
$sys_domain = \config('app.domain');
$opts = [
'app.name',
'app.url',
'app.domain',
'app.theme',
'app.webmail_url',
'app.support_email',
'app.company.copyright',
'app.companion_download_link',
'app.shared_folder_types',
'app.with_signup',
'app.with_user_search',
'mail.from.address',
];
$env = \app('config')->getMany($opts);
$env['countries'] = $countries ?: [];
$env['view'] = 'root';
$env['jsapp'] = 'user.js';
if ($req_domain == "admin.{$sys_domain}") {
$env['jsapp'] = 'admin.js';
} elseif ($req_domain == "reseller.{$sys_domain}") {
$env['jsapp'] = 'reseller.js';
}
$env['paymentProvider'] = \config('services.payment_provider');
$env['stripePK'] = \config('services.stripe.public_key');
$env['maxChunkSize'] = \App\Backends\Storage::maxChunkSize();
$env['languages'] = Theme::locales();
$env['menu'] = Theme::menu();
return $env;
}
/**
* Set test exchange rates.
*
* @param array $rates: Exchange rates
*/
public static function setTestExchangeRates(array $rates): void
{
self::$testRates = $rates;
}
/**
* Retrieve an exchange rate.
*
* @param string $sourceCurrency: Currency from which to convert
* @param string $targetCurrency: Currency to convert to
*
* @return float Exchange rate
*/
public static function exchangeRate(string $sourceCurrency, string $targetCurrency): float
{
if (strcasecmp($sourceCurrency, $targetCurrency) == 0) {
return 1.0;
}
if (isset(self::$testRates[$targetCurrency])) {
return (float) self::$testRates[$targetCurrency];
}
$currencyFile = resource_path("exchangerates-{$sourceCurrency}.php");
// Attempt to find the reverse exchange rate, if we don't have the file for the source currency
if (!file_exists($currencyFile)) {
$rates = include resource_path("exchangerates-{$targetCurrency}.php");
if (!isset($rates[$sourceCurrency])) {
throw new \Exception("Failed to find the reverse exchange rate for " . $sourceCurrency);
}
return 1.0 / (float) $rates[$sourceCurrency];
}
$rates = include $currencyFile;
if (!isset($rates[$targetCurrency])) {
throw new \Exception("Failed to find exchange rate for " . $targetCurrency);
}
return (float) $rates[$targetCurrency];
}
/**
* A helper to display human-readable amount of money using
* for specified currency and locale.
*
* @param int $amount Amount of money (in cents)
* @param string $currency Currency code
* @param string $locale Output locale
*
* @return string String representation, e.g. "9.99 CHF"
*/
public static function money(int $amount, $currency, $locale = 'de_DE'): string
{
$nf = new \NumberFormatter($locale, \NumberFormatter::CURRENCY);
$result = $nf->formatCurrency(round($amount / 100, 2), $currency);
// Replace non-breaking space
return str_replace("\xC2\xA0", " ", $result);
}
/**
* A helper to display human-readable percent value
* for specified currency and locale.
*
* @param int|float $percent Percent value (0 to 100)
* @param string $locale Output locale
*
* @return string String representation, e.g. "0 %", "7.7 %"
*/
public static function percent(float|int $percent, $locale = 'de_DE'): string
{
$nf = new \NumberFormatter($locale, \NumberFormatter::PERCENT);
$sep = $nf->getSymbol(\NumberFormatter::DECIMAL_SEPARATOR_SYMBOL);
$result = sprintf('%.2F', $percent);
$result = preg_replace('/\.00/', '', $result);
$result = preg_replace('/(\.[0-9])0/', '\1', $result);
$result = str_replace('.', $sep, $result);
return $result . ' %';
}
}

File Metadata

Mime Type
text/x-diff
Expires
Fri, Apr 24, 11:04 AM (1 w, 5 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18896237
Default Alt Text
(32 KB)

Event Timeline