refactor: replace custom CRM with Monica fork
Some checks failed
Build & Push Monica Image to Gitea Registry / build-and-push (push) Failing after 9s

This commit is contained in:
Kroonk
2026-05-22 16:19:55 +02:00
parent 38cc4c09ca
commit a213a1dba0
2215 changed files with 242567 additions and 6015 deletions

View File

@@ -0,0 +1,219 @@
<?php
namespace App\Helpers;
use App\Models\Contact\Gender;
use App\Models\Account\Account;
use Illuminate\Support\Collection;
class AccountHelper
{
/**
* Indicates whether the given account has limitations with her current
* plan.
*
* @return bool
*/
public static function hasLimitations(Account $account): bool
{
if ($account->has_access_to_paid_version_for_free) {
return false;
}
if (! config('monica.requires_subscription')) {
return false;
}
if ($account->isSubscribed()) {
return false;
}
return true;
}
/**
* Indicate whether an account has reached the contact limit if the account
* is on a free trial.
*
* @param Account $account
* @return bool
*/
public static function hasReachedContactLimit(Account $account): bool
{
return $account->allContacts()->real()->active()->count() >= config('monica.number_of_allowed_contacts_free_account');
}
/**
* Indicate whether an account has not reached the contact limit of free accounts.
*
* @param Account $account
* @return bool
*/
public static function isBelowContactLimit(Account $account): bool
{
return $account->allContacts()->real()->active()->count() <= config('monica.number_of_allowed_contacts_free_account');
}
/**
* Check if the account can be downgraded, based on a set of rules.
*
* @param Account $account
* @return bool
*/
public static function canDowngrade(Account $account): bool
{
$canDowngrade = true;
$numberOfUsers = $account->users()->count();
$numberPendingInvitations = $account->invitations()->count();
$numberActiveContacts = $account->allContacts()->active()->count();
// number of users in the account should be == 1
if ($numberOfUsers > 1) {
$canDowngrade = false;
}
// there should not be any pending user invitations
if ($numberPendingInvitations > 0) {
$canDowngrade = false;
}
// there should not be more than the number of contacts allowed
if ($numberActiveContacts > config('monica.number_of_allowed_contacts_free_account')) {
$canDowngrade = false;
}
return $canDowngrade;
}
/**
* Get the default gender for this account.
*
* @param Account $account
* @return string
*/
public static function getDefaultGender(Account $account): string
{
$defaultGenderType = Gender::UNKNOWN;
if ($account->default_gender_id) {
$defaultGender = Gender::where([
'account_id' => $account->id,
])->find($account->default_gender_id);
if ($defaultGender) {
$defaultGenderType = $defaultGender->type;
}
}
return $defaultGenderType;
}
/**
* Get the reminders for the month given in parameter.
* - 0 means current month
* - 1 means month+1
* - 2 means month+2...
*
* @param Account $account
* @param int $month
*/
public static function getUpcomingRemindersForMonth(Account $account, int $month)
{
$startOfMonth = now(DateHelper::getTimezone())->addMonthsNoOverflow($month)->startOfMonth();
// don't get reminders for past events:
if ($startOfMonth->isPast()) {
$startOfMonth = now(DateHelper::getTimezone());
}
$endOfMonth = now(DateHelper::getTimezone())->addMonthsNoOverflow($month)->endOfMonth();
return $account->reminderOutboxes()
->with(['reminder', 'reminder.contact'])
->whereBetween('planned_date', [$startOfMonth, $endOfMonth])
->where([
'user_id' => auth()->user()->id,
'nature' => 'reminder',
])
->orderBy('planned_date', 'asc')
->get()
->filter(function ($reminderOutbox) {
return $reminderOutbox->reminder->contact !== null;
});
}
/**
* Get the number of activities grouped by year.
*
* @param Account $account
* @return Collection
*/
public static function getYearlyActivitiesStatistics(Account $account): Collection
{
$activitiesStatistics = collect([]);
$activities = $account->activities()
->select('happened_at')
->latest('happened_at')
->get();
$years = [];
foreach ($activities as $activity) {
$yearStatistic = $activity->happened_at->format('Y');
$foundInYear = false;
foreach ($years as $year => $number) {
if ($year == $yearStatistic) {
$years[$year] = $number + 1;
$foundInYear = true;
}
}
if (! $foundInYear) {
$years[$yearStatistic] = 1;
}
}
foreach ($years as $year => $number) {
$activitiesStatistics->put($year, $number);
}
return $activitiesStatistics;
}
/**
* Get the number of calls grouped by year.
*
* @return Collection
*/
public static function getYearlyCallStatistics(Account $account): Collection
{
$callsStatistics = collect([]);
$calls = $account->calls()
->select('called_at')
->latest('called_at')
->get();
$years = [];
foreach ($calls as $call) {
$yearStatistic = $call->called_at->format('Y');
$foundInYear = false;
foreach ($years as $year => $number) {
if ($year == $yearStatistic) {
$years[$year] = $number + 1;
$foundInYear = true;
}
}
if (! $foundInYear) {
$years[$yearStatistic] = 1;
}
}
foreach ($years as $year => $number) {
$callsStatistics->put($year, $number);
}
return $callsStatistics;
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace App\Helpers;
use App\Models\Contact\Contact;
use Illuminate\Support\Collection;
use Illuminate\Database\Eloquent\ModelNotFoundException;
class AuditLogHelper
{
/**
* Prepare a collection of audit logs that is displayed on the Settings page.
*
* @param \Illuminate\Contracts\Pagination\LengthAwarePaginator|Collection<array-key, \App\Models\Instance\AuditLog> $logs
* @return Collection
*/
public static function getCollectionOfAudits($logs): Collection
{
$logsCollection = collect();
foreach ($logs as $log) {
$object = null;
$link = null;
// the log is about a contact
if (isset($log->object->{'contact_id'})) {
try {
// check if the contact that the log is about still exists
// in that case, we will display a link to point to this contact
$contact = Contact::findOrFail($log->object->{'contact_id'});
$object = $contact->name;
$link = route('people.show', ['contact' => $contact]);
} catch (ModelNotFoundException $e) {
// the contact doesn't exist anymore, we don't need a link, we'll only display a name
$object = $log->object->{'contact_name'};
}
$description = trans('logs.settings_log_'.$log->action.'_with_name', ['name' => $object]);
} else {
$description = trans('logs.settings_log_'.$log->action, ['name' => $log->object->{'name'}]);
}
$logsCollection->push([
'author_name' => ($log->author) ? $log->author->name : $log->author_name,
'description' => $description,
'link' => $link,
'object' => $object,
'audited_at' => $log->audited_at,
]);
}
return $logsCollection;
}
}

View File

@@ -0,0 +1,106 @@
<?php
namespace App\Helpers;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
class CollectionHelper
{
/**
* Sort the collection using the given callback.
*
* @param \Illuminate\Support\Collection $collect
* @param callable|string $callback
* @param int $options
* @param bool $descending
* @return Collection
*/
public static function sortByCollator($collect, $callback, $options = \Collator::SORT_STRING, $descending = false)
{
$results = [];
$callback = static::valueRetriever($callback);
// First we will loop through the items and get the comparator from a callback
// function which we were given. Then, we will sort the returned values and
// and grab the corresponding values for the sorted keys from this array.
foreach ($collect->all() as $key => $value) {
$results[$key] = $callback($value, $key);
}
// Using Collator to sort the array, with locale-sensitive sort ordering support.
static::getCollator()->asort($results, $options);
if ($descending) {
$results = array_reverse($results);
}
// Once we have sorted all of the keys in the array, we will loop through them
// and grab the corresponding model so we can set the underlying items list
// to the sorted version. Then we'll just return the collection instance.
foreach (array_keys($results) as $key) {
$results[$key] = $collect->get($key);
}
return new Collection($results);
}
/**
* Get a Collator object for the locale or current locale.
*
* @param string $locale
* @return \Collator
*/
public static function getCollator($locale = null)
{
static $collators = [];
if (! $locale) {
$locale = app()->getLocale();
}
if (! Arr::has($collators, $locale)) {
$collator = new \Collator($locale);
if (LocaleHelper::getLang($locale) == 'fr') {
$collator->setAttribute(\Collator::FRENCH_COLLATION, \Collator::ON);
}
$collators[$locale] = $collator;
return $collator;
}
return $collators[$locale];
}
/**
* Get a value retrieving callback.
*
* @param string|callable $value
* @return callable
*/
private static function valueRetriever($value)
{
if (! is_string($value) && is_callable($value)) {
return $value;
}
return function ($item) use ($value) {
return data_get($item, $value);
};
}
/**
* Group collection based on a specific property from its items.
*
* @param \Illuminate\Support\Collection $collection
* @param string $property
* @return mixed
*/
public static function groupByItemsProperty($collection, $property)
{
return $collection->mapToGroups(function ($item) use ($property) {
return [data_get($item, $property) => $item];
});
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Helpers;
use App\Models\User\User;
use App\Models\Settings\Term;
use Illuminate\Support\Facades\DB;
class ComplianceHelper
{
/**
* Give the status for the given term for the given user.
*
* @param User $user
* @param Term $term
* @return bool
*/
public static function hasSignedGivenTerm(User $user, Term $term): bool
{
$termUser = DB::table('term_user')->where('user_id', $user->id)
->where('account_id', $user->account_id)
->where('term_id', $term->id)
->first();
if (! $termUser) {
return false;
}
return true;
}
/**
* Indicate if the user has accepted the most recent terms and privacy.
* This really is a shortcut of the `hasSignedGivenTerm` method.
*
* @param User $user
* @return bool
*/
public static function isCompliantWithCurrentTerm(User $user): bool
{
$latestTerm = Term::latest()->first();
return self::hasSignedGivenTerm($user, $latestTerm);
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace App\Helpers;
class ComposerScripts
{
const CONFIG = 'bootstrap/cache/config.php';
/**
* Handle the pre-install Composer event.
*
* @param mixed $event
* @return void
*
* @codeCoverageIgnore
*/
public static function preInstall($event)
{
try {
if (file_exists('vendor')) {
\Illuminate\Foundation\ComposerScripts::postInstall($event);
}
static::clear();
} catch (\Throwable $e) {
// catch all
}
}
/**
* Handle the pre-update Composer event.
*
* @param mixed $event
* @return void
*
* @codeCoverageIgnore
*/
public static function preUpdate($event)
{
try {
if (file_exists('vendor')) {
\Illuminate\Foundation\ComposerScripts::postUpdate($event);
}
static::clear();
} catch (\Throwable $e) {
// catch all
}
}
/**
* @codeCoverageIgnore
*/
protected static function clear()
{
if (file_exists(self::CONFIG)) {
\unlink(self::CONFIG); /** @phpstan-ignore-line */
}
}
}

View File

@@ -0,0 +1,203 @@
<?php
namespace App\Helpers;
use Rinvex\Country\Country;
use Rinvex\Country\CountryLoader;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\App;
class CountriesHelper
{
/**
* Get list of countries.
*
* @return \Illuminate\Support\Collection
*/
public static function getAll(): Collection
{
$x = collect(countries(true, true));
$countries = $x->map(function (Country $item) {
return [
'id' => $item->getIsoAlpha2(),
'country' => static::getCommonNameLocale($item),
];
});
return collect($countries->sortByCollator('country'));
}
/**
* Get country name.
*
* @param string $iso code of the country
* @return string common name (localized) of the country
*/
public static function get($iso): string
{
$country = self::getCountry($iso);
if (is_null($country)) {
return '';
}
return static::getCommonNameLocale($country);
}
/**
* Find a country by the (english) name of the country.
*
* @param string $name Common name of a country
* @return string iso_3166_1_alpha2 code of the country
*/
public static function find($name): string
{
$country = collect(CountryLoader::where('name.common', $name));
if ($country->count() === 0) {
$country = collect(CountryLoader::where('iso_3166_1_alpha2', mb_strtoupper($name)));
}
if ($country->count() === 0) {
return '';
}
return (new Country($country->first()))->getIsoAlpha2();
}
/**
* Get the common name of country, in locale version.
*
* @param \Rinvex\Country\Country $country
* @return string
*/
private static function getCommonNameLocale(Country $country): string
{
$locale = App::getLocale();
$lang = LocaleHelper::getLocaleAlpha($locale);
return $country->getTranslation($lang)['common'];
}
/**
* Get country for a specific iso code.
*
* @param string $iso
* @return \Rinvex\Country\Country|null the Country element
*/
public static function getCountry($iso): ?Country
{
$country = collect(CountryLoader::where('iso_3166_1_alpha2', mb_strtoupper($iso)));
if ($country->count() === 0) {
$country = collect(CountryLoader::where('alt_spellings', mb_strtoupper($iso)));
}
if ($country->count() === 0) {
return null;
}
return new Country($country->first());
}
/**
* Get country for a specific language.
*
* @param string $locale language code (iso)
* @return \Rinvex\Country\Country|null the Country element
*/
public static function getCountryFromLocale($locale): ?Country
{
$countryCode = LocaleHelper::extractCountry($locale);
if (empty($countryCode)) {
$countryCode = self::getDefaultCountryFromLocale($locale);
}
if (is_null($countryCode)) {
$lang = LocaleHelper::getLocaleAlpha($locale);
$country = collect(CountryLoader::where("languages.$lang", '>', '0'));
if ($country->count() === 0) {
return null;
}
} else {
$country = collect(CountryLoader::where('iso_3166_1_alpha2', mb_strtoupper($countryCode)));
}
return new Country($country->first());
}
/**
* Get default country for a language.
*
* @param string $locale language code (iso)
* @return string|null iso_3166_1_alpha2 code
*/
private static function getDefaultCountryFromLocale($locale): ?string
{
switch (mb_strtolower($locale)) {
case 'cs':
$country = 'CZ';
break;
case 'en':
$country = 'US';
break;
case 'he':
$country = 'IL';
break;
case 'zh':
$country = 'CN';
break;
case 'de':
case 'es':
case 'fr':
case 'hr':
case 'it':
case 'nl':
case 'pt':
case 'ru':
case 'tr':
$country = mb_strtoupper($locale);
break;
default:
$country = null;
break;
}
return $country;
}
/**
* Get default timezone for the country.
*
* @param mixed $country Country element
* @return string timezone fo this sountry
*/
public static function getDefaultTimezone($country): string
{
// https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
// https://en.wikipedia.org/wiki/List_of_time_zones_by_country
switch ($country->getIsoAlpha3()) {
case 'AUS':
$timezone = 'Australia/Melbourne';
break;
case 'CHN':
$timezone = 'Asia/Shanghai';
break;
case 'ESP':
$timezone = 'Europe/Madrid';
break;
case 'PRT':
$timezone = 'Europe/Lisbon';
break;
case 'RUS':
$timezone = 'Europe/Moscow';
break;
case 'CAN':
$timezone = 'America/Toronto';
break;
case 'USA':
$timezone = 'America/Chicago';
break;
default:
$timezone = collect($country->getTimezones())->first();
break;
}
return $timezone ?? config('app.timezone');
}
}

67
app/Helpers/DBHelper.php Normal file
View File

@@ -0,0 +1,67 @@
<?php
namespace App\Helpers;
use PDO;
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Connection;
class DBHelper
{
/**
* Get connection.
*
* @param string $name
* @return \Illuminate\Database\Connection
*/
public static function connection($name = null): Connection
{
return DB::connection($name);
}
/**
* Get the version of DB engine.
*
* @return string|null
*/
public static function version(): ?string
{
try {
return static::connection()->getPdo()->getAttribute(PDO::ATTR_SERVER_VERSION);
} catch (\Exception $e) {
return null;
}
}
/**
* Test if db version if greater than $version param.
*
* @param string $version
* @return bool
*/
public static function testVersion($version)
{
return version_compare(static::version(), $version) >= 0;
}
/**
* Get list of tables on this instance.
*
* @return array
*/
public static function getTables()
{
return DB::select('SELECT table_name as `table_name`
FROM information_schema.tables
WHERE table_schema = :table_schema
AND table_name LIKE :table_prefix', [
'table_schema' => static::connection()->getDatabaseName(),
'table_prefix' => '%'.static::connection()->getTablePrefix().'%',
]);
}
public static function getTable($name)
{
return '`'.static::connection()->getTablePrefix().$name.'`';
}
}

366
app/Helpers/DateHelper.php Normal file
View File

@@ -0,0 +1,366 @@
<?php
namespace App\Helpers;
use Carbon\Carbon;
use function Safe\date;
use function Safe\strtotime;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
class DateHelper
{
/**
* Creates a Carbon object from DateTime format.
* If timezone is given, it parse the date with this timezone.
* Always return a date with default timezone (UTC).
*
* @param \DateTime|Carbon|string|null $date
* @param string $timezone
* @return Carbon|null
*/
public static function parseDateTime($date, $timezone = null): ?Carbon
{
if (is_null($date)) {
return null;
}
if ($date instanceof Carbon) {
// ok
} elseif ($date instanceof \DateTimeInterface) {
$date = Carbon::instance($date);
} else {
try {
$date = Carbon::parse($date, $timezone);
} catch (\Exception $e) {
// Parse error
return null;
}
}
$appTimezone = config('app.timezone');
if ($date->timezone !== $appTimezone) {
$date->setTimezone($appTimezone);
}
return $date;
}
/**
* Creates a Carbon object from Date format.
* If timezone is given, it parse the date with this timezone.
* Always return a date with default timezone (UTC).
*
* @param Carbon|string $date
* @param string $timezone
* @return Carbon|null
*/
public static function parseDate($date, $timezone = null): ?Carbon
{
if (! $date instanceof Carbon) {
try {
$date = Carbon::parse($date);
} catch (\Exception $e) {
// Parse error
return null;
}
}
$date = Carbon::create($date->year, $date->month, $date->day, 0, 0, 0, $timezone ?? $date->timezone);
$appTimezone = config('app.timezone');
if ($date->timezone !== $appTimezone) {
$date->setTimezone($appTimezone);
}
return $date === false ? null : $date;
}
/**
* Return timestamp date format.
*
* @param Carbon|\App\Models\Instance\SpecialDate|string|null $date
* @return string|null
*/
public static function getTimestamp($date): ?string
{
if (is_null($date)) {
return null;
}
if ($date instanceof \App\Models\Instance\SpecialDate) {
$date = $date->date;
}
if (! $date instanceof Carbon) {
$date = Carbon::parse($date);
}
return $date->translatedFormat(config('api.timestamp_format'));
}
/**
* Return date timestamp format.
*
* @param Carbon|\App\Models\Instance\SpecialDate|string|null $date
* @return string|null
*/
public static function getDate($date): ?string
{
if (is_null($date)) {
return null;
}
if ($date instanceof \App\Models\Instance\SpecialDate) {
$date = $date->date;
}
if (! $date instanceof Carbon) {
$date = Carbon::parse($date);
}
return $date->translatedFormat(config('api.date_timestamp_format'));
}
/**
* Get the timezone of the current user, or null.
*
* @return string|null
*/
public static function getTimezone(): ?string
{
return Auth::check() ? Auth::user()->timezone : null;
}
/**
* Return a date in a short format like "Oct 29, 1981".
*
* @param Carbon $date
* @return string
*/
public static function getShortDate(Carbon $date): string
{
return self::formatDate($date, 'format.short_date_year');
}
/**
* Return a date in a full format like "October 29, 1981".
*
* @param Carbon $date
* @return string
*/
public static function getFullDate(Carbon $date): string
{
return self::formatDate($date, 'format.full_date_year');
}
/**
* Return the month of the date like "Oct", or "Dec".
*
* @param Carbon $date
* @return string
*/
public static function getShortMonth(Carbon $date): string
{
return self::formatDate($date, 'format.short_month');
}
/**
* Return the month and year of the date like "October 2010",
* or "March 2032".
*
* @param Carbon $date
* @return string
*/
public static function getFullMonthAndDate(Carbon $date): string
{
return self::formatDate($date, 'format.full_month_year');
}
/**
* Return the day of the date like "Mon", or "Wed".
*
* @param Carbon $date
* @return string
*/
public static function getShortDay(Carbon $date): string
{
return self::formatDate($date, 'format.short_day');
}
/**
* Return a date in a short format
* like "Oct 29".
*
* @param Carbon $date
* @return string
*/
public static function getShortDateWithoutYear(Carbon $date): string
{
return self::formatDate($date, 'format.short_date');
}
/**
* Return a date and the time according to the timezone of the user, in a short format
* like "Oct 29, 1981 19:32".
*
* @param Carbon $date
* @return string
*/
public static function getShortDateWithTime(Carbon $date): string
{
return self::formatDate($date, 'format.short_date_year_time', true);
}
/**
* Return a date in a given format.
*
* @param Carbon $date
* @param string $format
* @param bool $withTimezone
* @return string
*/
private static function formatDate(Carbon $date, string $format, bool $withTimezone = false): string
{
$format = trans($format, [], Carbon::getLocale());
if ($withTimezone) {
$date = $date->setTimezone(static::getTimezone());
}
return $date->translatedFormat($format) ?: '';
}
/**
* Add a given number of week/month/year to a date.
*
* @param Carbon $date the start date
* @param string $frequency week/month/year
* @param int $number the number of week/month/year to increment to
* @return Carbon
*/
public static function addTimeAccordingToFrequencyType(Carbon $date, string $frequency, int $number): Carbon
{
switch ($frequency) {
case 'week':
$date = $date->addWeeks($number);
break;
case 'month':
$date = $date->addMonths($number);
break;
default:
$date = $date->addYears($number);
break;
}
return $date;
}
/**
* Get the name of the month and year of a given date with a given number
* of months more.
*
* @param int $month
* @return string
*/
public static function getMonthAndYear(int $month): string
{
$date = Carbon::now(static::getTimezone())->addMonthsNoOverflow($month);
$format = trans('format.short_month_year', [], Carbon::getLocale());
return $date->translatedFormat($format) ?: '';
}
/**
* Gets the next theoritical billing date.
* This is used on the Upgrade page to tell the user when the next billing
* date would be if he subscribed.
*
* @param string $interval
* @return Carbon
*/
public static function getNextTheoriticalBillingDate(string $interval): Carbon
{
if ($interval == 'monthly') {
return now(static::getTimezone())->addMonth();
}
return now(static::getTimezone())->addYear();
}
/**
* Gets a list of all the year from min to max (0 is the current year).
*
* @param int $max
* @param int $min
* @return Collection
*/
public static function getListOfYears($max = 120, $min = 0): Collection
{
$years = collect([]);
$maxYear = now(static::getTimezone())->subYears($min)->year;
$minYear = now(static::getTimezone())->subYears($max)->year;
for ($year = $maxYear; $year >= $minYear; $year--) {
$years->push([
'id' => $year,
'name' => $year,
]);
}
return $years;
}
/**
* Gets a list of all the months in a year.
*
* @return Collection
*/
public static function getListOfMonths(): Collection
{
$months = collect([]);
$currentDate = Carbon::parse('2000-01-01');
$format = trans('format.full_month', [], Carbon::getLocale());
for ($month = 1; $month <= 12; $month++) {
$currentDate->month = $month;
$months->push([
'id' => $month,
'name' => mb_convert_case($currentDate->translatedFormat($format), MB_CASE_TITLE, 'UTF-8'),
]);
}
return $months;
}
/**
* Gets a list of all the days in a month.
*
* @return Collection
*/
public static function getListOfDays(): Collection
{
$days = collect([]);
for ($day = 1; $day <= 31; $day++) {
$days->push(['id' => $day, 'name' => $day]);
}
return $days;
}
/**
* Gets a list of all the hours in a day.
*
* @return Collection
*/
public static function getListOfHours(): Collection
{
$currentDate = Carbon::parse('2000-01-01 00:00:00');
$format = trans('format.full_hour', [], Carbon::getLocale());
$hours = collect([]);
for ($hour = 1; $hour <= 24; $hour++) {
$currentDate->hour = $hour;
$hours->push([
'id' => date('H:i', strtotime("$hour:00")),
'name' => $currentDate->translatedFormat($format),
]);
}
return $hours;
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Helpers;
use App\Models\User\User;
class FormHelper
{
/**
* Get the name order that will be used when rendered the Add/Edit forms
* about contacts.
*
* @param User $user
* @return string
*/
public static function getNameOrderForForms(User $user): string
{
$nameOrder = 'firstname';
switch ($user->name_order) {
case 'lastname_firstname':
case 'lastname_firstname_nickname':
case 'lastname_nickname_firstname':
case 'nickname_lastname_firstname':
$nameOrder = 'lastname';
break;
}
return $nameOrder;
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace App\Helpers;
use App\Models\Contact\Gender;
use App\Models\Account\Account;
use App\Models\Contact\Contact;
use Illuminate\Support\Collection;
class GenderHelper
{
/**
* Return a collection of genders.
*
* @return Collection
*/
public static function getGendersInput()
{
$genders = auth()->user()->account->genders->map(function (Gender $gender): array {
return [
'id' => $gender->id,
'name' => $gender->name,
];
});
$genders = CollectionHelper::sortByCollator($genders, 'name');
$genders->prepend(['id' => '', 'name' => trans('app.gender_no_gender')]);
return $genders;
}
/**
* Replaces a specific gender of all the contacts in the account with another
* gender.
*
* @param Account $account
* @param Gender $genderToDelete
* @param Gender $genderToReplaceWith
* @return bool
*/
public static function replace(Account $account, Gender $genderToDelete, Gender $genderToReplaceWith): bool
{
Contact::where('account_id', $account->id)
->where('gender_id', $genderToDelete->id)
->update(['gender_id' => $genderToReplaceWith->id]);
return true;
}
}

View File

@@ -0,0 +1,118 @@
<?php
namespace App\Helpers;
use Carbon\Carbon;
use function Safe\json_decode;
use App\Models\Account\Account;
use App\Models\Instance\Instance;
use App\Models\Settings\Currency;
use Illuminate\Support\Facades\DB;
use function Safe\file_get_contents;
class InstanceHelper
{
/**
* Get the number of paid accounts in the instance.
*
* @return int
*/
public static function getNumberOfPaidSubscribers()
{
return Account::where('stripe_id', '!=', null)->count();
}
/**
* Get the plan information for the given time period.
*
* @param string $timePeriod Accepted values: 'monthly', 'annual'
* @return array|null
*/
public static function getPlanInformationFromConfig(string $timePeriod): ?array
{
$timePeriod = strtolower($timePeriod);
if ($timePeriod != 'monthly' && $timePeriod != 'annual') {
return null;
}
$currency = Currency::where('iso', strtoupper(config('cashier.currency')))->first();
$amount = MoneyHelper::format(config('monica.paid_plan_'.$timePeriod.'_price'), $currency);
return [
'type' => $timePeriod,
'name' => config('monica.paid_plan_'.$timePeriod.'_friendly_name'),
'id' => config('monica.paid_plan_'.$timePeriod.'_id'),
'price' => config('monica.paid_plan_'.$timePeriod.'_price'),
'friendlyPrice' => $amount,
];
}
/**
* Get the plan information for the given time period.
*
* @param \Laravel\Cashier\Subscription $subscription
* @return array|null
*/
public static function getPlanInformationFromSubscription(\Laravel\Cashier\Subscription $subscription): ?array
{
try {
$stripeSubscription = $subscription->asStripeSubscription();
$plan = $stripeSubscription->plan;
} catch (\Stripe\Exception\ApiErrorException $e) {
$stripeSubscription = null;
$plan = null;
}
if (is_null($stripeSubscription) || is_null($plan)) {
return [
'type' => $subscription->stripe_price,
'name' => $subscription->name,
'id' => $subscription->stripe_id,
'price' => '?',
'friendlyPrice' => '?',
'nextBillingDate' => '',
];
}
$currency = Currency::where('iso', strtoupper($plan->currency))->first();
$amount = MoneyHelper::format($plan->amount, $currency);
return [
'type' => $plan->interval === 'month' ? 'monthly' : 'annual',
'name' => $subscription->name,
'id' => $plan->id,
'price' => $plan->amount,
'friendlyPrice' => $amount,
'nextBillingDate' => DateHelper::getFullDate(Carbon::createFromTimestamp($stripeSubscription->current_period_end)),
];
}
/**
* Get changelogs entries.
*
* @param int $limit
* @return array
*/
public static function getChangelogEntries($limit = null)
{
$json = public_path('changelog.json');
$changelogs = json_decode(file_get_contents($json), true)['entries'];
if ($limit) {
$changelogs = array_slice($changelogs, 0, $limit);
}
return $changelogs;
}
/**
* Check if the instance has at least one account.
*
* @return bool
*/
public static function hasAtLeastOneAccount(): bool
{
return DB::table('accounts')->count() > 0;
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Helpers;
use App\Models\User\User;
use App\Models\Journal\Day;
use Illuminate\Database\Eloquent\ModelNotFoundException;
class JournalHelper
{
/**
* Get the number of paid accounts in the instance.
*
* @param User $user
* @return bool
*/
public static function hasAlreadyRatedToday(User $user): bool
{
try {
Day::where('account_id', $user->account_id)
->where('date', now($user->timezone)->toDateString())
->firstOrFail();
} catch (ModelNotFoundException $e) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,210 @@
<?php
namespace App\Helpers;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Matriphe\ISO639\ISO639;
use function Safe\preg_match;
use function Safe\preg_split;
use Illuminate\Support\Facades\App;
use libphonenumber\PhoneNumberUtil;
use Illuminate\Support\Facades\Auth;
use libphonenumber\PhoneNumberFormat;
use libphonenumber\NumberParseException;
class LocaleHelper
{
private const LANG_SPLIT = '/(-|_)/';
/**
* Get the current or default locale.
*
* @return string
*/
public static function getLocale()
{
if (Auth::check()) {
$locale = Auth::user()->locale;
} else {
$locale = app('language.detector')->detect() ?: config('app.locale');
}
return $locale;
}
/**
* Get the current lang from locale.
*
* @return string lang, lowercase form.
*/
public static function getLang($locale = null)
{
if (is_null($locale)) {
$locale = App::getLocale();
}
if (preg_match(self::LANG_SPLIT, $locale)) {
$locale = preg_split(self::LANG_SPLIT, $locale, 2)[0];
}
return mb_strtolower($locale);
}
/**
* Get the current country from locale.
*
* @return string country, uppercase form.
*/
public static function getCountry($locale = null)
{
$countryCode = self::extractCountry($locale);
if (is_null($countryCode)) {
$country = CountriesHelper::getCountryFromLocale($locale);
$countryCode = $country->getIsoAlpha2();
}
return mb_strtoupper($countryCode);
}
/**
* Extract the current country from locale, i.e. 'en-US' will return 'US'.
* If no country is present in the locale, it will return null.
*
* @return string|null country, uppercase form.
*/
public static function extractCountry($locale = null): ?string
{
if (is_null($locale)) {
$locale = App::getLocale();
}
if (preg_match(self::LANG_SPLIT, $locale)) {
$locale = preg_split(self::LANG_SPLIT, $locale, 2)[1];
return mb_strtoupper($locale);
}
return null;
}
/**
* Get the list of avalaible languages.
*
* @return \Illuminate\Support\Collection
*/
public static function getLocaleList()
{
return collect(config('lang-detector.languages'))->map(function (string $lang): array {
return [
'lang' => $lang,
'name' => self::getLocaleName($lang),
'name-orig' => self::getLocaleName($lang, $lang),
];
});
}
/**
* Get the name of one language.
*
* @param string $lang
* @param string $locale
* @return string
*/
private static function getLocaleName($lang, $locale = null): string
{
$name = trans('settings.locale_'.$lang, [], $locale);
if ($name == 'settings.locale_'.$lang) {
// The name of the new language is not already set, even in english
$name = $lang;
}
return (string) Str::of($name);
}
/**
* Get the direction: left to right/right to left.
*
* @return string
*/
public static function getDirection()
{
$lang = self::getLang();
switch ($lang) {
// Source: https://meta.wikimedia.org/wiki/Template:List_of_language_names_ordered_by_code
case 'ar':
case 'arc':
case 'dv':
case 'fa':
case 'ha':
case 'he':
case 'khw':
case 'ks':
case 'ku':
case 'ps':
case 'ur':
case 'yi':
return 'rtl';
default:
return 'ltr';
}
}
/**
* Association ISO-639-1 => ISO-639-2.
*
* @var array<string,string>
*/
private static $locales = [];
/**
* Get ISO-639-2/t (three-letter codes) from ISO-639-1 (two-letters code).
*
* @param string $locale
* @return string
*/
public static function getLocaleAlpha($locale)
{
if (Arr::has(static::$locales, $locale)) {
return Arr::get(static::$locales, $locale);
}
$locale = mb_strtolower($locale);
$languages = (new ISO639)->allLanguages();
$lang = '';
foreach ($languages as $l) {
if ($l[0] == $locale) {
$lang = $l[1];
break;
}
}
static::$locales[$locale] = $lang;
return $lang;
}
/**
* Format phone number by country.
*
* @param string $tel
* @param string|null $iso
* @param int $format
* @return string
*/
public static function formatTelephoneNumberByISO(string $tel, $iso, int $format = PhoneNumberFormat::INTERNATIONAL): string
{
if (empty($iso)) {
return $tel;
}
try {
$phoneUtil = PhoneNumberUtil::getInstance();
$phoneInstance = $phoneUtil->parse($tel, mb_strtoupper($iso));
$tel = $phoneUtil->format($phoneInstance, $format);
} catch (NumberParseException $e) {
// Do nothing if the number cannot be parsed successfully
}
return $tel;
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Helpers;
use App\Models\User\User;
use App\Interfaces\MailNotification;
class MailHelper
{
/**
* Get the HTML view that is rendered by the default markdown Laravel
* notification class.
* Yes, this is weird, but it's the only way to do it (as of Laravel 5.7).
*
* @param MailNotification $notification
* @param User $user
* @return string
*/
public static function emailView($notification, $user)
{
$message = $notification->toMail($user);
$markdown = new \Illuminate\Mail\Markdown(view(), config('mail.markdown'));
return $markdown->render($message->markdown, $message->toArray());
}
}

139
app/Helpers/MoneyHelper.php Normal file
View File

@@ -0,0 +1,139 @@
<?php
namespace App\Helpers;
use Money\Money;
use App\Models\Settings\Currency;
use Illuminate\Support\Facades\App;
use Money\Currencies\ISOCurrencies;
use Illuminate\Support\Facades\Auth;
use Money\Currency as MoneyCurrency;
use Money\Parser\DecimalMoneyParser;
use Money\Formatter\IntlMoneyFormatter;
use Money\Formatter\DecimalMoneyFormatter;
class MoneyHelper
{
/**
* Format a monetary amount with currency symbol.
* The value is formatted using current langage, as per the currency symbol.
*
* If the currency parameter is not passed, then the currency specified in
* the users's settings will be used. If the currency setting is not
* defined, then the amount will be returned without a currency symbol.
*
* @param int|null $amount Amount value in storable format (ex: 100 for 1,00€).
* @param Currency|int|null $currency Currency of amount.
* @return string Formatted amount for display with currency symbol (ex '1,235.87 €').
*/
public static function format($amount, $currency = null): string
{
if (is_null($amount)) {
$amount = 0;
}
$currency = self::getCurrency($currency);
if (! $currency || ! $currency->iso) {
$numberFormatter = new \NumberFormatter(App::getLocale(), \NumberFormatter::DECIMAL);
return $numberFormatter->format($amount);
}
$moneyCurrency = new MoneyCurrency($currency->iso);
$money = new Money($amount, $moneyCurrency);
$numberFormatter = new \NumberFormatter(App::getLocale(), \NumberFormatter::CURRENCY);
$moneyFormatter = new IntlMoneyFormatter($numberFormatter, new ISOCurrencies());
return $moneyFormatter->format($money);
}
/**
* Format a monetary amount, without the currency.
* The value is formatted using current langage.
*
* @param int|null $amount Amount value in storable format (ex: 100 for 1,00€).
* @param Currency|int|null $currency
* @return string Formatted amount for display without currency symbol (ex: '1234.50').
*/
public static function getValue($amount, $currency = null): string
{
$currency = self::getCurrency($currency);
if (! $currency || ! $currency->iso) {
return (string) ($amount / 100);
}
$moneyCurrency = new MoneyCurrency($currency->iso);
$money = new Money($amount ?? 0, $moneyCurrency);
$numberFormatter = new \NumberFormatter(App::getLocale(), \NumberFormatter::PATTERN_DECIMAL);
$moneyFormatter = new IntlMoneyFormatter($numberFormatter, new ISOCurrencies());
return $moneyFormatter->format($money);
}
/**
* Parse a monetary exchange value as storable integer.
* Currency is used to know the precision of this currency.
*
* @param mixed|null $exchange Amount value in exchange format (ex: 1.00).
* @param Currency|int|null $currency
* @return int Amount as storable format (ex: 14500).
*/
public static function parseInput($exchange, $currency): int
{
$currency = self::getCurrency($currency);
if (! $currency || ! $currency->iso) {
return (int) ((float) $exchange * 100);
}
$moneyParser = new DecimalMoneyParser(new ISOCurrencies());
$money = $moneyParser->parse((string) $exchange, new MoneyCurrency($currency->iso));
return (int) $money->getAmount();
}
/**
* Format a monetary value as exchange value.
* Exchange value is the amount to be entered in an input by a user,
* using ordinary format.
*
* @param int|null $amount Amount value in storable format (ex: 100 for 1,00€).
* @param Currency|int|null $currency
* @return string Real value of amount in exchange format (ex: 1.24).
*/
public static function exchangeValue($amount, $currency): string
{
$currency = self::getCurrency($currency);
if (! $currency || ! $currency->iso) {
return (string) ($amount / 100);
}
$moneyCurrency = new MoneyCurrency($currency->iso);
$money = new Money($amount ?? 0, $moneyCurrency);
$moneyFormatter = new DecimalMoneyFormatter(new ISOCurrencies());
return $moneyFormatter->format($money);
}
/**
* Get currency object.
*
* @param Currency|int|null $currency
* @return Currency|null
*/
public static function getCurrency($currency): ?Currency
{
if (is_int($currency)) {
$currency = Currency::find($currency);
}
if (! $currency && Auth::check()) {
$currency = Auth::user()->currency;
}
return $currency;
}
}

View File

@@ -0,0 +1,104 @@
<?php
namespace App\Helpers;
use Vectorface\Whip\Whip;
use Illuminate\Support\Arr;
use OK\Ipstack\Client as Ipstack;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Request;
use Stevebauman\Location\Facades\Location;
class RequestHelper
{
/**
* Get client ip.
*
* @return array|string|null
*/
public static function ip()
{
$whip = new Whip();
$ip = $whip->getValidIpAddress();
if ($ip === false) {
$ip = Request::header('Cf-Connecting-Ip');
if (is_null($ip)) {
$ip = Request::ip();
}
}
return $ip;
}
/**
* Get client country.
*
* @param string $ip
* @return string|null
*/
public static function country($ip): ?string
{
$position = Location::get($ip);
return $position ? $position->countryCode : null;
}
/**
* Get client country and currency.
*
* @param string|null $ip
* @return array
*/
public static function infos($ip)
{
$ip = $ip ?? static::ip();
if (config('location.ipstack_apikey') != null) {
$ipstack = new Ipstack(config('location.ipstack_apikey'));
$position = $ipstack->get($ip, true);
if ($position !== null && Arr::get($position, 'country_code')) {
return [
'country' => Arr::get($position, 'country_code'),
'currency' => Arr::get($position, 'currency.code'),
'timezone' => Arr::get($position, 'time_zone.id'),
];
}
}
if (config('location.ipdata.token') != null) {
try {
$position = static::getIpData($ip);
return [
'country' => Arr::get($position, 'country_code'),
'currency' => Arr::get($position, 'currency.code'),
'timezone' => Arr::get($position, 'time_zone.name'),
];
} catch (\Exception $e) {
// skip
}
}
return [
'country' => static::country($ip),
'currency' => null,
'timezone' => null,
];
}
/**
* Get data from ipdata.
*
* @param string $ip
* @return array
*/
private static function getIpData(string $ip): array
{
$token = config('location.ipdata.token', '');
$url = "https://api.ipdata.co/{$ip}?api-key=".$token;
return Http::get($url)->throw()->json();
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace App\Helpers;
use function Safe\preg_match;
use App\Models\Contact\Contact;
use Illuminate\Support\Facades\Auth;
use App\Models\Contact\ContactFieldType;
use Illuminate\Database\Eloquent\Builder;
class SearchHelper
{
/**
* Search contacts by the given query.
*
* @param string $needle
* @param string $orderByColumn
* @param string $orderByDirection
* @param string|null $addressBookName
* @return Builder
*/
public static function searchContacts(string $needle, string $orderByColumn, string $orderByDirection = 'asc', string $addressBookName = null): Builder
{
$accountId = Auth::user()->account_id;
// match against `field: string` queries
if (preg_match('/(.{1,})[:](.{1,})/', $needle, $matches)) {
$search_field = $matches[1];
$search_term = $matches[2];
$field = ContactFieldType::where('account_id', $accountId)
->where('name', 'LIKE', $search_field)
->first();
$field_id = is_null($field) ? 0 : $field->id;
/** @var Builder */
$builder = Contact::whereHas('contactFields', function ($query) use ($accountId, $field_id, $search_term) {
$query->where([
['account_id', $accountId],
['data', 'like', "$search_term%"],
['contact_field_type_id', $field_id],
]);
});
return $builder->addressBook($accountId, $addressBookName)
->orderBy($orderByColumn, $orderByDirection);
}
return Contact::search($needle, $accountId, $orderByColumn, $orderByDirection)
->addressBook($accountId, $addressBookName);
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace App\Helpers;
use App\Models\Account\Account;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Illuminate\Filesystem\FilesystemAdapter;
class StorageHelper
{
/**
* Get a filesystem instance.
*
* @param string $name
* @return \Illuminate\Filesystem\FilesystemAdapter
*/
public static function disk($name = null): FilesystemAdapter
{
/** @var \Illuminate\Filesystem\FilesystemAdapter */
$disk = Storage::disk($name);
return $disk;
}
/**
* Get the storage size of the account, in bytes.
*
* @param Account $account
* @return int
*/
public static function getAccountStorageSize(Account $account): int
{
$documentsSize = DB::table('documents')
->where('account_id', $account->id)
->sum('filesize');
$photosSize = DB::table('photos')
->where('account_id', $account->id)
->sum('filesize');
return $documentsSize + $photosSize;
}
/**
* Indicates whether the account has the reached the maximum storage size.
*
* @param Account $account
* @return bool
*/
public static function hasReachedAccountStorageLimit(Account $account): bool
{
if (! config('monica.requires_subscription')) {
return false;
}
$currentAccountSize = self::getAccountStorageSize($account);
return $currentAccountSize > (config('monica.max_storage_size') * 1000000);
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Helpers;
class StringHelper
{
/**
* Test if string is null or whitespace.
*
* @param mixed $text
* @return bool
*/
public static function isNullOrWhitespace($text): bool
{
return ctype_space($text) || $text === '' || is_null($text);
}
}

View File

@@ -0,0 +1,126 @@
<?php
namespace App\Helpers;
use DateTimeZone;
class TimezoneHelper
{
/**
* Get a list of all timezones.
*
* @return array
*/
public static function getListOfTimezones(): array
{
$list = [];
$timezones = DateTimeZone::listIdentifiers();
if ($timezones !== false) {
foreach ($timezones as $timezone) {
[$tz, $name] = self::formatTimezone($timezone);
array_push($list, [
'id' => $tz,
'timezone' => $timezone,
'name' => $name,
]);
}
}
$collect = collect($list)
->groupBy('id')
->sortKeys();
$result = [];
foreach ($collect as $item) {
$values = $item->sortByCollator(function ($value) {
return $value['name'];
});
foreach ($values as $val) {
array_push($result, $val);
}
}
return $result;
}
/**
* Format a timezone to be displayed (english only).
*
* @param string $timezone
* @return array int value of the offset, string formatted timezone
*/
private static function formatTimezone($timezone): array
{
$dtimezone = new DateTimeZone($timezone);
$time = now($timezone);
$offset = $time->format('P');
$loc = $dtimezone->getLocation();
if ($timezone == 'UTC') {
$formatted = '(UTC) Universal Time Coordinated';
} else {
$name = $time->tzName;
$i = strpos($name, '/');
if ($i > 0) {
$name = substr($name, $i + 1);
}
$name = str_replace(['St_', '/', '_'], ['St. ', ', ', ' '], $name);
if (empty($loc['comments'])) {
$formatted = '(UTC '.$offset.') '.$name;
} else {
$formatted = '(UTC '.$offset.') '.$loc['comments'].' ('.$name.')';
}
}
$tz = str_replace(':', '', $offset);
$tz = intval($tz);
return [$tz, $formatted];
}
/**
* Equivalent timezone to convert deprecated timezone.
*
* @var array<string>
*
* @see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
*/
protected static $equivalentTimezone = [
'Australia/Canberra' => 'Australia/Sydney',
'Asia/Calcutta' => 'Asia/Kolkata',
'Asia/Chongqing' => 'Asia/Shanghai',
'Asia/Katmandu' => 'Asia/Kathmandu',
'Asia/Rangoon' => 'Asia/Yangon',
'Asia/Ulan_Bator' => 'Asia/Ulaanbaatar',
'Canada/Atlantic' => 'America/Halifax',
'Canada/Newfoundland' => 'America/St_Johns',
'Canada/Saskatchewan' => 'America/Regina',
'Etc/Greenwich' => 'UTC', // This is not an equivalent, but it the same zone
'Pacific/Samoa' => 'Pacific/Pago_Pago',
'US/Alaska' => 'America/Anchorage',
'US/Arizona' => 'America/Phoenix',
'US/Central' => 'America/Chicago',
'US/East-Indiana' => 'America/Indiana/Indianapolis',
'US/Eastern' => 'America/New_York',
'US/Mountain' => 'America/Denver',
];
/**
* Adjust a timezone with equivalent name (remove deprecated).
*
* @param string $timezone
* @return string
*/
public static function adjustEquivalentTimezone($timezone): string
{
if (array_key_exists($timezone, self::$equivalentTimezone)) {
return self::$equivalentTimezone[$timezone];
}
return $timezone;
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Helpers;
use Illuminate\Support\Arr;
use Sabre\VObject\Component\VCard;
class VCardHelper
{
/**
* Get country model object from given VCard file.
*
* @param VCard $vCard
* @return string|null
*/
public static function getCountryISOFromSabreVCard(VCard $vCard): ?string
{
$vCardAddress = $vCard->ADR;
if (empty($vCardAddress)) {
return null;
}
$country = Arr::get($vCardAddress->getParts(), '6');
return empty($country) ? null : CountriesHelper::find($country);
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace App\Helpers;
use App\Jobs\GetGPSCoordinate;
use App\Models\Account\Weather;
use App\Models\Contact\Address;
use App\Jobs\GetWeatherInformation;
use Illuminate\Support\Facades\Bus;
class WeatherHelper
{
/**
* Get the weather for the given address, if it exists.
*
* @param Address|null $address
* @return Weather|null
*/
public static function getWeatherForAddress($address): ?Weather
{
if (is_null($address)) {
return null;
}
$weather = $address->place->weathers()
->orderBy('created_at', 'desc')
->first();
// only get weather data if weather is either not existant or if is
// more than 6h old
if (is_null($weather) || ! $weather->created_at->between(now()->subHours(6), now())) {
self::callWeatherAPI($address);
}
return $weather;
}
/**
* Make the call to the weather service.
*
* @param Address $address
*/
private static function callWeatherAPI(Address $address): void
{
$jobs = [];
if (is_null($address->place->latitude)
&& config('monica.enable_geolocation') && ! is_null(config('monica.location_iq_api_key'))) {
$jobs[] = new GetGPSCoordinate($address->place);
}
if (config('monica.enable_weather') && ! is_null(config('monica.weatherapi_key'))) {
$jobs[] = new GetWeatherInformation($address->place);
}
Bus::batch($jobs)
->dispatch();
}
}

17
app/Helpers/helpers.php Normal file
View File

@@ -0,0 +1,17 @@
<?php
use App\Helpers\LocaleHelper;
if (! function_exists('htmldir')) {
/**
* Get the direction: left to right/right to left.
*
* @return string
*
* @see LocaleHelper::getDirection()
*/
function htmldir()
{
return LocaleHelper::getDirection();
}
}