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,69 @@
<?php
namespace App\Console\Commands;
use App\Models\Account\Account;
use Illuminate\Console\Command;
use App\Models\Instance\Statistic;
use Illuminate\Support\Facades\DB;
class CalculateStatistics extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'monica:calculatestatistics';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Calculate general usage statistics';
/**
* Execute the console command.
*
* @return void
*/
public function handle(): void
{
$statistic = new Statistic;
$statistic->number_of_users = DB::table('users')->count();
$statistic->number_of_contacts = DB::table('contacts')->count();
$statistic->number_of_notes = DB::table('notes')->count();
$statistic->number_of_reminders = DB::table('reminders')->count();
$statistic->number_of_tasks = DB::table('tasks')->count();
$statistic->number_of_invitations_sent = DB::table('accounts')->sum('number_of_invitations_sent');
// number_of_accounts_with_more_than_one_user
$number_of_accounts_with_more_than_one_user = 0;
foreach (Account::all() as $account) {
if ($account->users()->count() > 1) {
$number_of_accounts_with_more_than_one_user = $number_of_accounts_with_more_than_one_user + 1;
}
}
$statistic->number_of_accounts_with_more_than_one_user = $number_of_accounts_with_more_than_one_user;
$statistic->number_of_import_jobs = DB::table('import_jobs')->count();
$statistic->number_of_tags = DB::table('tags')->count();
$statistic->number_of_activities = DB::table('activities')->count();
$statistic->number_of_addresses = DB::table('addresses')->count();
$statistic->number_of_api_calls = DB::table('api_usage')->count();
$statistic->number_of_calls = DB::table('calls')->count();
$statistic->number_of_contact_fields = DB::table('contact_fields')->count();
$statistic->number_of_contact_field_types = DB::table('contact_field_types')->count();
$statistic->number_of_debts = DB::table('debts')->count();
$statistic->number_of_entries = DB::table('entries')->count();
$statistic->number_of_gifts = DB::table('gifts')->count();
$statistic->number_of_oauth_access_tokens = DB::table('oauth_access_tokens')->count();
$statistic->number_of_oauth_clients = DB::table('oauth_clients')->count();
$statistic->number_of_relationships = DB::table('relationships')->count();
$statistic->number_of_subscriptions = DB::table('subscriptions')->count();
$statistic->number_of_conversations = DB::table('conversations')->count();
$statistic->number_of_messages = DB::table('messages')->count();
$statistic->save();
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace App\Console\Commands;
use App\Models\User\SyncToken;
use Illuminate\Console\Command;
use App\Events\TokenDeleteEvent;
use App\Services\Instance\TokenClean;
use Illuminate\Support\Facades\Event;
class Clean extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'monica:clean
{--dry-run : Do everything except actually clean monica instance.}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Clean monica instance';
/**
* Execute the console command.
*
* @return void
*/
public function handle(): void
{
Event::listen(TokenDeleteEvent::class, function ($event) {
$this->handleTokenDelete($event->token);
});
app(TokenClean::class)->execute([
'dryrun' => (bool) $this->option('dry-run'),
]);
}
/**
* Handle TokenDeleteEvent event.
*
* @param SyncToken $token
*/
private function handleTokenDelete($token)
{
$this->info('Delete token '.$token->id.' - User '.$token->user_id.' - Type '.$token->name.' - timestamp '.$token->timestamp);
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace App\Console\Commands;
use App\Models\Account\Account;
use Illuminate\Console\Command;
use Illuminate\Console\ConfirmableTrait;
class CreateAccount extends Command
{
use ConfirmableTrait;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'account:create
{--email= : Login email for the account.}
{--password= : Password to set for the account.}
{--firstname= : First name for the account.}
{--lastname= : Last name for the account.}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a new account';
/**
* Missing argument errors. Exposed for testing.
*/
const ERROR_MISSING_EMAIL = '! You must specify an email';
const ERROR_MISSING_PASSWORD = '! You must specify a password';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$email = $this->option('email');
if (empty($email)) {
$this->error($this::ERROR_MISSING_EMAIL);
}
$password = $this->option('password');
if (empty($password)) {
$this->error($this::ERROR_MISSING_PASSWORD);
}
$firstName = $this->option('firstname') ?? 'John';
$lastName = $this->option('lastname') ?? 'Doe';
if (empty($email) || empty($password)) {
return;
}
if ($this->confirmToProceed('This will create a new user for '.$firstName.' '.$lastName.' with email '.$email)) {
Account::createDefault($firstName, $lastName, $email, $password);
$this->info('| You can now sign in to your account:');
$this->line('| username: '.$email);
$this->line('| password: <hidden>');
}
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace App\Console\Commands;
use Carbon\Carbon;
use Illuminate\Console\Command;
use App\Jobs\SynchronizeAddressBooks;
use App\Models\Account\AddressBookSubscription;
class DavClientsUpdate extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'monica:davclients';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Update all dav subscriptions';
/**
* Execute the console command.
*
* @return void
*/
public function handle()
{
$subscriptions = AddressBookSubscription::active()->get();
$now = now();
$subscriptions->filter(function ($subscription) use ($now) {
return $this->isTimeToRunSync($subscription, $now);
})->each(function ($subscription) {
SynchronizeAddressBooks::dispatch($subscription);
});
}
/**
* Test if the last synchronized timestamp is older than the subscription's frequency time.
*
* @param AddressBookSubscription $subscription
* @param Carbon $now
* @return bool
*/
private function isTimeToRunSync(AddressBookSubscription $subscription, Carbon $now): bool
{
return is_null($subscription->last_synchronized_at)
|| $subscription->last_synchronized_at->addMinutes($subscription->frequency)->lessThan($now);
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace App\Console\Commands;
use App\Models\User\User;
use Illuminate\Console\Command;
class Deactivate2FA extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = '2fa:deactivate
{--force : Force the operation to run when in production.}
{--email= : The email of the user to deactivate 2FA.}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Deactivate 2FA for this user';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
// retrieve the email from the option
$email = $this->option('email');
// if no email was passed to the option, prompt the user to enter the email
if (! $email) {
$email = $this->ask('what is the user\'s email?');
}
// retrieve the user with the specified email
$user = User::where('email', $email)->first();
if (! $user) {
// show an error and exist if the user does not exist
$this->error('No user with that email.');
return;
}
if (is_null($user->google2fa_secret)) {
// show an error and exist if the user does not exist
$this->error('2FA is currently not activated for this user.');
return;
}
// Print a warning
$this->info('2FA will be deactivated for '.$user->email);
$this->info('This action can\'t be cancelled.');
// ask for confirmation if not forced
if (! $this->option('force') && ! $this->confirm('Do you wish to continue?')) {
return;
}
// remove google2fa_secret key
$user->google2fa_secret = null;
// save the user
$user->save();
// show the new secret key
$this->info('2FA has been deactivated for '.$user->email);
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Console\Commands;
use App\Jobs\ExportAllAsSQL;
use Illuminate\Console\Command;
class ExportAll extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'export:all';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Exports all data as SQL to storage/app';
/**
* Execute the console command.
*
* @return void
*/
public function handle(): void
{
$job = new ExportAllAsSQL();
$this->info('Exported as '.$job->handle().'');
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class GetVersion extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'monica:getversion';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Get current version of monica';
/**
* Execute the console command.
*
* @return void
*/
public function handle(): void
{
$this->line(config('monica.app_version'));
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace App\Console\Commands\Helpers;
use Tests\Helpers\CommandCallerFake;
/**
* @method static void exec(\Illuminate\Console\Command $command, string $message, string $commandline)
* @method static void artisan(\Illuminate\Console\Command $command, string $message, string $commandline, array $arguments)
*/
class Command
{
/**
* Switch to a fake executor for testing purpose.
*
* @return CommandCallerContract
*/
public static function fake(): CommandCallerContract
{
static::setBackend($fake = app(CommandCallerFake::class));
return $fake;
}
/**
* The Command Executor.
*
* @var CommandCallerContract|null
*/
private static $commandCaller;
/**
* Get the current backend command.
*
* @return CommandCallerContract
*/
private static function getBackend(): CommandCallerContract
{
if (! static::$commandCaller) {
static::$commandCaller = app(CommandCaller::class); // @codeCoverageIgnore
}
return static::$commandCaller;
}
/**
* Set the current backend command.
*
* @param CommandCallerContract $executor
*/
public static function setBackend(CommandCallerContract $executor): void
{
static::$commandCaller = $executor;
}
/**
* Handle dynamic, static calls to the object.
*
* @param string $method
* @param array $args
* @return mixed
*
* @throws \RuntimeException
*/
public static function __callStatic($method, $args)
{
$instance = static::getBackend();
return $instance->$method(...$args);
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Console\Commands\Helpers;
use function Safe\exec;
use Illuminate\Console\Command;
use Illuminate\Console\Application;
use Symfony\Component\Console\Output\OutputInterface;
class CommandCaller implements CommandCallerContract
{
/**
* Print a message on the console, then execute a command.
*
* @param Command $command Laravel command context
* @param string $message Message to output
* @param string $commandline Command to execute
*
* @codeCoverageIgnore
*/
public function exec(Command $command, string $message, string $commandline): void
{
$command->info($message);
$command->line($commandline, null, OutputInterface::VERBOSITY_VERBOSE);
exec($commandline.' 2>&1', $output);
foreach ($output as $line) {
$command->line($line, null, OutputInterface::VERBOSITY_VERY_VERBOSE);
}
$command->line('', null, OutputInterface::VERBOSITY_VERBOSE);
}
/**
* Print a message on the console, then execute an artisan command.
*
* @param Command $command Laravel command context
* @param string $message Message to output
* @param string $commandline Artisan command name to execute
* @param array $arguments Optional arguments to pass to the artisan command
*
* @codeCoverageIgnore
*/
public function artisan(Command $command, string $message, string $commandline, array $arguments = []): void
{
$info = '';
foreach ($arguments as $key => $value) {
if (is_string($key)) {
$info .= ' '.$key.'="'.$value.'"';
} else {
$info .= ' '.$value;
}
}
$this->exec($command, $message, Application::formatCommandString($commandline.$info));
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Console\Commands\Helpers;
use Illuminate\Console\Command;
interface CommandCallerContract
{
/**
* Print a message on the console, then execute a command.
*
* @param Command $command Laravel command context
* @param string $message Message to output
* @param string $commandline Command to execute
*/
public function exec(Command $command, string $message, string $commandline): void;
/**
* Print a message on the console, then execute an artisan command.
*
* @param Command $command Laravel command context
* @param string $message Message to output
* @param string $commandline Artisan command name to execute
* @param array $arguments Optional arguments to pass to the artisan command
*/
public function artisan(Command $command, string $message, string $commandline, array $arguments = []): void;
}

View File

@@ -0,0 +1,134 @@
<?php
namespace App\Console\Commands;
use App\Models\Account\Account;
use Illuminate\Console\Command;
class ImportAccounts extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'account:import_ldap
{--ldap_uri= : LDAP URI.}
{--ldap_user= : LDAP Bind DN.}
{--ldap_pass= : LDAP Bind Password.}
{--ldap_base= : LDAP base DN for searching.}
{--ldap_filter= : Filter to search for user accounts.}
{--ldap_attr_mail= : LDAP attribute to map to email (default: mail).}
{--ldap_attr_firstname= : LDAP attribute to map to firstname (default: gn).}
{--ldap_attr_lastname= : LDAP attribute to map to lastname (default: sn).}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Import user accounts from LDAP';
/**
* Missing argument errors. Exposed for testing.
*/
const ERROR_MISSING_LDAP_FILTER = '! You must specify an LDAP Filter';
const ERROR_MISSING_LDAP_BASE = '! You must specify an LDAP Base';
const ERROR_MISSING_LDAP_USER = '! You must specify an LDAP User';
const ERROR_MISSING_LDAP_PASS = '! You must specify an LDAP Password';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$ldap_uri = $this->option('ldap_uri') ?? '127.0.0.1';
$ldap_attr_mail = $this->option('ldap_attr_mail') ?? 'mail';
$ldap_attr_firstname = $this->option('ldap_attr_firstname') ?? 'givenName';
$ldap_attr_lastname = $this->option('ldap_attr_lastname') ?? 'sn';
$ldap_user = $this->option('ldap_user');
if (empty($ldap_user)) {
$this->error($this::ERROR_MISSING_LDAP_USER);
}
$ldap_pass = $this->option('ldap_pass');
if (empty($ldap_pass)) {
$this->error($this::ERROR_MISSING_LDAP_PASS);
}
$ldap_base = $this->option('ldap_base');
if (empty($ldap_base)) {
$this->error($this::ERROR_MISSING_LDAP_BASE);
}
$ldap_filter = $this->option('ldap_filter');
if (empty($ldap_filter)) {
$this->error($this::ERROR_MISSING_LDAP_FILTER);
}
if (empty($ldap_user) || empty($ldap_pass) || empty($ldap_base) || empty($ldap_filter)) {
return;
}
$ldap_conn = ldap_connect($ldap_uri);
if (! $ldap_conn) {
$this->error('Could not connect to LDAP URI');
return;
}
if (! ldap_set_option($ldap_conn, LDAP_OPT_PROTOCOL_VERSION, 3)) {
$this->error('Could not set LDAP protocol v3');
return false;
}
try {
$bind = ldap_bind($ldap_conn, $ldap_user, $ldap_pass);
if (! $bind) {
$this->error('Could not bind with given LDAP credentials');
return;
}
} catch (\Exception $e) {
$this->error($e->getMessage());
return;
}
$ldap_res = [];
try {
$ldap_res = ldap_search($ldap_conn, $ldap_base, $ldap_filter, [$ldap_attr_mail, $ldap_attr_firstname, $ldap_attr_lastname]);
} catch (\Exception $e) {
$this->error($e->getMessage());
return;
}
$ldap_data = ldap_get_entries($ldap_conn, $ldap_res);
for ($i = 0; $i < $ldap_data['count']; $i++) {
if (! (isset($ldap_data[$i][$ldap_attr_mail]) && $ldap_data[$i][$ldap_attr_mail]['count'] > 0)) {
continue;
}
$user_mail = $ldap_data[$i][$ldap_attr_mail][0];
$user_firstname = 'John';
$user_lastname = 'Doe';
$user_password = bin2hex(random_bytes(64));
if (isset($ldap_data[$i][$ldap_attr_firstname]) && $ldap_data[$i][$ldap_attr_firstname]['count'] > 0) {
$user_firstname = $ldap_data[$i][$ldap_attr_firstname][0];
}
if (isset($ldap_data[$i][$ldap_attr_lastname]) && $ldap_data[$i][$ldap_attr_lastname]['count'] > 0) {
$user_lastname = $ldap_data[$i][$ldap_attr_lastname][0];
}
$this->info('Importing user "'.$user_mail.'"');
try {
Account::createDefault($user_firstname, $user_lastname, $user_mail, $user_password);
} catch (\Exception $import_error) {
$this->warn('Could not import user "'.$user_mail.'": '.$import_error->getMessage());
}
}
}
}

View File

@@ -0,0 +1,249 @@
<?php
namespace App\Console\Commands;
use function Safe\fopen;
use App\Models\User\User;
use function Safe\fclose;
use App\Helpers\DateHelper;
use App\Models\Contact\Gender;
use App\Models\Contact\Contact;
use Illuminate\Console\Command;
use App\Models\Contact\ContactField;
use App\Models\Contact\ContactFieldType;
use App\Jobs\Avatars\GetAvatarsFromInternet;
use App\Services\Contact\Address\CreateAddress;
use App\Services\Contact\Reminder\CreateReminder;
class ImportCSV extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'import:csv {user : email or id of a user} {file : path to the CSV file} {--format=google}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Imports CSV in Google format to user account';
/**
* The contact field email object.
*
* @var int|null
*/
public $contactFieldEmailId;
/**
* The contact field phone object.
*
* @var int|null
*/
public $contactFieldPhoneId;
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$file = $this->argument('file');
if (is_numeric($this->argument('user'))) {
$user = User::find($this->argument('user'));
} else {
$user = User::where('email', $this->argument('user'))->first();
}
if (! $user) {
$this->error('You need to provide a valid User ID or email address!');
return -1;
}
if (! file_exists($file)) {
$this->error('You need to provide a valid file path.');
return -2;
}
if (is_string($file)) {
$this->info("Importing CSV file {$file} to user {$user->id}");
}
// create special gender for this import
// we don't know which gender all the contacts are, so we need to create a special status for them, as we
// can't guess whether they are men, women or else.
$gender = Gender::where('name', config('dav.default_gender'))->first();
if (! $gender) {
$gender = new Gender;
$gender->account_id = $user->account_id;
$gender->name = config('dav.default_gender');
$gender->save();
}
$first = true;
$imported = 0;
$handle = fopen($file, 'r');
try {
while (($data = fgetcsv($handle)) !== false) { /** @phpstan-ignore-line */
// don't import the columns
if ($first) {
$first = false;
continue;
}
// if first & last name do not exist skip row
if (empty($data[1]) && empty($data[3])) {
continue;
}
$this->csvToContact($data, $user->account_id, $gender->id);
$imported++;
}
} finally {
fclose($handle);
}
$this->info("Imported {$imported} Contacts");
}
/**
* Create contact.
*/
private function csvToContact($data, $account_id, $gender_id)
{
$contact = new Contact();
$contact->account_id = $account_id;
$contact->gender_id = $gender_id;
if (! empty($data[1])) {
$contact->first_name = $data[1]; // Given Name
}
if (! empty($data[2])) {
$contact->middle_name = $data[2]; // Additional Name
}
if (! empty($data[3])) {
$contact->last_name = $data[3]; // Family Name
}
$street = null;
if (! empty($data[49])) {
$street = $data[49]; // address 1 street
}
$city = null;
if (! empty($data[50])) {
$city = $data[50]; // address 1 city
}
$province = null;
if (! empty($data[52])) {
$province = $data[52]; // address 1 region (state)
}
$postalCode = null;
if (! empty($data[53])) {
$postalCode = $data[53]; // address 1 postal code (zip) 53
}
if (! empty($data[66])) {
$contact->job = $data[66]; // organization 1 name 66
}
$contact->setAvatarColor();
$contact->save();
if (! empty($data[28])) {
// Email 1 Value
ContactField::firstOrCreate([
'account_id' => $contact->account_id,
'contact_id' => $contact->id,
'data' => $data[28],
'contact_field_type_id' => $this->contactFieldEmailId(),
]);
}
if ($postalCode || $province || $street || $city) {
$request = [
'account_id' => $contact->account_id,
'contact_id' => $contact->id,
'street' => $street,
'city' => $city,
'province' => $province,
'postal_code' => $postalCode,
];
app(CreateAddress::class)->execute($request);
}
if (! empty($data[42])) {
// Phone 1 Value
ContactField::firstOrCreate([
'account_id' => $contact->account_id,
'contact_id' => $contact->id,
'data' => $data[42],
'contact_field_type_id' => $this->contactFieldPhoneId(),
]);
}
if (! empty($data[14])) {
$birthdate = DateHelper::parseDate($data[14]);
$specialDate = $contact->setSpecialDate('birthdate', $birthdate->year, $birthdate->month, $birthdate->day);
app(CreateReminder::class)->execute([
'account_id' => $contact->account_id,
'contact_id' => $contact->id,
'initial_date' => DateHelper::getDate($specialDate),
'frequency_type' => 'year',
'frequency_number' => 1,
'title' => trans(
'people.people_add_birthday_reminder',
['name' => $contact->first_name]
),
'delible' => false,
]);
}
GetAvatarsFromInternet::dispatch($contact);
}
/**
* Get the default contact field email id for the account.
*
* @return int
*/
private function contactFieldEmailId()
{
if (! $this->contactFieldEmailId) {
$contactFieldType = ContactFieldType::where('type', 'email')->first();
$this->contactFieldEmailId = $contactFieldType->id;
}
return $this->contactFieldEmailId;
}
/**
* Get the default contact field phone id for the account.
*
* @return int
*/
private function contactFieldPhoneId()
{
if (! $this->contactFieldPhoneId) {
$contactFieldType = ContactFieldType::where('type', 'phone')->first();
$this->contactFieldPhoneId = $contactFieldType->id;
}
return $this->contactFieldPhoneId;
}
}

View File

@@ -0,0 +1,116 @@
<?php
namespace App\Console\Commands;
use App\Models\User\User;
use Illuminate\Http\File;
use Illuminate\Console\Command;
use App\Jobs\AddContactFromVCard;
use App\Models\Account\ImportJob;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Facades\Storage;
class ImportVCards extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'import:vcard
{--user= : user to import the contacts}
{--path= : path of the file to import}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Imports contacts from vCard files for a specific user';
/**
* Execute the console command.
*
* @param Filesystem $filesystem
* @return mixed
*/
public function handle(Filesystem $filesystem)
{
$email = $this->option('user');
// if no email was passed to the option, prompt the user to enter the email
if (! $email) {
$email = $this->ask('what is the user\'s email?');
}
// retrieve the user with the specified email
$user = User::where('email', $email)->first();
if (! $user) {
// show an error and exist if the user does not exist
$this->error('No user with that email.');
return -1;
}
$path = $this->option('path');
// if no email was passed to the option, prompt the user to enter the email
if (! $path) {
$path = $this->ask('what file you want to import?');
}
if (! $filesystem->exists($path) || ! $this->acceptedExtensions($filesystem, $path)) {
$this->error('The provided vcard file was not found or is not valid!');
return -2;
}
$importJob = $this->import($path, $user);
return $this->report($importJob) ? 0 : 1;
}
private function acceptedExtensions(Filesystem $filesystem, string $path): bool
{
switch ($filesystem->extension($path)) {
case 'vcf':
case 'vcard':
return true;
default:
return false;
}
}
private function import(string $path, User $user): ImportJob
{
$pathName = Storage::putFile('public', new File($path));
$importJob = $user->account->importjobs()->create([
'user_id' => $user->id,
'type' => 'vcard',
'filename' => $pathName,
]);
AddContactFromVCard::dispatchSync($importJob);
return $importJob;
}
private function report(ImportJob $importJob)
{
$importJob->refresh();
if ($importJob->failed) {
$this->warn('Error: '.$importJob->failed_reason);
return false;
}
$this->info('Contacts found: '.$importJob->contacts_found);
$this->info('Contacts skipped: '.$importJob->contacts_skipped);
$this->info('Contacts imported: '.$importJob->contacts_imported);
return true;
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Foundation\Inspiring;
class Inspire extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'inspire';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Display an inspiring quote';
/**
* Execute the console command.
*
* @return void
*/
public function handle(): void
{
$this->comment(PHP_EOL.Inspiring::quote().PHP_EOL);
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Console\Commands;
use DirectoryIterator;
use Illuminate\Console\Command;
class LangGenerate extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'lang:generate';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Generate i18n json assets';
/**
* Execute the console command.
*
* @return void
*/
public function handle(): void
{
$dirs = new DirectoryIterator(resource_path('lang').'/');
foreach ($dirs as $dir) {
if (! $dir->isDir()) {
continue;
}
$lang = $dir->getFilename();
if ($lang == '.' || $lang == '..') {
continue;
}
$this->call('lang:js', [
'--json' => true,
'--source' => $dir->getPathname(),
'target' => 'public/js/langs/'.$lang.'.json',
]);
}
}
}

View File

@@ -0,0 +1,121 @@
<?php
namespace App\Console\Commands;
use App\Helpers\DBHelper;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Console\ConfirmableTrait;
class MigrateDatabaseCollation extends Command
{
use ConfirmableTrait;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'migrate:collation
{--force : Force the operation to run when in production.}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Update database for utf8mb4 collation';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if ($this->confirmToProceed()) {
try {
$connection = DBHelper::connection();
if ($connection->getDriverName() != 'mysql') {
return;
}
$databasename = $connection->getDatabaseName();
$schemata = $connection->table('information_schema.schemata')
->select('DEFAULT_CHARACTER_SET_NAME')
->where('schema_name', '=', $databasename)
->get();
$schema = $schemata->first()->DEFAULT_CHARACTER_SET_NAME;
if (config('database.use_utf8mb4') && $schema == 'utf8') {
$this->line('Migrate to utf8mb4 schema collation');
$this->toUtf8mb4($connection, $databasename);
} elseif (! config('database.use_utf8mb4') && $schema == 'utf8mb4') {
$this->line('Migrate to utf8 schema collation');
$this->toUtf8($connection, $databasename);
} else {
$this->info('Nothing to migrate, everything is ok.');
}
} catch (\Exception $e) {
$this->error(' ');
$this->error(' Check if the DB_USE_UTF8MB4 variable in .env file is correctly set ');
$this->error(' ');
$this->info('');
throw $e;
}
}
}
/**
* Switch to utf8mb4.
*
* @param \Illuminate\Database\Connection $connection
* @param string $databasename
*/
private function toUtf8mb4($connection, $databasename)
{
// Tables
$tables = $connection->table('information_schema.tables')
->select('table_name')
->where('table_schema', '=', $databasename)
->get();
foreach ($tables as $table) {
DB::statement('ALTER TABLE `'.$table->table_name.'` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;');
}
// Database
$pdo = $connection->getPdo();
$pdo->setAttribute(\PDO::ATTR_EMULATE_PREPARES, true);
DB::statement('ALTER DATABASE `'.$databasename.'` CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;');
$pdo->setAttribute(\PDO::ATTR_EMULATE_PREPARES, false);
}
/**
* Switch to utf8.
*
* @param \Illuminate\Database\Connection $connection
* @param string $databasename
*/
private function toUtf8($connection, $databasename)
{
// Tables
$tables = $connection->table('information_schema.tables')
->select('table_name')
->where('table_schema', '=', $databasename)
->get();
foreach ($tables as $table) {
DB::statement('ALTER TABLE `'.$table->table_name.'` CONVERT TO CHARACTER SET utf8 COLLATE utf8_unicode_ci;');
}
// Database
$pdo = $connection->getPdo();
$pdo->setAttribute(\PDO::ATTR_EMULATE_PREPARES, true);
DB::statement('ALTER DATABASE `'.$databasename.'` CHARACTER SET = utf8 COLLATE = utf8_unicode_ci;');
$pdo->setAttribute(\PDO::ATTR_EMULATE_PREPARES, false);
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace App\Console\Commands;
use App\Models\User\User;
use Illuminate\Console\Command;
use App\Jobs\SynchronizeAddressBooks;
use App\Services\DavClient\CreateAddressBookSubscription;
class NewAddressBookSubscription extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'monica:newaddressbooksubscription
{--email= : Monica account to add subscription to}
{--url= : CardDAV url of the address book}
{--login= : Login}
{--password= : Password of the account}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Add a new all dav subscriptions';
/**
* Execute the console command.
*
* @return void
*/
public function handle()
{
$user = User::where('email', $this->option('email'))->firstOrFail();
$url = $this->option('url') ?? $this->ask('url', 'CardDAV url of the address book');
$login = $this->option('login') ?? $this->ask('login', 'Login name');
$password = $this->option('password') ?? $this->ask('password', 'User password');
try {
$addressBookSubscription = app(CreateAddressBookSubscription::class)->execute([
'account_id' => $user->account_id,
'user_id' => $user->id,
'base_uri' => $url,
'username' => $login,
'password' => $password,
]);
} catch (\Exception $e) {
$this->error($e->getMessage());
}
if (! isset($addressBookSubscription)) {
$this->error('Could not add subscription');
} else {
$this->info('Subscription added');
SynchronizeAddressBooks::dispatch($addressBookSubscription, true);
}
}
}

View File

@@ -0,0 +1,137 @@
<?php
namespace App\Console\Commands\OneTime;
use App\Models\Contact\Contact;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
use Illuminate\Console\ConfirmableTrait;
use Symfony\Component\Console\Output\OutputInterface;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
class MoveAvatars extends Command
{
use ConfirmableTrait;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'monica:moveavatars
{--force : Force the operation to run when in production.}
{--dryrun : Simulate the execution but not write anything.}
{--storage= : new storage to move avatars to}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Move avatars from their current storage to current default storage';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if (! $this->confirmToProceed()) {
return;
}
Contact::where('has_avatar', true)
->chunk(200, function ($contacts) {
$this->handleContacts($contacts);
});
}
private function handleContacts($contacts)
{
foreach ($contacts as $contact) {
if ($contact->avatar_location == $this->newStorage()) {
continue;
}
try {
$this->handleOneContact($contact);
} catch (FileNotFoundException $e) {
continue;
}
}
}
private function handleOneContact($contact)
{
// move avatars to new location
$this->moveContactAvatars($contact);
if (! $this->option('dryrun')) {
$contact->deleteAvatars();
$this->line(' Files deleted from old location.', null, OutputInterface::VERBOSITY_VERBOSE);
// Update location. The filename has not changed.
$contact->avatar_location = $this->newStorage();
$contact->save();
}
}
private function moveContactAvatars($contact)
{
$this->line('Contact id:'.$contact->id.' | Avatar location:'.$contact->avatar_location.' | File name:'.$contact->avatar_file_name);
$avatarFileNames = [];
array_push($avatarFileNames, $this->getFileName($contact));
array_push($avatarFileNames, $this->getFileName($contact, 110));
array_push($avatarFileNames, $this->getFileName($contact, 174));
$storage = Storage::disk($contact->avatar_location);
$newStorage = Storage::disk($this->newStorage());
foreach ($avatarFileNames as $avatarFileName) {
if ($newStorage->exists($avatarFileName)) {
$this->line(' File already pushed: '.$avatarFileName, null, OutputInterface::VERBOSITY_VERBOSE);
continue;
}
if (! $this->option('dryrun')) {
$avatarFile = $storage->get($avatarFileName);
$newStorage->put($avatarFileName, $avatarFile, config('filesystems.default_visibility'));
}
$this->line(' File pushed: '.$avatarFileName, null, OutputInterface::VERBOSITY_VERBOSE);
}
}
private function getFileName($contact, $size = null)
{
$filename = pathinfo($contact->avatar_file_name, PATHINFO_FILENAME);
$extension = pathinfo($contact->avatar_file_name, PATHINFO_EXTENSION);
$avatarFileName = 'avatars/'.$filename.'.'.$extension;
if (! is_null($size)) {
$avatarFileName = 'avatars/'.$filename.'_'.$size.'.'.$extension;
}
if ($this->fileExists($contact->avatar_location, $avatarFileName)) {
return $avatarFileName;
}
}
private function fileExists($storage, $avatarFileName): bool
{
$storage = Storage::disk($storage);
if (! $storage->exists($avatarFileName)) {
$this->line(' ! File not found: '.$avatarFileName, null, OutputInterface::VERBOSITY_VERBOSE);
throw new FileNotFoundException();
}
return true;
}
private function newStorage()
{
return $this->option('storage') ?? config('filesystems.default');
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace App\Console\Commands\OneTime;
use App\Events\MoveAvatarEvent;
use App\Models\Contact\Contact;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Event;
use Illuminate\Console\ConfirmableTrait;
use App\Exceptions\FileNotFoundException;
use Symfony\Component\Console\Output\OutputInterface;
use App\Jobs\Avatars\MoveContactAvatarToPhotosDirectory;
/**
* This command moves current avatars to the new Photos directory and converts
* each avatar to a Photo object.
*/
class MoveAvatarsToPhotosDirectory extends Command
{
use ConfirmableTrait;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'monica:moveavatarstophotosdirectory
{--force : Force the operation to run when in production.}
{--dryrun : Simulate the execution but not write anything.}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Move avatars to the Photos directory, and create a photo object for each one of them';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if (! $this->confirmToProceed()) {
return;
}
Event::listen(MoveAvatarEvent::class, function ($event) {
$this->handleEvent($event->contact);
});
$delay = now();
Contact::where('has_avatar', true)
->chunk(100, function ($contacts) use ($delay) {
foreach ($contacts as $contact) {
if ($contact->avatar_source === 'default') {
$this->handleContact($contact, $delay);
}
}
// add some delay, so we treat 100 contacts each minutes
$delay = $delay->addMinutes(1);
});
}
private function handleContact($contact, $delay)
{
try {
if ($this->option('dryrun')) {
MoveContactAvatarToPhotosDirectory::dispatchNow($contact, true);
} else {
MoveContactAvatarToPhotosDirectory::dispatch($contact, false)
->delay($delay);
}
} catch (FileNotFoundException $e) {
$this->warn(' ! File not found: '.$e->fileName, OutputInterface::VERBOSITY_VERBOSE);
}
}
private function handleEvent($contact)
{
$this->info('Contact id:'.$contact->id.' | Avatar location:'.$contact->avatar_location.' | File name:'.$contact->avatar_file_name);
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Console\ConfirmableTrait;
use Laravel\Passport\PersonalAccessClient;
use Symfony\Component\Console\Output\OutputInterface;
class Passport extends Command
{
use ConfirmableTrait;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'monica:passport {--force : Force the operation to run when in production.}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Check if encryption keys and Personal Access Client are present, and create them if not.';
/**
* Execute the console command.
*
* @return void
*/
public function handle(): void
{
if ($this->confirmToProceed()) {
$this->checkEncryptionKeys();
$this->checkPersonalAccessClient();
}
}
private function checkEncryptionKeys()
{
$this->info('Checking encryption keys...', OutputInterface::VERBOSITY_VERBOSE);
if (! empty(config('passport.private_key')) && ! empty(config('passport.public_key'))) {
$this->info('✓ PASSPORT_PRIVATE_KEY and PASSPORT_PUBLIC_KEY detected.', OutputInterface::VERBOSITY_VERBOSE);
return;
}
if (file_exists(base_path('storage/oauth-private.key')) && file_exists(base_path('storage/oauth-public.key'))) {
$this->info('✓ Files storage/oauth-private.key and storage/oauth-public.key detected.', OutputInterface::VERBOSITY_VERBOSE);
return;
}
$this->artisan('✓ Creating encryption keys', 'passport:keys', ['--no-interaction']);
$this->warn('! Please be careful to backup '.base_path('storage/oauth-public.key').' and '.base_path('storage/oauth-private.key').' files !', OutputInterface::VERBOSITY_VERBOSE);
}
private function checkPersonalAccessClient()
{
$this->info('Checking Personal Access Client...', OutputInterface::VERBOSITY_VERBOSE);
if (PersonalAccessClient::count() > 0) {
$this->info('✓ Personal Access Client already created.', OutputInterface::VERBOSITY_VERBOSE);
return;
}
$this->artisan('✓ Creating personal access client', 'passport:client', ['--personal', '--no-interaction']);
}
}

View File

@@ -0,0 +1,113 @@
<?php
namespace App\Console\Commands;
use PharIo\Version\Version;
use App\Models\Contact\Contact;
use Illuminate\Console\Command;
use App\Models\Instance\Instance;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Http;
use Illuminate\Console\ConfirmableTrait;
use Illuminate\Http\Client\RequestException;
use Symfony\Component\Console\Output\OutputInterface;
class PingVersionServer extends Command
{
use ConfirmableTrait;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'monica:ping
{--force : Force the operation to run when in production.}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Ping version.monicahq.com to know if a new version is available';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if (! config('monica.check_version')) {
return;
}
if (! $this->confirmToProceed('Checking version deactivated', function () {
return $this->getLaravel()->environment() === 'production';
})) {
return false;
}
$instance = Instance::first();
$instance->current_version = config('monica.app_version');
if ($instance->current_version == '') {
Log::warning('Current instance version is not set, skipping version check.');
return;
}
// Query version.monicahq.com
try {
$this->log('Call url: '.config('monica.weekly_ping_server_url'));
$response = Http::acceptJson()
->post(config('monica.weekly_ping_server_url'), [
'uuid' => $instance->uuid,
'version' => $instance->current_version,
'contacts' => Contact::count(),
])
->throw();
} catch (RequestException $e) {
$this->error('Error calling "'.config('monica.weekly_ping_server_url').'": '.$e->getMessage());
Log::error(__CLASS__.' Error calling "'.config('monica.weekly_ping_server_url').'": '.$e->getMessage(), [$e]);
return;
}
// Receive the JSON
$json = $response->json();
$this->log('instance version: '.$instance->current_version);
$currentVersion = $this->getVersion($instance->current_version);
$this->log('current version: '.$json['latest_version']);
$latestVersion = $this->getVersion($json['latest_version']);
if ($latestVersion > $currentVersion) {
$instance->latest_version = $json['latest_version'];
$instance->latest_release_notes = $json['notes'];
$instance->number_of_versions_since_current_version = $json['number_of_versions_since_user_version'];
} else {
$instance->latest_release_notes = null;
$instance->number_of_versions_since_current_version = null;
}
$instance->save();
}
public function log($string)
{
$this->info($string, OutputInterface::VERBOSITY_VERBOSE);
}
private function getVersion(string $version): ?Version
{
try {
return new Version($version);
} catch (\Exception $e) {
$this->error("Error parsing version '$version': ".$e->getMessage());
}
return null;
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\Contact\ReminderOutbox;
use App\Jobs\Reminder\NotifyUserAboutReminder;
class SendReminders extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'send:reminders';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Send reminders that are scheduled for the contacts';
/**
* Execute the console command.
*
* @return void
*/
public function handle(): void
{
// Grab all the reminders that are supposed to be sent in the next two days
// Why 2? because in terms of timezone, we can have up to more than 24 hours
// between two timezones and we need to take into accounts reminders
// that are not in the same timezone.
ReminderOutbox::where('planned_date', '<', now()->addDays(2))
->orderBy('planned_date', 'asc')
->chunk(500, function ($reminderOutboxes) {
$this->send($reminderOutboxes);
});
}
/**
* Send the reminder to the user and schedule the future.
*
* @return void
*/
private function send($reminderOutboxes)
{
foreach ($reminderOutboxes as $reminderOutbox) {
if ($reminderOutbox->user->isTheRightTimeToBeReminded($reminderOutbox->planned_date)) {
NotifyUserAboutReminder::dispatch($reminderOutbox);
}
}
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace App\Console\Commands;
use App\Models\Contact\Contact;
use Illuminate\Console\Command;
use App\Jobs\StayInTouch\ScheduleStayInTouch;
class SendStayInTouch extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'send:stay_in_touch';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Send notifications about staying in touch with contacts';
/**
* Execute the console command.
*
* @return void
*/
public function handle(): void
{
// we add two days to make sure we cover all timezones
Contact::where('stay_in_touch_trigger_date', '<', now()->addDays(2))
->whereNotNull('stay_in_touch_frequency')
->orderBy('stay_in_touch_trigger_date', 'asc')
->chunk(500, function ($contacts) {
$this->schedule($contacts);
});
}
private function schedule($contacts)
{
foreach ($contacts as $contact) {
ScheduleStayInTouch::dispatch($contact);
}
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Mail;
class SendTestEmail extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'monica:test-email
{--email= : The email address to send to}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Send a test email to check that delivery is working';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
/** retrieve the email from the option.
* @var string $email
*/
$email = $this->option('email');
// if no email was passed to the option, prompt the user to enter the email
if (! $email) {
$email = (string) $this->ask('What email address should I send the test email to?');
}
// Validate user provided email address
if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
$this->error("Invalid email address: \"$email\".");
return -1;
}
$this->info("Preparing and sending email to \"$email\"");
// immediately deliver the test email (bypassing the queue)
Mail::raw(
"Hi $email, you requested a test email from Monica.",
function ($message) use ($email) {
$message->to($email)
->subject('Monica email delivery test');
}
);
$this->info('Email sent!');
return 0;
}
}

View File

@@ -0,0 +1,135 @@
<?php
namespace App\Console\Commands;
use function Safe\exec;
use function Safe\mkdir;
use Illuminate\Console\Command;
use function Safe\file_put_contents;
use Illuminate\Console\ConfirmableTrait;
use Symfony\Component\Console\Output\OutputInterface;
class SentryRelease extends Command
{
use ConfirmableTrait;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'sentry:release
{--force : Force the operation to run when in production.}
{--release= : release version for sentry.}
{--store-release : store release version in config/.release file.}
{--commit= : commit associated with this release.}
{--environment= : sentry environment.}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a release for sentry';
/**
* Installation path of sentry cli.
*
* @var string
*/
private $install_dir;
/**
* sentry cli name.
*
* @var string
*/
private const SENTRY_CLI = 'sentry-cli';
/**
* Sentry cli download url.
*
* @var string
*/
private const SENTRY_URL = 'https://sentry.io/get-cli/';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if (! config('monica.sentry_support') || ! $this->check()) {
return;
}
if ($this->confirmToProceed()) {
$this->install_dir = env('SENTRY_ROOT', getenv('HOME').'/.local/bin');
$release = $this->option('release') ?? config('sentry.release');
$commit = $this->option('commit') ??
(is_dir(__DIR__.'/../../../.git') ? trim(exec('git log --pretty="%H" -n1 HEAD')) : $release);
// Sentry update
$this->exec('Update sentry', $this->getSentryCli().' update');
// Create a release
$this->execSentryCli('Create a release', 'releases new '.$release.' --finalize --project '.config('sentry-release.project'));
// Associate commits with the release
$this->execSentryCli('Associate commits with the release', 'releases set-commits '.$release.' --commit "'.config('sentry-release.repo').'@'.$commit.'"');
// Create a deploy
$this->execSentryCli('Create a deploy', 'releases deploys '.$release.' new --env '.$this->option('environment').' --name '.config('monica.app_version'));
if ($this->option('store-release')) {
// Set sentry release
$this->line('Store release in config/.release file', null, OutputInterface::VERBOSITY_VERBOSE);
file_put_contents(__DIR__.'/../../../config/.release', $release);
}
}
}
private function check(): bool
{
$check = true;
if (empty(config('sentry-release.auth_token'))) {
$this->error('You must provide an auth_token (SENTRY_AUTH_TOKEN)');
$check = false;
}
if (empty(config('sentry-release.organisation'))) {
$this->error('You must provide an organisation slug (SENTRY_ORG)');
$check = false;
}
if (empty(config('sentry-release.project'))) {
$this->error('You must set the project (SENTRY_PROJECT)');
$check = false;
}
if (empty(config('sentry-release.repo'))) {
$this->error('You must set the repository (SENTRY_REPO)');
$check = false;
}
if (empty($this->option('environment'))) {
$this->error('No environment given');
$check = false;
}
return $check;
}
private function getSentryCli()
{
if (! file_exists($this->install_dir.'/'.self::SENTRY_CLI)) {
mkdir($this->install_dir, 0777, true);
$this->exec('Downloading sentry-cli', 'curl -sL '.self::SENTRY_URL.' | INSTALL_DIR='.$this->install_dir.' bash');
}
return $this->install_dir.'/'.self::SENTRY_CLI;
}
private function execSentryCli($message, $command)
{
$this->exec($message, $this->getSentryCli().' '.$command);
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace App\Console\Commands;
use App\Models\Account\Account;
use Illuminate\Console\Command;
class SetPremiumAccount extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'account:setpremium {accountId}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Give a premium access to an account.';
/**
* Execute the console command.
*
* @return void
*/
public function handle(): void
{
$account = Account::findOrFail($this->argument('accountId'));
$account->update([
'has_access_to_paid_version_for_free' => true,
]);
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace App\Console\Commands;
use App\Models\User\User;
use Illuminate\Console\Command;
class SetUserAdmin extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'monica:admin
{--force : Force the operation to run when in production.}
{--email= : The email of the user whose admin status you want to change}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Toggle administrator privileges for a user';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
// retrieve the email from the option
$email = $this->option('email');
// if no email was passed to the option, prompt the user to enter the email
if (! $email) {
$email = $this->ask('What is the users email?');
}
// retrieve the user with the specified email
$user = User::where('email', $email)->first();
if (! $user) {
// show an error and exist if the user does not exist
$this->error('No user with that email.');
return;
}
// Print a warning
if ($user->admin) {
$this->warn($user->email.' will be removed from the administrators of this instance');
} else {
$this->warn($user->email.' will be added to the administrators of this instance');
}
// ask for confirmation if not forced
if (! $this->option('force') && ! $this->confirm('Do you wish to continue?')) {
return;
}
// toglle admin status
$user->admin = ! $user->admin;
$user->save();
// Show new status
if ($user->admin) {
$this->info($user->email.' has been added to the administrators of this instance');
} else {
$this->info($user->email.' has been removed from the administrators of this instance');
}
}
}

View File

@@ -0,0 +1,82 @@
<?php
namespace App\Console\Commands;
use function Safe\touch;
use App\Helpers\InstanceHelper;
use App\Models\Account\Account;
use Illuminate\Console\Command;
class SetupProduction extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'setup:production
{--force : Force the operation to run when in production.}
{--email= : Login email for the first account.}
{--password= : Password to set for the first account.}
{--skipSeed : Skip the populate database process.}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Perform setup of Monica.';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if ((! $this->option('force')) && (! $this->confirm('You are about to setup and configure Monica. Do you wish to continue?'))) {
return;
}
/*
* If the .env file does not exist, then key generation
* will fail. So we create one if it does not already exist.
*/
if (! file_exists(__DIR__.'/../../../.env')) {
touch(__DIR__.'/../../../.env');
}
$this->call('monica:update', ['--force' => true]);
if (! $this->option('skipSeed')) {
$this->line('✓ Filling database');
$this->call('db:seed', ['--force' => true]);
}
$this->line('');
$this->line('-----------------------------');
$this->line('|');
$this->line('| Welcome to Monica v'.config('monica.app_version'));
$this->line('|');
$this->line('-----------------------------');
$email = $this->option('email');
$password = $this->option('password');
if (! empty($email) && ! empty($password)) {
Account::createDefault('John', 'Doe', $email, $password);
$this->info('| You can now sign in to your account:');
$this->line('| username: '.$email);
$this->line('| password: <hidden>');
} elseif (InstanceHelper::hasAtLeastOneAccount()) {
$this->info('| You can now log in to your account');
} else {
$this->info('| You can now register to the first account by opening the application:');
}
$this->line('| URL: '.config('app.url'));
$this->line('-----------------------------');
$this->info('Setup is done. Have fun.');
}
}

View File

@@ -0,0 +1,677 @@
<?php
namespace App\Console\Commands;
use function Safe\exec;
use App\Models\User\User;
use App\Helpers\DateHelper;
use Illuminate\Support\Carbon;
use App\Models\Account\Account;
use App\Models\Contact\Contact;
use Illuminate\Console\Command;
use App\Helpers\CountriesHelper;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Console\Application;
use App\Models\Contact\LifeEventType;
use App\Models\Contact\ContactFieldType;
use App\Services\Contact\Gift\CreateGift;
use App\Services\Contact\Tag\AssociateTag;
use Illuminate\Foundation\Testing\WithFaker;
use App\Services\Contact\Address\CreateAddress;
use App\Services\Contact\Contact\CreateContact;
use App\Services\Contact\Reminder\CreateReminder;
use Symfony\Component\Console\Helper\ProgressBar;
use App\Services\Contact\LifeEvent\CreateLifeEvent;
use Symfony\Component\Console\Output\ConsoleOutput;
use App\Services\Contact\Conversation\CreateConversation;
use App\Services\Contact\Relationship\CreateRelationship;
use App\Services\Account\Activity\Activity\CreateActivity;
use App\Services\Contact\Contact\UpdateBirthdayInformation;
use App\Services\Contact\Contact\UpdateDeceasedInformation;
use App\Services\Contact\Conversation\AddMessageToConversation;
use App\Services\Account\Activity\Activity\AttachContactToActivity;
class SetupTest extends Command
{
use WithFaker;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'setup:test
{--skipSeed : Skip the populate database with fake data.}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create the test environment with optional fake data for testing purposes.';
/**
* The number of contacts that need to be generated.
*
* @var int
*/
private $numberOfContacts;
/**
* The Current contact instance.
*
* @var Contact
*/
private $contact;
/**
* The current Contact instance.
*
* @var Account
*/
private $account;
/**
* The list of countries.
*
* @var Collection
*/
private $countries = null;
/**
* The logged user.
*
* @var User
*/
private $user;
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if (! $this->confirm('Are you sure you want to proceed? This will delete ALL data in your environment.')) {
return;
}
$this->artisan('✓ Performing migrations', 'migrate:fresh');
$this->artisan('✓ Symlink the storage folder', 'storage:link');
if (! $this->option('skipSeed')) {
$this->numberOfContacts = $this->ask('How many contacts would you like to have in this test account?');
$this->info('✓ Filling database with fake data');
$this->seed();
}
$this->line('');
$this->line('-----------------------------');
$this->line('|');
$this->line('| Welcome to Monica v'.config('monica.app_version'));
$this->line('|');
$this->line('-----------------------------');
$this->info('| You can now sign in to your account:');
$this->line('| username: admin@admin.com');
$this->line('| password: admin0');
$this->line('| URL: '.config('app.url'));
$this->line('-----------------------------');
$this->info('Setup is done. Have fun.');
}
public function exec($message, $command)
{
$this->info($message);
$this->line($command);
exec($command, $output);
$this->line(implode('\n', $output));
$this->line('');
}
public function artisan($message, $command, array $arguments = [])
{
$this->info($message);
$this->line(Application::formatCommandString($command));
$this->callSilent($command, $arguments);
$this->line('');
}
/**
* Run the database seeds.
*
* @return void
*/
public function seed()
{
$this->setUpFaker();
// Get or create the first account
if (User::where('email', 'admin@admin.com')->exists()) {
$this->user = User::where('email', 'admin@admin.com')->first();
$userId = $this->user->value('id');
$this->account = Account::where('id', $userId)->first();
} else {
$this->account = Account::createDefault('John', 'Doe', 'admin@admin.com', 'admin0');
// set default admin account to confirmed
/** @var User */
$adminUser = $this->account->users()->first();
$this->confirmUser($adminUser);
$this->user = $adminUser;
}
// create a random number of contacts
//$this->numberOfContacts = rand(60, 100);
echo 'Generating '.$this->numberOfContacts.' fake contacts'.PHP_EOL;
$output = new ConsoleOutput();
$progress = new ProgressBar($output, $this->numberOfContacts);
$progress->start();
for ($i = 0; $i < $this->numberOfContacts; $i++) {
$gender = (rand(1, 2) == 1) ? 'male' : 'female';
$this->contact = app(CreateContact::class)->execute([
'account_id' => $this->account->id,
'author_id' => $this->user->id,
'first_name' => $this->faker->firstName($gender),
'last_name' => (rand(1, 2) == 1) ? $this->faker->lastName : null,
'nickname' => (rand(1, 2) == 1) ? $this->faker->name : null,
'gender_id' => $this->getRandomGender()->id,
'is_partial' => false,
'is_birthdate_known' => false,
'is_deceased' => false,
'is_deceased_date_known' => false,
]);
$this->populateTags();
$this->populateFoodPreferences();
$this->populateDeceasedDate();
$this->populateBirthday();
$this->populateFirstMetInformation();
$this->populateRelationships();
$this->populateNotes();
$this->populateActivities();
$this->populateTasks();
$this->populateDebts();
$this->populateCalls();
$this->populateConversations();
$this->populateLifeEvents();
$this->populateGifts();
$this->populateAddresses();
$this->populateContactFields();
$this->populatePets();
$this->changeUpdatedAt();
$progress->advance();
}
$this->populateDayRatings();
$this->populateEntries();
$progress->finish();
// create the second test, blank account
if (! User::where('email', 'blank@blank.com')->exists()) {
$blankAccount = Account::createDefault('Blank', 'State', 'blank@blank.com', 'blank0');
$blankUser = $blankAccount->users()->first();
$this->confirmUser($blankUser);
}
}
public function populateTags()
{
if (rand(1, 2) == 1) {
$i = 0;
do {
app(AssociateTag::class)->execute([
'account_id' => $this->account->id,
'contact_id' => $this->contact->id,
'name' => $this->faker->word,
]);
$i++;
} while ($i < 10);
}
}
public function populateFoodPreferences()
{
// add food preferences
if (rand(1, 2) == 1) {
$this->contact->food_preferences = $this->faker->realText();
$this->contact->save();
}
}
public function populateDeceasedDate()
{
// deceased?
if (rand(1, 7) == 1) {
$birthdate = $this->faker->dateTimeThisCentury();
app(UpdateDeceasedInformation::class)->execute([
'account_id' => $this->account->id,
'contact_id' => $this->contact->id,
'is_deceased' => rand(1, 2) == 1,
'is_date_known' => rand(1, 2) == 1,
'day' => (int) $birthdate->format('d'),
'month' => (int) $birthdate->format('m'),
'year' => (int) $birthdate->format('Y'),
'add_reminder' => rand(1, 2) == 1,
]);
}
}
public function populateBirthday()
{
if (rand(1, 2) == 1) {
$birthdate = $this->faker->dateTimeThisCentury();
app(UpdateBirthdayInformation::class)->execute([
'account_id' => $this->account->id,
'contact_id' => $this->contact->id,
'is_date_known' => rand(1, 2) == 1,
'day' => (int) $birthdate->format('d'),
'month' => (int) $birthdate->format('m'),
'year' => (int) $birthdate->format('Y'),
'is_age_based' => rand(1, 2) == 1,
'age' => rand(1, 99),
'add_reminder' => rand(1, 2) == 1,
'is_deceased' => $this->contact->is_dead,
]);
}
}
public function populateFirstMetInformation()
{
if (rand(1, 2) == 1) {
$this->contact->first_met_where = $this->faker->realText(20);
}
if (rand(1, 2) == 1) {
$this->contact->first_met_additional_info = $this->faker->realText(20);
$firstMetDate = $this->faker->dateTimeThisCentury();
if (rand(1, 2) == 1) {
// add a date where we don't know the year
$specialDate = $this->contact->setSpecialDate('first_met', 0, intval($firstMetDate->format('m')), intval($firstMetDate->format('d')));
} else {
// add a date where we know the year
$specialDate = $this->contact->setSpecialDate('first_met', intval($firstMetDate->format('Y')), intval($firstMetDate->format('m')), intval($firstMetDate->format('d')));
}
app(CreateReminder::class)->execute([
'account_id' => $this->account->id,
'contact_id' => $this->contact->id,
'initial_date' => $specialDate->date->toDateString(),
'frequency_type' => 'year',
'frequency_number' => 1,
'title' => trans(
'people.introductions_reminder_title',
['name' => $this->contact->first_name]
),
]);
}
if (rand(1, 2) == 1) {
do {
$rand = rand(1, $this->numberOfContacts);
} while (in_array($rand, [$this->contact->id]));
$this->contact->first_met_through_contact_id = $rand;
}
$this->contact->save();
}
public function populateRelationships()
{
if (rand(1, 2) == 1) {
foreach (range(1, rand(2, 6)) as $index) {
$gender = (rand(1, 2) == 1) ? 'male' : 'female';
$relatedContact = app(CreateContact::class)->execute([
'account_id' => $this->account->id,
'author_id' => $this->user->id,
'first_name' => $this->faker->firstName($gender),
'last_name' => (rand(1, 2) == 1) ? $this->faker->lastName : null,
'nickname' => (rand(1, 2) == 1) ? $this->faker->name : null,
'gender_id' => $this->getRandomGender()->id,
'is_partial' => rand(1, 2) == 1,
'is_birthdate_known' => false,
'is_deceased' => false,
'is_deceased_date_known' => false,
]);
$relatedContactBirthDate = $this->faker->dateTimeThisCentury();
app(UpdateBirthdayInformation::class)->execute([
'account_id' => $this->account->id,
'contact_id' => $relatedContact->id,
'is_date_known' => rand(1, 2) == 1,
'day' => (int) $relatedContactBirthDate->format('d'),
'month' => (int) $relatedContactBirthDate->format('m'),
'year' => (int) $relatedContactBirthDate->format('Y'),
'is_age_based' => rand(1, 2) == 1,
'age' => rand(1, 99),
'add_reminder' => rand(1, 2) == 1,
'is_deceased' => $relatedContact->is_dead,
]);
// set relationship
$relationshipId = $this->contact->account->relationshipTypes->random()->id;
$relationship = app(CreateRelationship::class)->execute([
'account_id' => $this->account->id,
'contact_is' => $this->contact->id,
'of_contact' => $relatedContact->id,
'relationship_type_id' => $relationshipId,
]);
}
}
}
public function populateNotes()
{
if (rand(1, 2) == 1) {
for ($j = 0; $j < rand(1, 13); $j++) {
$note = $this->contact->notes()->create([
'body' => $this->faker->realText(rand(40, 500)),
'account_id' => $this->account->id,
'is_favorited' => rand(1, 3) == 1,
'favorited_at' => $this->faker->dateTimeThisCentury(),
]);
}
}
}
public function populateActivities()
{
if (rand(1, 2) == 1) {
for ($j = 0; $j < rand(1, 13); $j++) {
$date = DateHelper::getDate(Carbon::instance($this->faker->dateTimeThisYear($max = 'now')));
$request = [
'account_id' => $this->account->id,
'activity_type_id' => rand(1, 13),
'summary' => $this->faker->realText(rand(40, 100)),
'description' => (rand(1, 2) == 1 ? $this->faker->realText(rand(100, 1000)) : null),
'happened_at' => $date,
'contacts' => [$this->contact->id],
];
$activity = app(CreateActivity::class)->execute($request);
$request = [
'account_id' => $this->account->id,
'activity_id' => $activity->id,
'contacts' => [$this->contact->id],
];
app(AttachContactToActivity::class)->execute($request);
DB::table('journal_entries')->insertGetId([
'account_id' => $this->account->id,
'date' => $date,
'journalable_id' => $activity->id,
'journalable_type' => 'App\Models\Account\Activity',
]);
}
}
}
public function populateTasks()
{
if (rand(1, 2) == 1) {
for ($j = 0; $j < rand(1, 10); $j++) {
$task = $this->contact->tasks()->create([
'title' => $this->faker->realText(rand(40, 100)),
'description' => $this->faker->realText(rand(100, 1000)),
'completed' => (rand(1, 2) == 1 ? 0 : 1),
'completed_at' => (rand(1, 2) == 1 ? $this->faker->dateTimeThisCentury() : null),
'account_id' => $this->account->id,
]);
}
}
}
public function populateDebts()
{
if (rand(1, 2) == 1) {
for ($j = 0; $j < rand(1, 6); $j++) {
$this->contact->debts()->create([
'in_debt' => (rand(1, 2) == 1 ? 'yes' : 'no'),
'amount' => rand(321, 39391),
'reason' => $this->faker->realText(rand(100, 1000)),
'status' => 'inprogress',
'account_id' => $this->account->id,
]);
}
}
}
public function populateGifts()
{
if (rand(1, 2) == 1) {
for ($j = 0; $j < rand(1, 31); $j++) {
app(CreateGift::class)->execute([
'account_id' => $this->account->id,
'contact_id' => $this->contact->id,
'status' => (rand(1, 3) == 1 ? 'offered' : 'idea'),
'name' => $this->faker->realText(rand(10, 100)),
'comment' => $this->faker->realText(rand(1000, 5000)),
'url' => $this->faker->url,
'amount' => rand(12, 120),
]);
}
}
}
public function populateAddresses()
{
if (rand(1, 3) == 1) {
$request = [
'account_id' => $this->account->id,
'contact_id' => $this->contact->id,
'country' => $this->getRandomCountry(),
'name' => $this->faker->word,
'street' => (rand(1, 3) == 1) ? $this->faker->streetAddress : null,
'city' => (rand(1, 3) == 1) ? $this->faker->city : null,
'province' => (rand(1, 3) == 1) ? $this->faker->state : null,
'postal_code' => (rand(1, 3) == 1) ? $this->faker->postcode : null,
];
app(CreateAddress::class)->execute($request);
}
}
private function getRandomCountry()
{
if ($this->countries == null) {
$this->countries = CountriesHelper::getAll();
}
return $this->countries->random()['id'];
}
public function populateContactFields()
{
if (rand(1, 3) == 1) {
// Fetch number of types
$numberOfTypes = ContactFieldType::where('account_id', $this->account->id)->count();
for ($j = 0; $j < rand(1, $numberOfTypes); $j++) {
// Retrieve random ContactFieldType
$contactFieldType = ContactFieldType::where('account_id', $this->account->id)->orderBy(DB::raw('RAND()'))->firstOrFail();
// Fake data according to type
$data = null;
switch ($contactFieldType->name) {
case 'Email':
$data = $this->faker->email;
break;
case 'Phone':
$data = $this->faker->phoneNumber;
break;
case 'Facebook':
$data = 'https://facebook.com/'.$this->faker->userName;
break;
case 'Twitter':
$data = 'https://twitter.com/'.$this->faker->userName;
break;
case 'Whatsapp':
$data = $this->faker->phoneNumber;
break;
case 'Telegram':
$data = $this->faker->phoneNumber;
break;
default:
$data = $this->faker->url;
break;
}
$this->contact->contactFields()->create([
'contact_field_type_id' => $contactFieldType->id,
'data' => $data,
'account_id' => $this->account->id,
]);
}
}
}
public function populateEntries()
{
for ($j = 0; $j < rand(10, 100); $j++) {
$date = $this->faker->dateTimeThisYear();
$entryId = DB::table('entries')->insertGetId([
'account_id' => $this->account->id,
'title' => $this->faker->realText(rand(12, 20)),
'post' => $this->faker->realText(rand(400, 500)),
'created_at' => $date,
]);
DB::table('journal_entries')->insertGetId([
'account_id' => $this->account->id,
'date' => $date,
'journalable_id' => $entryId,
'journalable_type' => 'App\Models\Journal\Entry',
'created_at' => now(),
]);
}
}
public function populatePets()
{
if (rand(1, 3) == 1) {
for ($j = 0; $j < rand(1, 3); $j++) {
$date = $this->faker->dateTimeThisYear();
DB::table('pets')->insertGetId([
'account_id' => $this->account->id,
'contact_id' => $this->contact->id,
'pet_category_id' => rand(1, 11),
'name' => (rand(1, 3) == 1) ? $this->faker->firstName : null,
'created_at' => $date,
]);
}
}
}
public function populateDayRatings()
{
for ($j = 0; $j < rand(10, 100); $j++) {
$date = $this->faker->dateTimeThisYear();
$dayId = DB::table('days')->insertGetId([
'account_id' => $this->account->id,
'rate' => rand(1, 3),
'date' => $date,
'created_at' => $date,
]);
DB::table('journal_entries')->insertGetId([
'account_id' => $this->account->id,
'date' => $date,
'journalable_id' => $dayId,
'journalable_type' => 'App\Models\Journal\Day',
'created_at' => now(),
]);
}
}
public function changeUpdatedAt()
{
$this->contact->last_consulted_at = Carbon::instance($this->faker->dateTimeThisYear());
$this->contact->save();
}
public function populateCalls()
{
if (rand(1, 3) == 1) {
$this->contact->calls()->create([
'account_id' => $this->account->id,
'called_at' => $this->faker->dateTimeThisYear(),
]);
}
}
public function populateConversations()
{
if (rand(1, 3) == 1) {
for ($j = 0; $j < rand(1, 20); $j++) {
$contactFieldType = ContactFieldType::where('account_id', $this->account->id)->orderBy(DB::raw('RAND()'))->firstOrFail();
$conversation = app(CreateConversation::class)->execute([
'happened_at' => $this->faker->dateTimeThisCentury(),
'contact_id' => $this->contact->id,
'contact_field_type_id' => $contactFieldType->id,
'account_id' => $this->account->id,
]);
for ($k = 0; $k < rand(1, 20); $k++) {
app(AddMessageToConversation::class)->execute([
'account_id' => $this->account->id,
'contact_id' => $this->contact->id,
'conversation_id' => $conversation->id,
'written_at' => $this->faker->dateTimeThisCentury(),
'written_by_me' => (rand(1, 2) == 1),
'content' => $this->faker->realText(),
]);
}
}
}
}
public function populateLifeEvents()
{
if (rand(1, 3) == 1) {
for ($j = 0; $j < rand(1, 20); $j++) {
$lifeEventType = LifeEventType::where('account_id', $this->account->id)->orderBy(DB::raw('RAND()'))->firstOrFail();
app(CreateLifeEvent::class)->execute([
'account_id' => $this->account->id,
'contact_id' => $this->contact->id,
'life_event_type_id' => $lifeEventType->id,
'happened_at' => $this->faker->dateTimeThisCentury(),
'name' => $this->faker->realText(),
'note' => $this->faker->realText(),
'has_reminder' => false,
'happened_at_month_unknown' => false,
'happened_at_day_unknown' => false,
]);
}
}
}
public function getRandomGender()
{
return $this->account->genders->random();
}
public function confirmUser($user)
{
$user->markEmailAsVerified();
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Console\Commands\Tests;
use App\Models\User\User;
use Illuminate\Console\Command;
use App\Services\User\AcceptPolicy;
class SetupFrontEndTestUser extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'setup:frontendtestuser {--database= : The database connection to use}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a user exclusively for front-end testing with Cypress.';
/**
* Execute the console command.
*
* @return void
*/
public function handle(): void
{
app('migrator')->setConnection($this->option('database'));
$user = factory(User::class)->create();
$user->account->populateDefaultFields();
app(AcceptPolicy::class)->execute([
'account_id' => $user->account->id,
'user_id' => $user->id,
'ip_address' => null,
]);
$this->info($user->getKey());
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace App\Console\Commands\Tests;
use Illuminate\Support\Str;
use function Safe\json_decode;
use function Safe\json_encode;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
/**
* @codeCoverageIgnore
*/
class UpdateLangs extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'setup:updatelang';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Update langage files.';
/**
* Execute the console command.
*
* @return void
*/
public function handle(): void
{
if (! File::exists(base_path('vendor/laravel-lang/lang'))) {
throw new \Exception("This command requires laravel-lang/lang package. Run 'composer require --dev laravel-lang/lang' to install it.");
}
$en = json_decode(File::get(lang_path('en.json')), true);
foreach (File::directories(lang_path()) as $lang) {
$lang = basename($lang);
$locale = Str::of($lang)->replace('-', '_');
switch ($lang) {
case 'zh':
$locale = 'zh_CN';
break;
case 'en':
continue 2;
}
if (File::exists($orig_path = lang_path("$lang.json"))
&& File::exists($trans_path = base_path("vendor/laravel-lang/lang/locales/$locale/$locale.json"))) {
$lang_orig = json_decode(File::get($orig_path), true);
$lang_trans = json_decode(File::get($trans_path), true);
foreach ($en as $key) {
$lang_orig[$key] = $lang_trans[$key];
}
File::put($orig_path, json_encode($lang_orig, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)."\n");
}
}
}
}

View File

@@ -0,0 +1,116 @@
<?php
namespace App\Console\Commands;
use App\Helpers\DBHelper;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Console\ConfirmableTrait;
class Update extends Command
{
use ConfirmableTrait;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'monica:update
{--force : Force the operation to run when in production.}
{--composer-install : Updating composer dependencies.}
{--skip-storage-link : Skip storage link create.}
{--dev : Install dev dependencies too.}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Update monica dependencies and migrations after a new release';
/**
* Execute the console command.
*
* @return void
*/
public function handle(): void
{
if ($this->confirmToProceed()) {
try {
$this->artisan('✓ Maintenance mode: on', 'down', [
'--retry' => '10',
]);
// Clear or rebuild all cache
if (config('cache.default') != 'database' || Schema::hasTable(config('cache.stores.database.table'))) {
$this->artisan('✓ Resetting application cache', 'cache:clear');
}
if ($this->getLaravel()->environment() == 'production') {
$this->artisan('✓ Clear config cache', 'config:clear');
$this->artisan('✓ Resetting route cache', 'route:cache');
if ($this->getLaravel()->version() > '5.6') {
$this->artisan('✓ Resetting view cache', 'view:cache');
} else {
$this->artisan('✓ Resetting view cache', 'view:clear');
}
} else {
$this->artisan('✓ Clear config cache', 'config:clear');
$this->artisan('✓ Clear route cache', 'route:clear');
$this->artisan('✓ Clear view cache', 'view:clear');
}
if ($this->option('composer-install') === true) {
$this->exec('✓ Updating composer dependencies', 'composer install --no-interaction'.($this->option('dev') === false ? ' --no-dev' : ''));
}
if ($this->option('skip-storage-link') !== true && $this->getLaravel()->environment() != 'testing' && ! file_exists(public_path('storage'))) {
$this->artisan('✓ Symlink the storage folder', 'storage:link');
}
if ($this->migrateCollationTest()) {
$this->artisan('✓ Performing collation migrations', 'migrate:collation', ['--force']);
}
$this->artisan('✓ Performing migrations', 'migrate', ['--force']);
$this->artisan('✓ Check for encryption keys', 'monica:passport', ['--force']);
$this->artisan('✓ Ping for new version', 'monica:ping', ['--force']);
// Cache config
if ($this->getLaravel()->environment() == 'production'
&& (config('cache.default') != 'database' || Schema::hasTable(config('cache.stores.database.table')))) {
$this->artisan('✓ Cache configuraton', 'config:cache');
}
} finally {
$this->artisan('✓ Maintenance mode: off', 'up');
}
$this->line('Monica v'.config('monica.app_version').' is set up, enjoy.');
}
}
private function migrateCollationTest()
{
$connection = DBHelper::connection();
if ($connection->getDriverName() != 'mysql') {
return false;
}
$databasename = $connection->getDatabaseName();
$schemata = DB::select(
'select DEFAULT_CHARACTER_SET_NAME from information_schema.schemata where schema_name = ?',
[$databasename]
);
$schema = $schemata[0]->DEFAULT_CHARACTER_SET_NAME;
return config('database.use_utf8mb4') && $schema == 'utf8'
|| ! config('database.use_utf8mb4') && $schema == 'utf8mb4';
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Jobs\Avatars\UpdateAllGravatars;
class UpdateGravatars extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'monica:updategravatars';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Update all gravatars';
/**
* Execute the console command.
*
* @return void
*/
public function handle(): void
{
UpdateAllGravatars::dispatch();
}
}

75
app/Console/Kernel.php Normal file
View File

@@ -0,0 +1,75 @@
<?php
namespace App\Console;
use App\Console\Scheduling\CronEvent;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
/**
* The Artisan commands provided by your application.
*
* @var array
*/
protected $commands = [
];
/**
* Register the Closure based commands for the application.
*
* @return void
*/
protected function commands()
{
$this->load(__DIR__.'/Commands');
$this->load(__DIR__.'/Commands/OneTime');
if ($this->app->environment() != 'production') {
$this->load(__DIR__.'/Commands/Tests');
}
require base_path('routes/console.php');
}
/**
* Define the application's command schedule.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
* @codeCoverageIgnore
*/
protected function schedule(Schedule $schedule)
{
$this->scheduleCommand($schedule, 'queue:prune-batches', 'daily');
$this->scheduleCommand($schedule, 'send:reminders', 'hourly');
$this->scheduleCommand($schedule, 'send:stay_in_touch', 'hourly');
$this->scheduleCommand($schedule, 'monica:davclients', 'hourly');
$this->scheduleCommand($schedule, 'monica:calculatestatistics', 'daily');
$this->scheduleCommand($schedule, 'monica:ping', 'daily');
$this->scheduleCommand($schedule, 'monica:clean', 'daily');
$this->scheduleCommand($schedule, 'monica:updategravatars', 'weekly');
if (config('app.cloudflare')) {
$this->scheduleCommand($schedule, 'cloudflare:reload', 'daily');
}
$this->scheduleCommand($schedule, 'model:prune', 'daily');
}
/**
* Define a new schedule command with a frequency.
*
* @codeCoverageIgnore
*/
private function scheduleCommand(Schedule $schedule, string $command, $frequency)
{
$schedule->command($command)->when(function () use ($command, $frequency) {
$event = CronEvent::command($command);
if ($frequency) {
$event = $event->$frequency();
}
return $event->isDue();
});
}
}

View File

@@ -0,0 +1,128 @@
<?php
namespace App\Console\Scheduling;
use Carbon\Carbon;
use App\Models\Instance\Cron;
class CronEvent
{
/**
* The cron model.
*
* @var Cron
*/
private $cron;
/**
* Frequency to run the command in minutes.
*
* @var int
*/
private $minutes = 1;
/**
* Frequency to run the command in days.
*
* @var int
*/
private $days = 0;
public function __construct(Cron $cron)
{
$this->cron = $cron;
}
/**
* Get the command.
*
* @param string $command
* @return self
*/
public static function command(string $command): self
{
/** @var \App\Models\Instance\Cron $cron */
$cron = Cron::firstOrCreate(['command' => $command]);
return new self($cron);
}
/**
* Get current Cron.
*
* @return Cron
*/
public function cron()
{
return $this->cron;
}
/**
* Run the command once per hour.
*
* @return self
*/
public function hourly(): self
{
$this->minutes = 60;
$this->days = 0;
return $this;
}
/**
* Run the command once per day.
*
* @return self
*/
public function daily(): self
{
$this->minutes = 0;
$this->days = 1;
return $this;
}
/**
* Run the command once a week.
*
* @return self
*/
public function weekly(): self
{
$this->minutes = 0;
$this->days = 7;
return $this;
}
/**
* Test if the command is due to run.
*
* @return bool
*/
public function isDue(): bool
{
$now = now();
if ($this->cron->last_run !== null) {
$t = $this->cron->last_run;
if ($this->minutes !== 0) {
$next_run = Carbon::create($t->year, $t->month, $t->day, $t->hour, (int) floor($t->minute / $this->minutes) * $this->minutes, 0)
->addMinutes($this->minutes);
} elseif ($this->days !== 0) {
$next_run = Carbon::create($t->year, $t->month, (int) floor($t->day / $this->days) * $this->days, 0, 0, 0)
->addDays($this->days);
}
if (! isset($next_run) || $next_run > $now) {
return false;
}
}
$this->cron->update(['last_run' => $now]);
return true;
}
}

8
app/Events/Event.php Normal file
View File

@@ -0,0 +1,8 @@
<?php
namespace App\Events;
abstract class Event
{
//
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Events;
use App\Models\Contact\Contact;
use Illuminate\Queue\SerializesModels;
class MoveAvatarEvent extends Event
{
use SerializesModels;
/**
* The contact.
*
* @var Contact
*/
public $contact;
/**
* Create a new event instance.
*
* @param Contact $contact
* @return void
*/
public function __construct($contact)
{
$this->contact = $contact;
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Events;
use Illuminate\Queue\SerializesModels;
class RecoveryLogin extends Event
{
use SerializesModels;
/**
* The authenticated user.
*
* @var \Illuminate\Contracts\Auth\Authenticatable
*/
public $user;
/**
* Create a new event instance.
*
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @return void
*
* @codeCoverageIgnore
*/
public function __construct($user)
{
$this->user = $user;
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Events;
use App\Models\User\SyncToken;
use Illuminate\Queue\SerializesModels;
class TokenDeleteEvent extends Event
{
use SerializesModels;
/**
* The deleted token.
*
* @var SyncToken
*/
public $token;
/**
* Create a new event instance.
*
* @param SyncToken $token
* @return void
*
* @codeCoverageIgnore
*/
public function __construct($token)
{
$this->token = $token;
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Exceptions;
use Illuminate\Contracts\Filesystem\FileNotFoundException as FileNotFoundExceptionBase;
class FileNotFoundException extends FileNotFoundExceptionBase
{
/**
* @var string
*/
public $fileName;
/**
* Create a new instance.
*
* @param string $fileName
* @return void
*/
public function __construct($fileName)
{
$this->fileName = $fileName;
parent::__construct();
}
public function __toString(): string
{
return 'File not found: '.$this->fileName;
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace App\Exceptions;
use Throwable;
use Illuminate\Session\TokenMismatchException;
use League\OAuth2\Server\Exception\OAuthServerException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
class Handler extends ExceptionHandler
{
/**
* A list of the exception types that should not be reported.
*
* @var array<int, class-string<Throwable>>
*/
protected $dontReport = [
OAuthServerException::class,
WrongIdException::class,
];
/**
* Register the exception handling callbacks for the application.
*
* @return void
* @codeCoverageIgnore
*/
public function register()
{
if (config('monica.sentry_support') && config('app.env') == 'production') {
$this->reportable(function (Throwable $e) {
if ($this->shouldReport($e) && app()->bound('sentry')) {
app('sentry')->captureException($e);
}
});
}
}
/**
* Render an exception into an HTTP response.
*
* @param \Illuminate\Http\Request $request
* @param \Throwable $e
* @return \Illuminate\Http\Response|\Symfony\Component\HttpFoundation\Response
*/
public function render($request, Throwable $e)
{
// hopefully catches those pesky token expiries
// and send them back to login.
if ($e instanceof TokenMismatchException) {
return redirect()->route('loginRedirect');
}
// Convert all non-http exceptions to a proper 500 http exception
// if we don't do this exceptions are shown as a default template
// instead of our own view in resources/views/errors/500.blade.php
if ($this->shouldReport($e) && ! $this->isHttpException($e) && ! config('app.debug')) {
$e = new HttpException(500, $e->getMessage());
}
return parent::render($request, $e);
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Exceptions;
use RuntimeException;
/**
* Exception thrown if the env variable is not set or does not exist.
*/
class MissingEnvVariableException extends RuntimeException
{
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Exceptions;
use RuntimeException;
/**
* Exception thrown if the env variable is not set or does not exist.
*/
class NoAccountException extends RuntimeException
{
}

View File

@@ -0,0 +1,9 @@
<?php
namespace App\Exceptions;
use RuntimeException;
class NoCoordinatesException extends RuntimeException
{
}

View File

@@ -0,0 +1,13 @@
<?php
namespace App\Exceptions;
use RuntimeException;
class RateLimitedSecondException extends RuntimeException
{
public function __construct($e)
{
parent::__construct('', 429, $e);
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Exceptions;
use RuntimeException;
/**
* Exception thrown by Stripe.
*/
class StripeException extends RuntimeException
{
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Exceptions;
use RuntimeException;
/**
* Exception thrown by IdHasher if a desired key item is wrong.
*/
class WrongIdException extends RuntimeException
{
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Exceptions;
use RuntimeException;
/**
* Exception thrown if the given value is not in the list of authorized values.
*/
class WrongValueException extends RuntimeException
{
}

View File

@@ -0,0 +1,63 @@
<?php
namespace App\ExportResources\Account;
use App\Models\Contact\Gender;
use App\ExportResources\User\User;
use App\ExportResources\User\Module;
use App\ExportResources\ExportResource;
use App\ExportResources\Contact\Contact;
use App\ExportResources\Contact\Document;
use App\ExportResources\Instance\AuditLog;
use App\ExportResources\Journal\JournalEntry;
use App\ExportResources\Contact\ContactFieldType;
use App\ExportResources\Relationship\Relationship;
use App\ExportResources\Contact\Gender as GenderResource;
class Account extends ExportResource
{
protected $columns = [
'uuid',
'created_at',
'updated_at',
];
protected $properties = [
'number_of_invitations_sent',
];
public function data(): ?array
{
return [
'data' => [
User::countCollection($this->users),
Contact::countCollection($this->allContacts),
Relationship::countCollection($this->relationships),
Addressbook::countCollection($this->addressBooks),
AddressbookSubscription::countCollection($this->addressBookSubscriptions),
Photo::countCollection($this->photos),
Document::countCollection($this->documents),
Activity::countCollection($this->activities),
],
'properties' => [
'default_gender' => $this->when($this->default_gender_id !== null, function () {
$defaultGender = Gender::where(['account_id' => $this->id])->find($this->default_gender_id);
return $defaultGender->uuid;
}),
'journal_entries' => JournalEntry::collection($this->journalEntries()->entry()->get()),
'modules' => Module::collection($this->modules),
'reminder_rules' => ReminderRule::collection($this->reminderRules),
'audit_logs' => AuditLog::collection($this->auditLogs),
],
'instance' => [
'activity_types' => ActivityType::collection($this->activityTypes),
'activity_type_categories' => ActivityTypeCategory::collection($this->activityTypeCategories),
'contact_field_types' => ContactFieldType::collection($this->contactFieldTypes),
'genders' => GenderResource::collection($this->genders),
'life_event_types' => LifeEventType::collection($this->lifeEventTypes),
'life_event_categories' => LifeEventCategory::collection($this->lifeEventCategories),
],
];
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App\ExportResources\Account;
use App\ExportResources\ExportResource;
class Activity extends ExportResource
{
protected $columns = [
'uuid',
'created_at',
'updated_at',
];
protected $properties = [
'summary',
'description',
'happened_at',
];
public function data(): ?array
{
return [
'properties' => [
$this->mergeWhen($this->type !== null, function () {
return [
'type' => $this->type->uuid,
];
}),
],
];
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App\ExportResources\Account;
use App\ExportResources\ExportResource;
class ActivityType extends ExportResource
{
protected $columns = [
'uuid',
'created_at',
'updated_at',
];
protected $properties = [
'translation_key',
'name',
'location_type',
];
public function data(): ?array
{
return [
'properties' => [
$this->mergeWhen($this->category !== null, function () {
return [
'category' => $this->category->uuid,
];
}),
],
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\ExportResources\Account;
use App\ExportResources\ExportResource;
class ActivityTypeCategory extends ExportResource
{
protected $columns = [
'uuid',
'created_at',
'updated_at',
];
protected $properties = [
'translation_key',
'name',
];
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\ExportResources\Account;
use App\ExportResources\ExportResource;
class Addressbook extends ExportResource
{
protected $columns = [
'uuid',
'created_at',
'updated_at',
];
protected $properties = [
'name',
'description',
];
public function data(): ?array
{
return [
'user' => $this->user->uuid,
'contacts' => $this->contacts->mapUuid(),
];
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace App\ExportResources\Account;
use App\Models\User\SyncToken;
use App\ExportResources\ExportResource;
use App\ExportResources\User\SyncToken as SyncTokenResource;
class AddressbookSubscription extends ExportResource
{
protected $columns = [
'uuid',
'created_at',
'updated_at',
];
protected $properties = [
'name',
'uri',
'username',
'readonly',
'active',
'capabilities',
'frequency',
'last_syncronized_at',
];
public function data(): ?array
{
return [
'properties' => [
'addressbook' => $this->addressBook->uuid,
'sync_token' => $this->syncToken,
$this->merge(function () {
$localSyncToken = SyncToken::where('account_id', $this->account_id)->find($this->localSyncToken);
return [
'local_sync_token' => new SyncTokenResource($localSyncToken),
];
}),
],
];
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\ExportResources\Account;
use App\ExportResources\ExportResource;
class LifeEventCategory extends ExportResource
{
protected $columns = [
'uuid',
'created_at',
'updated_at',
];
protected $properties = [
'core_monica_data',
];
public function data(): ?array
{
return [
'properties' => [
'translation_key' => $this->default_life_event_category_key,
],
];
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\ExportResources\Account;
use App\ExportResources\ExportResource;
class LifeEventType extends ExportResource
{
protected $columns = [
'uuid',
'created_at',
'updated_at',
];
protected $properties = [
'name',
'core_monica_data',
'specific_information_structure',
];
public function data(): ?array
{
return [
'properties' => [
'translation_key' => $this->default_life_event_type_key,
$this->mergeWhen($this->lifeEventCategory !== null, function () {
return [
'category' => $this->lifeEventCategory->uuid,
];
}),
],
];
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\ExportResources\Account;
use App\ExportResources\ExportResource;
class Photo extends ExportResource
{
protected $columns = [
'uuid',
'created_at',
'updated_at',
];
protected $properties = [
'original_filename',
'filesize',
'mime_type',
];
public function data(): ?array
{
return [
'properties' => [
'dataUrl' => $this->dataUrl(),
],
];
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace App\ExportResources\Account;
use App\ExportResources\ExportResource;
class ReminderRule extends ExportResource
{
protected $columns = [
'number_of_days_before',
'created_at',
'updated_at',
];
protected $properties = [
'active',
];
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App\ExportResources\Contact;
use App\ExportResources\ExportResource;
class Address extends ExportResource
{
protected $columns = [
'uuid',
'created_at',
'updated_at',
];
protected $properties = [
'name',
];
public function data(): ?array
{
return [
'properties' => [
'street' => $this->place->street,
'city' => $this->place->city,
'province' => $this->place->province,
'postal_code' => $this->place->postal_code,
'latitude' => $this->place->latitude,
'longitude' => $this->place->longitude,
'country' => $this->place->country,
],
];
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App\ExportResources\Contact;
use App\ExportResources\ExportResource;
class Call extends ExportResource
{
protected $columns = [
'uuid',
'created_at',
'updated_at',
];
protected $properties = [
'called_at',
'content',
'contact_called',
];
public function data(): ?array
{
return [
'properties' => [
$this->mergeWhen($this->emotions->count() > 0, [
'emotions' => $this->emotions->map(function ($emotion) {
return $emotion->name;
})->toArray(),
]),
],
];
}
}

View File

@@ -0,0 +1,106 @@
<?php
namespace App\ExportResources\Contact;
use App\ExportResources\Account\Photo;
use App\ExportResources\ExportResource;
use App\ExportResources\Account\Activity;
use App\ExportResources\Instance\SpecialDate;
use App\Models\Contact\Reminder as ContactReminder;
class Contact extends ExportResource
{
protected $columns = [
'uuid',
'created_at',
'updated_at',
];
protected $properties = [
'first_name',
'middle_name',
'last_name',
'nickname',
'description',
'is_starred',
'is_partial',
'is_active',
'is_dead',
'job',
'company',
'food_preferences',
'last_talked_to',
'last_consulted_at',
'number_of_views',
'stay_in_touch_frequency',
'stay_in_touch_trigger_date',
'vcard',
'distant_etag',
];
public function data(): ?array
{
return [
'properties' => [
'avatar' => [
'avatar_source' => $this->avatar_source,
'avatar_gravatar_url' => $this->avatar_gravatar_url,
'avatar_adorable_uuid' => $this->avatar_adorable_uuid,
'avatar_default_url' => $this->avatar_default_url,
$this->mergeWhen($this->avatarPhoto !== null, function () {
return ['avatar_photo' => $this->avatarPhoto->uuid];
}),
'has_avatar' => $this->has_avatar,
'avatar_external_url' => $this->avatar_external_url,
'avatar_file_name' => $this->avatar_file_name,
'avatar_location' => $this->avatar_location,
'gravatar_url' => $this->gravatar_url,
'default_avatar_color' => $this->default_avatar_color,
],
'tags' => $this->when($this->tags->count() > 0, function () {
return $this->tags->map(function ($tag) {
return $tag->name;
})->toArray();
}),
$this->mergeWhen($this->gender !== null, function () {
return ['gender' => $this->gender->uuid];
}),
$this->mergeWhen($this->birthdate, [
'birthdate' => new SpecialDate($this->birthdate),
]),
$this->mergeWhen($this->deceasedDate, [
'deceased_date' => new SpecialDate($this->deceasedDate),
]),
$this->mergeWhen($this->deceased_reminder_id, function () {
return ['deceased_reminder' => new Reminder(ContactReminder::find($this->deceased_reminder_id))];
}),
$this->mergeWhen($this->firstMetDate, [
'first_met_date' => new SpecialDate($this->firstMetDate),
]),
$this->mergeWhen($this->getIntroducer() !== null, function () {
return ['first_met_through' => $this->getIntroducer()->uuid];
}),
$this->mergeWhen($this->first_met_reminder_id !== null, function () {
return ['first_met_reminder' => new Reminder(ContactReminder::find($this->first_met_reminder_id))];
}),
],
'data' => [
Call::countCollection($this->calls),
ContactField::countCollection($this->contactFields),
Debt::countCollection($this->debts),
Gift::countCollection($this->gifts),
Note::countCollection($this->notes),
Reminder::countCollection($this->reminders),
Task::countCollection($this->tasks),
Address::countCollection($this->addresses),
Pet::countCollection($this->pets),
Conversation::countCollection($this->conversations),
LifeEvent::countCollection($this->lifeEvents),
Activity::uuidCollection($this->activities),
Photo::uuidCollection($this->photos),
Document::uuidCollection($this->documents),
// Occupation::collection($this->occupations),
],
];
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\ExportResources\Contact;
use App\ExportResources\ExportResource;
class ContactField extends ExportResource
{
protected $columns = [
'uuid',
'created_at',
'updated_at',
];
protected $properties = [
'data',
];
public function data(): ?array
{
return [
'properties' => [
$this->mergeWhen($this->contactFieldType !== null, function () {
return ['type' => $this->contactFieldType->uuid];
}),
],
];
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\ExportResources\Contact;
use App\ExportResources\ExportResource;
class ContactFieldType extends ExportResource
{
protected $columns = [
'uuid',
'created_at',
'updated_at',
];
protected $properties = [
'name',
'fontawesome_icon',
'protocol',
'delible',
'type',
];
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\ExportResources\Contact;
use App\ExportResources\ExportResource;
class Conversation extends ExportResource
{
protected $columns = [
'uuid',
'created_at',
'updated_at',
];
protected $properties = [
'happened_at',
];
public function data(): ?array
{
return [
'properties' => [
$this->mergeWhen($this->contactFieldType !== null, function () {
return ['contact_field_type' => $this->contactFieldType->uuid];
}),
'messages' => Message::collection($this->messages),
],
];
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\ExportResources\Contact;
use App\ExportResources\ExportResource;
class Debt extends ExportResource
{
protected $columns = [
'uuid',
'created_at',
'updated_at',
];
protected $properties = [
'amount',
'currency',
'status',
];
public function data(): ?array
{
return [
'properties' => [
'in_debt' => $this->in_debt === 'yes',
],
];
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App\ExportResources\Contact;
use App\ExportResources\ExportResource;
class Document extends ExportResource
{
protected $columns = [
'uuid',
'created_at',
'updated_at',
];
protected $properties = [
'original_filename',
'filesize',
'type',
'mime_type',
'number_of_downloads',
];
public function data(): ?array
{
return [
'properties' => [
$this->mergeWhen(($dataUrl = $this->dataUrl()) !== null, [
'dataUrl' => $dataUrl,
]),
],
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\ExportResources\Contact;
use App\ExportResources\ExportResource;
class Gender extends ExportResource
{
protected $columns = [
'uuid',
'created_at',
'updated_at',
];
protected $properties = [
'name',
'type',
];
}

View File

@@ -0,0 +1,37 @@
<?php
namespace App\ExportResources\Contact;
use App\ExportResources\ExportResource;
class Gift extends ExportResource
{
protected $columns = [
'uuid',
'created_at',
'updated_at',
];
protected $properties = [
'name',
'comment',
'url',
'amount',
'status',
'date',
];
public function data(): ?array
{
return [
'properties' => [
$this->mergeWhen($this->recipient !== null, function () {
return ['recipient' => $this->recipient->uuid];
}),
$this->mergeWhen($this->photos->count() > 0, [
'photos' => $this->photos->mapUuid(),
]),
],
];
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace App\ExportResources\Contact;
use App\ExportResources\ExportResource;
class LifeEvent extends ExportResource
{
protected $columns = [
'uuid',
'created_at',
'updated_at',
];
protected $properties = [
'name',
'note',
'happened_at',
'specific_information',
];
public function data(): ?array
{
return [
'properties' => [
$this->mergeWhen($this->lifeEventType != null, function () {
return ['type' => $this->lifeEventType->uuid];
}),
],
];
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\ExportResources\Contact;
use App\ExportResources\ExportResource;
class Message extends ExportResource
{
protected $columns = [
'uuid',
'created_at',
'updated_at',
];
protected $properties = [
'content',
'written_at',
'written_by_me',
];
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\ExportResources\Contact;
use App\ExportResources\ExportResource;
class Note extends ExportResource
{
protected $columns = [
'uuid',
'created_at',
'updated_at',
];
protected $properties = [
'body',
'is_favorite',
'favorited_at',
];
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\ExportResources\Contact;
use App\ExportResources\ExportResource;
class Pet extends ExportResource
{
protected $columns = [
'uuid',
'created_at',
'updated_at',
];
protected $properties = [
'name',
];
public function data(): ?array
{
return [
'properties' => [
$this->mergeWhen($this->petCategory !== null, function () {
return ['category' => $this->petCategory->name];
}),
],
];
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\ExportResources\Contact;
use App\ExportResources\ExportResource;
class Reminder extends ExportResource
{
protected $columns = [
'uuid',
'created_at',
'updated_at',
];
protected $properties = [
'initial_date',
'title',
'description',
'frequency_type',
'frequency_number',
'delible',
'inactive',
];
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\ExportResources\Contact;
use App\ExportResources\ExportResource;
class Task extends ExportResource
{
protected $columns = [
'uuid',
'created_at',
'updated_at',
];
protected $properties = [
'title',
'description',
'completed',
'completed_at',
];
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\ExportResources;
use Illuminate\Support\Str;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
class CountResourceCollection extends AnonymousResourceCollection
{
/**
* Transform the resource into a JSON array.
*
* @param \Illuminate\Http\Request $request
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
*/
public function toArray($request)
{
return [
'count' => $this->count(),
'type' => Str::of($this->collects)->afterLast('\\')->kebab()->replace('-', '_'),
'values' => parent::toArray($request),
];
}
}

View File

@@ -0,0 +1,137 @@
<?php
namespace App\ExportResources;
use Illuminate\Support\Arr;
use Illuminate\Http\Resources\MissingValue;
use Illuminate\Http\Resources\Json\JsonResource;
class ExportResource extends JsonResource
{
/**
* The resource instance.
*
* @var array
*/
protected $columns = [];
/**
* The resource instance.
*
* @var array
*/
protected $properties = null;
/**
* Create a new resource instance.
*
* @param mixed $resource
* @return void
*/
final public function __construct($resource)
{
$this->resource = $resource;
}
/**
* Create a new anonymous resource collection.
*
* @param mixed $resource
* @return CountResourceCollection|MissingValue
*/
public static function countCollection($resource)
{
if ($resource->count() === 0) {
return new MissingValue();
}
return tap(new CountResourceCollection($resource, static::class), function ($collection) {
if (property_exists(static::class, 'preserveKeys')) {
$collection->preserveKeys = (new static([]))->preserveKeys === true;
}
});
}
/**
* Create a new anonymous resource collection.
*
* @param mixed $resource
* @return MapUuidResourceCollection|MissingValue
*/
public static function uuidCollection($resource)
{
if ($resource->count() === 0) {
return new MissingValue();
}
return tap(new MapUuidResourceCollection($resource, static::class), function ($collection) {
if (property_exists(static::class, 'preserveKeys')) {
$collection->preserveKeys = (new static([]))->preserveKeys === true;
}
});
}
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
*/
public function toArray($request)
{
if (is_null($this->resource)) {
return [];
}
return is_array($this->resource)
? $this->resource
: $this->export($this->columns, $this->properties, $this->data());
}
/**
* @return array|null
*/
public function data(): ?array
{
return null;
}
/**
* Create the Insert query for the given table.
*
* @param array $columns
* @param array $properties
* @param array $data
* @return array|null
*/
protected function export(array $columns, array $properties = null, array $data = null): ?array
{
$result = [];
if (! $this->resource->exists()) {
return null;
}
foreach ($columns as $column) {
$result[$column] = $this->{$column};
}
if ($data !== null) {
foreach ($data as $key => $value) {
if (isset($result[$key]) && is_array($result[$key])) {
$result[$key] = array_merge($result[$key], $value);
} else {
$result[$key] = $value;
}
}
}
if ($properties !== null) {
$result['properties'] = array_merge(collect($properties)->mapWithKeys(function ($item, $key) {
return ($value = $this->{$item}) !== null ? [$item => $value] : new MissingValue();
})->toArray(), Arr::get($result, 'properties', []));
}
return $result;
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace App\ExportResources\Instance;
use App\ExportResources\ExportResource;
class AuditLog extends ExportResource
{
protected $columns = [
'created_at',
'updated_at',
];
protected $properties = [
'author_name',
'action',
'objects',
'audited_at',
'should_appear_on_dashboard',
];
public function data(): ?array
{
return [
'properties' => [
'author' => $this->when($this->author !== null, function () {
return $this->author->uuid;
}),
'contact' => $this->when($this->contact !== null, function () {
return $this->contact->uuid;
}),
],
];
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\ExportResources\Instance\Emotion;
use App\ExportResources\ExportResource;
class Emotion extends ExportResource
{
protected $columns = [
'created_at',
'updated_at',
];
protected $properties = [
'name',
];
public function data(): ?array
{
return [
'properties' => [
'primary' => $this->primary->name,
'secondary' => $this->secondary->name,
],
];
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\ExportResources\Instance;
use App\ExportResources\ExportResource;
class SpecialDate extends ExportResource
{
protected $columns = [
'uuid',
'is_age_based',
'is_year_unknown',
'date',
'created_at',
'updated_at',
];
}

View File

@@ -0,0 +1,50 @@
<?php
namespace App\ExportResources\Journal;
use App\ExportResources\ExportResource;
class JournalEntry extends ExportResource
{
protected $columns = [
'created_at',
'updated_at',
];
protected $properties = [
'date',
];
public function data(): ?array
{
$data = $this->getObjectData();
if ($data !== null) {
switch ($data['type']) {
case 'entry':
return [
'uuid' => $this->journalable->uuid,
'properties' => [
'type' => $data['type'],
'title' => $data['title'],
'post' => $data['post'],
'date' => $data['date'],
],
];
case 'day':
return [
'uuid' => $this->journalable->uuid,
'properties' => [
'type' => $data['type'],
'rate' => $data['rate'],
'comment' => $data['comment'],
'day' => $data['day'],
'month' => $data['month'],
'year' => $data['year'],
],
];
}
}
return null;
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\ExportResources;
use Illuminate\Support\Str;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
class MapUuidResourceCollection extends AnonymousResourceCollection
{
/**
* Transform the resource into a JSON array.
*
* @param \Illuminate\Http\Request $request
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
*/
public function toArray($request)
{
return [
'count' => $this->count(),
'type' => Str::of($this->collects)->afterLast('\\')->kebab()->replace('-', '_'),
'values' => $this->collection->mapUuid(),
];
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\ExportResources\Relationship;
use App\ExportResources\ExportResource;
class Relationship extends ExportResource
{
protected $columns = [
'uuid',
'created_at',
'updated_at',
];
public function data(): ?array
{
return [
'properties' => [
'type' => $this->relationshipType->name,
'contact_is' => $this->contactIs->uuid,
'of_contact' => $this->ofContact->uuid,
],
];
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\ExportResources\User;
use App\ExportResources\ExportResource;
class Module extends ExportResource
{
protected $columns = [
'key',
'translation_key',
'created_at',
'updated_at',
];
protected $properties = [
'active',
'delible',
];
}

View File

@@ -0,0 +1,18 @@
<?php
namespace App\ExportResources\User;
use App\ExportResources\ExportResource;
class SyncToken extends ExportResource
{
protected $columns = [
'created_at',
'updated_at',
];
protected $properties = [
'name',
'timestamp',
];
}

View File

@@ -0,0 +1,61 @@
<?php
namespace App\ExportResources\User;
use App\Models\Contact\Contact;
use App\ExportResources\ExportResource;
use Illuminate\Http\Resources\MissingValue;
class User extends ExportResource
{
protected $columns = [
'uuid',
'first_name',
'last_name',
'email',
'email_verified_at',
'google2fa_secret',
'created_at',
'updated_at',
];
protected $properties = [
'locale',
'metric',
'fluid_container',
'contacts_sort_order',
'name_order',
'dashboard_active_tab',
'gifts_active_tab',
'profile_active_tab',
'timezone',
'profile_new_life_event_badge_seen',
'temperature_scale',
];
public function data(): ?array
{
return [
'properties' => [
$this->mergeWhen($this->currency !== null, [
'currency' => $this->currency->iso,
]),
$this->mergeWhen($this->invited_by_user_id !== null, function () {
try {
$invited_by_user = Contact::where('account_id', $this->account_id)
->findOrFail($this->invited_by_user_id);
return [
'invited_by_user' => $invited_by_user->uuid,
];
} catch (\Exception $e) {
return new MissingValue();
}
}),
$this->mergeWhen($this->me !== null, function () {
return ['me_contact' => $this->me->uuid];
}),
],
];
}
}

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;
}
}

Some files were not shown because too many files have changed in this diff Show More