Page MenuHomePhorge

No OneTemporary

diff --git a/src/app/Console/Command.php b/src/app/Console/Command.php
new file mode 100644
index 00000000..2036fb70
--- /dev/null
+++ b/src/app/Console/Command.php
@@ -0,0 +1,104 @@
+<?php
+
+namespace App\Console;
+
+class Command extends \Illuminate\Console\Command
+{
+ /**
+ * Find the domain.
+ *
+ * @param string $domain Domain ID or namespace
+ *
+ * @return \App\Domain|null
+ */
+ public function getDomain($domain)
+ {
+ return $this->getObject(\App\Domain::class, $domain, 'namespace');
+ }
+
+ /**
+ * Find an object.
+ *
+ * @param string $objectClass The name of the class
+ * @param string $objectIdOrTitle The name of a database field to match.
+ * @param string|null $objectTitle An additional database field to match.
+ *
+ * @return mixed
+ */
+ public function getObject($objectClass, $objectIdOrTitle, $objectTitle)
+ {
+ $object = $objectClass::find($objectIdOrTitle);
+
+ if (!$object && !empty($objectTitle)) {
+ $object = $objectClass::where($objectTitle, $objectIdOrTitle)->first();
+ }
+
+ return $object;
+ }
+
+ /**
+ * Find the user.
+ *
+ * @param string $user User ID or email
+ *
+ * @return \App\User|null
+ */
+ public function getUser($user)
+ {
+ return $this->getObject(\App\User::class, $user, 'email');
+ }
+
+ /**
+ * Find the wallet.
+ *
+ * @param string $wallet Wallet ID
+ *
+ * @return \App\Wallet|null
+ */
+ public function getWallet($wallet)
+ {
+ return $this->getObject(\App\Wallet::class, $wallet, null);
+ }
+
+ /**
+ * Return a string for output, with any additional attributes specified as well.
+ *
+ * @param mixed $entry An object
+ *
+ * @return string
+ */
+ protected function toString($entry)
+ {
+ /**
+ * Haven't figured out yet, how to test if this command implements an option for additional
+ * attributes.
+ if (!in_array('attr', $this->options())) {
+ return $entry->{$entry->getKeyName()};
+ }
+ */
+
+ $str = [
+ $entry->{$entry->getKeyName()}
+ ];
+
+ foreach ($this->option('attr') as $attr) {
+ if ($attr == $entry->getKeyName()) {
+ $this->warn("Specifying {$attr} is not useful.");
+ continue;
+ }
+
+ if (!array_key_exists($attr, $entry->toArray())) {
+ $this->error("Attribute {$attr} isn't available");
+ continue;
+ }
+
+ if (is_numeric($entry->{$attr})) {
+ $str[] = $entry->{$attr};
+ } else {
+ $str[] = !empty($entry->{$attr}) ? $entry->{$attr} : "null";
+ }
+ }
+
+ return implode(" ", $str);
+ }
+}
diff --git a/src/app/Console/Commands/DataCountries.php b/src/app/Console/Commands/Data/Import/CountriesCommand.php
similarity index 56%
rename from src/app/Console/Commands/DataCountries.php
rename to src/app/Console/Commands/Data/Import/CountriesCommand.php
index ac94cdf6..080a54b2 100644
--- a/src/app/Console/Commands/DataCountries.php
+++ b/src/app/Console/Commands/Data/Import/CountriesCommand.php
@@ -1,97 +1,114 @@
<?php
-namespace App\Console\Commands;
+namespace App\Console\Commands\Data\Import;
-use Illuminate\Console\Command;
+use App\Console\Command;
+use Carbon\Carbon;
-class DataCountries extends Command
+class CountriesCommand extends Command
{
private $currency_fixes = [
// Country code => currency
'LT' => 'EUR',
];
/**
* The name and signature of the console command.
*
* @var string
*/
- protected $signature = 'data:countries';
+ protected $signature = 'data:import:countries';
/**
* The console command description.
*
* @var string
*/
- protected $description = 'Fetches countries map from wikipedia';
+ protected $description = 'Fetches countries map from country.io';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
+ $today = Carbon::now()->toDateString();
+
$countries = [];
$currencies = [];
- $currencies_url = 'http://country.io/currency.json';
- $countries_url = 'http://country.io/names.json';
-
- $this->info("Fetching currencies from $currencies_url...");
+ $currencySource = 'http://country.io/currency.json';
+ $countrySource = 'http://country.io/names.json';
- // fetch currency table and create an index by country page url
- $currencies_json = file_get_contents($currencies_url);
-
- if (!$currencies_json) {
- $this->error("Failed to fetch currencies");
- return;
- }
+ //
+ // countries
+ //
+ $file = storage_path("countries-{$today}.json");
- $this->info("Fetching countries from $countries_url...");
+ \App\Utils::downloadFile($countrySource, $file);
- $countries_json = file_get_contents($countries_url);
+ $countryJson = file_get_contents($file);
- if (!$countries_json) {
+ if (!$countryJson) {
$this->error("Failed to fetch countries");
- return;
+ return 1;
}
- $currencies = json_decode($currencies_json, true);
- $countries = json_decode($countries_json, true);
+ $countries = json_decode($countryJson, true);
if (!is_array($countries) || empty($countries)) {
$this->error("Invalid countries data");
+ return 1;
+ }
+
+ //
+ // currencies
+ //
+ $file = storage_path("currencies-{$today}.json");
+
+ \App\Utils::downloadFile($currencySource, $file);
+
+ // fetch currency table and create an index by country page url
+ $currencyJson = file_get_contents($file);
+
+ if (!$currencyJson) {
+ $this->error("Failed to fetch currencies");
return;
}
+ $currencies = json_decode($currencyJson, true);
+
if (!is_array($currencies) || empty($currencies)) {
$this->error("Invalid currencies data");
- return;
+ return 1;
}
+ //
+ // export
+ //
$file = resource_path('countries.php');
- $this->info("Generating resource file $file...");
-
asort($countries);
$out = "<?php return [\n";
+
foreach ($countries as $code => $name) {
$currency = $currencies[$code] ?? null;
if (!empty($this->currency_fixes[$code])) {
$currency = $this->currency_fixes[$code];
}
if (!$currency) {
- $this->error("Unknown currency for {$name} ({$code}). Skipped.");
+ $this->warn("Unknown currency for {$name} ({$code}). Skipped.");
continue;
}
$out .= sprintf(" '%s' => ['%s','%s'],\n", $code, $currency, addslashes($name));
}
+
$out .= "];\n";
file_put_contents($file, $out);
}
}
diff --git a/src/app/Console/Commands/Data/Import/IP4NetsCommand.php b/src/app/Console/Commands/Data/Import/IP4NetsCommand.php
new file mode 100644
index 00000000..5200fd78
--- /dev/null
+++ b/src/app/Console/Commands/Data/Import/IP4NetsCommand.php
@@ -0,0 +1,225 @@
+<?php
+
+namespace App\Console\Commands\Data\Import;
+
+use App\Console\Command;
+use Carbon\Carbon;
+
+class IP4NetsCommand extends Command
+{
+ /**
+ * The name and signature of the console command.
+ *
+ * @var string
+ */
+ protected $signature = 'data:import:ip4nets';
+
+ /**
+ * The console command description.
+ *
+ * @var string
+ */
+ protected $description = 'Update IP4 Networks';
+
+ /**
+ * Create a new command instance.
+ *
+ * @return void
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+ /**
+ * Execute the console command.
+ *
+ * @return int
+ */
+ public function handle()
+ {
+ $rirs = [
+ 'afrinic' => 'http://ftp.afrinic.net/stats/afrinic/delegated-afrinic-latest',
+ 'apnic' => 'http://ftp.apnic.net/apnic/stats/apnic/delegated-apnic-latest',
+ 'arin' => 'http://ftp.arin.net/pub/stats/arin/delegated-arin-extended-latest',
+ 'lacnic' => 'http://ftp.lacnic.net/pub/stats/lacnic/delegated-lacnic-latest',
+ 'ripencc' => 'https://ftp.ripe.net/ripe/stats/delegated-ripencc-latest'
+ ];
+
+ $today = Carbon::now()->toDateString();
+
+ foreach ($rirs as $rir => $url) {
+ $file = storage_path("{$rir}-{$today}");
+
+ \App\Utils::downloadFile($url, $file);
+
+ $serial = $this->serialFromStatsFile($file);
+
+ if (!$serial) {
+ \Log::error("Can not derive serial from {$file}");
+ continue;
+ }
+
+ $numLines = $this->countLines($file);
+
+ if (!$numLines) {
+ \Log::error("No relevant lines could be found in {$file}");
+ continue;
+ }
+
+ $bar = \App\Utils::createProgressBar(
+ $this->output,
+ $numLines,
+ "Importing IPv4 Networks from {$file}"
+ );
+
+ $fp = fopen($file, 'r');
+
+ $nets = [];
+
+ while (!feof($fp)) {
+ $line = trim(fgets($fp));
+
+ if ($line == "") {
+ continue;
+ }
+
+ if ((int)$line) {
+ continue;
+ }
+
+ if ($line[0] == "#") {
+ continue;
+ }
+
+ $items = explode('|', $line);
+
+ if (sizeof($items) < 7) {
+ continue;
+ }
+
+ if ($items[1] == "*") {
+ continue;
+ }
+
+ if ($items[2] != "ipv4") {
+ continue;
+ }
+
+ if ($items[5] == "00000000") {
+ $items[5] = "19700102";
+ }
+
+ if ($items[1] == "" || $items[1] == "ZZ") {
+ continue;
+ }
+
+ $bar->advance();
+
+ $mask = 32 - log($items[4], 2);
+
+ $net = \App\IP4Net::where(
+ [
+ 'net_number' => $items[3],
+ 'net_mask' => $mask,
+ 'net_broadcast' => long2ip((ip2long($items[3]) + 2 ** (32 - $mask)) - 1)
+ ]
+ )->first();
+
+ if ($net) {
+ if ($net->updated_at > Carbon::now()->subDays(1)) {
+ continue;
+ }
+
+ // don't use ->update() method because it doesn't update updated_at which we need for expiry
+ $net->rir_name = $rir;
+ $net->country = $items[1];
+ $net->serial = $serial;
+ $net->updated_at = Carbon::now();
+
+ $net->save();
+
+ continue;
+ }
+
+ $nets[] = [
+ 'rir_name' => $rir,
+ 'net_number' => $items[3],
+ 'net_mask' => $mask,
+ 'net_broadcast' => long2ip((ip2long($items[3]) + 2 ** (32 - $mask)) - 1),
+ 'country' => $items[1],
+ 'serial' => $serial,
+ 'created_at' => Carbon::parse($items[5], 'UTC'),
+ 'updated_at' => Carbon::now()
+ ];
+
+ if (sizeof($nets) >= 100) {
+ \App\IP4Net::insert($nets);
+ $nets = [];
+ }
+ }
+
+ if (sizeof($nets) > 0) {
+ \App\IP4Net::insert($nets);
+ $nets = [];
+ }
+
+ $bar->finish();
+
+ $this->info("DONE");
+ }
+
+ return 0;
+ }
+
+ private function countLines($file)
+ {
+ $numLines = 0;
+
+ $fh = fopen($file, 'r');
+
+ while (!feof($fh)) {
+ $line = trim(fgets($fh));
+
+ $items = explode('|', $line);
+
+ if (sizeof($items) < 3) {
+ continue;
+ }
+
+ if ($items[2] == "ipv4") {
+ $numLines++;
+ }
+ }
+
+ fclose($fh);
+
+ return $numLines;
+ }
+
+ private function serialFromStatsFile($file)
+ {
+ $serial = null;
+
+ $fh = fopen($file, 'r');
+
+ while (!feof($fh)) {
+ $line = trim(fgets($fh));
+
+ $items = explode('|', $line);
+
+ if (sizeof($items) < 2) {
+ continue;
+ }
+
+ if ((int)$items[2]) {
+ $serial = (int)$items[2];
+ break;
+ }
+ }
+
+ fclose($fh);
+
+ return $serial;
+ }
+}
diff --git a/src/app/Console/Commands/Data/Import/IP6NetsCommand.php b/src/app/Console/Commands/Data/Import/IP6NetsCommand.php
new file mode 100644
index 00000000..afb0c67a
--- /dev/null
+++ b/src/app/Console/Commands/Data/Import/IP6NetsCommand.php
@@ -0,0 +1,223 @@
+<?php
+
+namespace App\Console\Commands\Data\Import;
+
+use App\Console\Command;
+use Carbon\Carbon;
+
+class IP6NetsCommand extends Command
+{
+ /**
+ * The name and signature of the console command.
+ *
+ * @var string
+ */
+ protected $signature = 'data:import:ip6nets';
+
+ /**
+ * The console command description.
+ *
+ * @var string
+ */
+ protected $description = 'Import IP6 Networks.';
+
+ /**
+ * Create a new command instance.
+ *
+ * @return void
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+ /**
+ * Execute the console command.
+ *
+ * @return mixed
+ */
+ public function handle()
+ {
+ $rirs = [
+ 'afrinic' => 'http://ftp.afrinic.net/stats/afrinic/delegated-afrinic-latest',
+ 'apnic' => 'http://ftp.apnic.net/apnic/stats/apnic/delegated-apnic-latest',
+ 'arin' => 'http://ftp.arin.net/pub/stats/arin/delegated-arin-extended-latest',
+ 'lacnic' => 'http://ftp.lacnic.net/pub/stats/lacnic/delegated-lacnic-latest',
+ 'ripencc' => 'https://ftp.ripe.net/ripe/stats/delegated-ripencc-latest'
+ ];
+
+ $today = Carbon::now()->toDateString();
+
+ foreach ($rirs as $rir => $url) {
+ $file = storage_path("{$rir}-{$today}");
+
+ \App\Utils::downloadFile($url, $file);
+
+ $serial = $this->serialFromStatsFile($file);
+
+ if (!$serial) {
+ \Log::error("Can not derive serial from {$file}");
+ continue;
+ }
+
+ $numLines = $this->countLines($file);
+
+ if (!$numLines) {
+ \Log::error("No relevant lines could be found in {$file}");
+ continue;
+ }
+
+ $bar = \App\Utils::createProgressBar(
+ $this->output,
+ $numLines,
+ "Importing IPv6 Networks from {$file}"
+ );
+
+ $fp = fopen($file, 'r');
+
+ $nets = [];
+
+ while (!feof($fp)) {
+ $line = trim(fgets($fp));
+
+ if ($line == "") {
+ continue;
+ }
+
+ if ((int)$line) {
+ continue;
+ }
+
+ if ($line[0] == "#") {
+ continue;
+ }
+
+ $items = explode('|', $line);
+
+ if (sizeof($items) < 7) {
+ continue;
+ }
+
+ if ($items[1] == "*") {
+ continue;
+ }
+
+ if ($items[2] != "ipv6") {
+ continue;
+ }
+
+ if ($items[5] == "00000000") {
+ $items[5] = "19700102";
+ }
+
+ if ($items[1] == "" || $items[1] == "ZZ") {
+ continue;
+ }
+
+ $bar->advance();
+
+ $broadcast = \App\Utils::ip6Broadcast($items[3], (int)$items[4]);
+
+ $net = \App\IP6Net::where(
+ [
+ 'net_number' => $items[3],
+ 'net_mask' => (int)$items[4],
+ 'net_broadcast' => $broadcast
+ ]
+ )->first();
+
+ if ($net) {
+ if ($net->updated_at > Carbon::now()->subDays(1)) {
+ continue;
+ }
+
+ // don't use ->update() method because it doesn't update updated_at which we need for expiry
+ $net->rir_name = $rir;
+ $net->country = $items[1];
+ $net->serial = $serial;
+ $net->updated_at = Carbon::now();
+
+ $net->save();
+
+ continue;
+ }
+
+ $nets[] = [
+ 'rir_name' => $rir,
+ 'net_number' => $items[3],
+ 'net_mask' => (int)$items[4],
+ 'net_broadcast' => $broadcast,
+ 'country' => $items[1],
+ 'serial' => $serial,
+ 'created_at' => Carbon::parse($items[5], 'UTC'),
+ 'updated_at' => Carbon::now()
+ ];
+
+ if (sizeof($nets) >= 100) {
+ \App\IP6Net::insert($nets);
+ $nets = [];
+ }
+ }
+
+ if (sizeof($nets) > 0) {
+ \App\IP6Net::insert($nets);
+ $nets = [];
+ }
+
+ $bar->finish();
+
+ $this->info("DONE");
+ }
+ }
+
+ private function countLines($file)
+ {
+ $numLines = 0;
+
+ $fh = fopen($file, 'r');
+
+ while (!feof($fh)) {
+ $line = trim(fgets($fh));
+
+ $items = explode('|', $line);
+
+ if (sizeof($items) < 3) {
+ continue;
+ }
+
+ if ($items[2] == "ipv6") {
+ $numLines++;
+ }
+ }
+
+ fclose($fh);
+
+ return $numLines;
+ }
+
+ private function serialFromStatsFile($file)
+ {
+ $serial = null;
+
+ $fh = fopen($file, 'r');
+
+ while (!feof($fh)) {
+ $line = trim(fgets($fh));
+
+ $items = explode('|', $line);
+
+ if (sizeof($items) < 2) {
+ continue;
+ }
+
+ if ((int)$items[2]) {
+ $serial = (int)$items[2];
+ break;
+ }
+ }
+
+ fclose($fh);
+
+ return $serial;
+ }
+}
diff --git a/src/app/Console/Commands/Data/ImportCommand.php b/src/app/Console/Commands/Data/ImportCommand.php
new file mode 100644
index 00000000..2e9849e9
--- /dev/null
+++ b/src/app/Console/Commands/Data/ImportCommand.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace App\Console\Commands\Data;
+
+use Illuminate\Console\Command;
+
+class ImportCommand extends Command
+{
+ /**
+ * The name and signature of the console command.
+ *
+ * @var string
+ */
+ protected $signature = 'data:import';
+
+ /**
+ * The console command description.
+ *
+ * @var string
+ */
+ protected $description = 'Command description';
+
+ /**
+ * Create a new command instance.
+ *
+ * @return void
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+ /**
+ * Execute the console command.
+ *
+ * @return int
+ */
+ public function handle()
+ {
+ $commands = [
+ Import\CountriesCommand::class,
+ Import\IP4NetsCommand::class,
+ Import\IP6NetsCommand::class
+ ];
+
+ foreach ($commands as $command) {
+ $execution = new $command();
+ $execution->output = $this->output;
+ $execution->handle();
+ }
+
+ return 0;
+ }
+}
diff --git a/src/app/Console/Kernel.php b/src/app/Console/Kernel.php
index e9123e07..44ae80b2 100644
--- a/src/app/Console/Kernel.php
+++ b/src/app/Console/Kernel.php
@@ -1,47 +1,57 @@
<?php
namespace App\Console;
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 = [
//
];
/**
* Define the application's command schedule.
*
* @param Schedule $schedule The application's command schedule
*
* @return void
*/
protected function schedule(Schedule $schedule)
{
- // $schedule->command('inspire')
- // ->hourly();
+ // This command imports countries and the current set of IPv4 and IPv6 networks allocated to countries.
+ $schedule->command('data:import')->dailyAt('05:00');
+
+ $schedule->command('wallet:charge')->dailyAt('00:00');
+ $schedule->command('wallet:charge')->dailyAt('04:00');
+ $schedule->command('wallet:charge')->dailyAt('08:00');
+ $schedule->command('wallet:charge')->dailyAt('12:00');
+ $schedule->command('wallet:charge')->dailyAt('16:00');
+ $schedule->command('wallet:charge')->dailyAt('20:00');
+
+ // this is a laravel 8-ism
+ //$schedule->command('wallet:charge')->everyFourHours();
}
/**
* Register the commands for the application.
*
* @return void
*/
protected function commands()
{
$this->load(__DIR__ . '/Commands');
if (\app('env') != 'production') {
$this->load(__DIR__ . '/Development');
}
include base_path('routes/console.php');
}
}
diff --git a/src/app/IP4Net.php b/src/app/IP4Net.php
new file mode 100644
index 00000000..1e57ba77
--- /dev/null
+++ b/src/app/IP4Net.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace App;
+
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Support\Facades\DB;
+
+class IP4Net extends Model
+{
+ protected $table = "ip4nets";
+
+ protected $fillable = [
+ 'net_number',
+ 'net_mask',
+ 'net_broadcast',
+ 'country',
+ 'serial'
+ ];
+}
diff --git a/src/app/IP6Net.php b/src/app/IP6Net.php
new file mode 100644
index 00000000..30f1782f
--- /dev/null
+++ b/src/app/IP6Net.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace App;
+
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Support\Facades\DB;
+
+class IP6Net extends Model
+{
+ protected $table = "ip6nets";
+
+ protected $fillable = [
+ 'rir_name',
+ 'net_number',
+ 'net_mask',
+ 'net_broadcast',
+ 'country',
+ 'serial',
+ 'created_at',
+ 'updated_at'
+ ];
+
+ public static function getNet($ip, $mask = 128)
+ {
+ $query = "
+ SELECT id FROM ip6nets
+ WHERE INET6_ATON(net_number) <= INET6_ATON(?)
+ AND INET6_ATON(net_broadcast) >= INET6_ATON(?)
+ ORDER BY INET6_ATON(net_number), net_mask DESC LIMIT 1
+ ";
+
+ $results = DB::select($query, [$ip, $ip]);
+
+ if (sizeof($results) == 0) {
+ return null;
+ }
+
+ return \App\IP6Net::find($results[0]->id);
+ }
+}
diff --git a/src/app/Observers/UserObserver.php b/src/app/Observers/UserObserver.php
index e9c16ae5..62891f82 100644
--- a/src/app/Observers/UserObserver.php
+++ b/src/app/Observers/UserObserver.php
@@ -1,261 +1,261 @@
<?php
namespace App\Observers;
use App\Entitlement;
use App\Domain;
use App\Transaction;
use App\User;
use App\Wallet;
use Illuminate\Support\Facades\DB;
class UserObserver
{
/**
* Handle the "creating" event.
*
* Ensure that the user is created with a random, large integer.
*
* @param \App\User $user The user being created.
*
* @return void
*/
public function creating(User $user)
{
if (!$user->id) {
while (true) {
$allegedly_unique = \App\Utils::uuidInt();
if (!User::find($allegedly_unique)) {
$user->{$user->getKeyName()} = $allegedly_unique;
break;
}
}
}
$user->email = \strtolower($user->email);
// only users that are not imported get the benefit of the doubt.
$user->status |= User::STATUS_NEW | User::STATUS_ACTIVE;
// can't dispatch job here because it'll fail serialization
}
/**
* Handle the "created" event.
*
* Ensures the user has at least one wallet.
*
* Should ensure some basic settings are available as well.
*
* @param \App\User $user The user created.
*
* @return void
*/
public function created(User $user)
{
$settings = [
- 'country' => 'CH',
+ 'country' => \App\Utils::countryForRequest(),
'currency' => 'CHF',
/*
'first_name' => '',
'last_name' => '',
'billing_address' => '',
'organization' => '',
'phone' => '',
'external_email' => '',
*/
];
foreach ($settings as $key => $value) {
$settings[$key] = [
'key' => $key,
'value' => $value,
'user_id' => $user->id,
];
}
// Note: Don't use setSettings() here to bypass UserSetting observers
// Note: This is a single multi-insert query
$user->settings()->insert(array_values($settings));
$user->wallets()->create();
// Create user record in LDAP, then check if the account is created in IMAP
$chain = [
new \App\Jobs\User\VerifyJob($user->id),
];
\App\Jobs\User\CreateJob::withChain($chain)->dispatch($user->id);
}
/**
* Handle the "deleted" event.
*
* @param \App\User $user The user deleted.
*
* @return void
*/
public function deleted(User $user)
{
//
}
/**
* Handle the "deleting" event.
*
* @param User $user The user that is being deleted.
*
* @return void
*/
public function deleting(User $user)
{
if ($user->isForceDeleting()) {
$this->forceDeleting($user);
return;
}
// TODO: Especially in tests we're doing delete() on a already deleted user.
// Should we escape here - for performance reasons?
// TODO: I think all of this should use database transactions
// Entitlements do not have referential integrity on the entitled object, so this is our
// way of doing an onDelete('cascade') without the foreign key.
$entitlements = Entitlement::where('entitleable_id', $user->id)
->where('entitleable_type', User::class)->get();
foreach ($entitlements as $entitlement) {
$entitlement->delete();
}
// Remove owned users/domains
$wallets = $user->wallets()->pluck('id')->all();
$assignments = Entitlement::whereIn('wallet_id', $wallets)->get();
$users = [];
$domains = [];
$entitlements = [];
foreach ($assignments as $entitlement) {
if ($entitlement->entitleable_type == Domain::class) {
$domains[] = $entitlement->entitleable_id;
} elseif ($entitlement->entitleable_type == User::class && $entitlement->entitleable_id != $user->id) {
$users[] = $entitlement->entitleable_id;
} else {
$entitlements[] = $entitlement->id;
}
}
$users = array_unique($users);
$domains = array_unique($domains);
// Domains/users/entitlements need to be deleted one by one to make sure
// events are fired and observers can do the proper cleanup.
if (!empty($users)) {
foreach (User::whereIn('id', $users)->get() as $_user) {
$_user->delete();
}
}
if (!empty($domains)) {
foreach (Domain::whereIn('id', $domains)->get() as $_domain) {
$_domain->delete();
}
}
if (!empty($entitlements)) {
Entitlement::whereIn('id', $entitlements)->delete();
}
// FIXME: What do we do with user wallets?
\App\Jobs\User\DeleteJob::dispatch($user->id);
}
/**
* Handle the "deleting" event on forceDelete() call.
*
* @param User $user The user that is being deleted.
*
* @return void
*/
public function forceDeleting(User $user)
{
// TODO: We assume that at this moment all belongings are already soft-deleted.
// Remove owned users/domains
$wallets = $user->wallets()->pluck('id')->all();
$assignments = Entitlement::withTrashed()->whereIn('wallet_id', $wallets)->get();
$entitlements = [];
$domains = [];
$users = [];
foreach ($assignments as $entitlement) {
$entitlements[] = $entitlement->id;
if ($entitlement->entitleable_type == Domain::class) {
$domains[] = $entitlement->entitleable_id;
} elseif (
$entitlement->entitleable_type == User::class
&& $entitlement->entitleable_id != $user->id
) {
$users[] = $entitlement->entitleable_id;
}
}
$users = array_unique($users);
$domains = array_unique($domains);
// Remove the user "direct" entitlements explicitely, if they belong to another
// user's wallet they will not be removed by the wallets foreign key cascade
Entitlement::withTrashed()
->where('entitleable_id', $user->id)
->where('entitleable_type', User::class)
->forceDelete();
// Users need to be deleted one by one to make sure observers can do the proper cleanup.
if (!empty($users)) {
foreach (User::withTrashed()->whereIn('id', $users)->get() as $_user) {
$_user->forceDelete();
}
}
// Domains can be just removed
if (!empty($domains)) {
Domain::withTrashed()->whereIn('id', $domains)->forceDelete();
}
// Remove transactions, they also have no foreign key constraint
Transaction::where('object_type', Entitlement::class)
->whereIn('object_id', $entitlements)
->delete();
Transaction::where('object_type', Wallet::class)
->whereIn('object_id', $wallets)
->delete();
}
/**
* Handle the "retrieving" event.
*
* @param User $user The user that is being retrieved.
*
* @todo This is useful for audit.
*
* @return void
*/
public function retrieving(User $user)
{
// TODO \App\Jobs\User\ReadJob::dispatch($user->id);
}
/**
* Handle the "updating" event.
*
* @param User $user The user that is being updated.
*
* @return void
*/
public function updating(User $user)
{
\App\Jobs\User\UpdateJob::dispatch($user->id);
}
}
diff --git a/src/app/Utils.php b/src/app/Utils.php
index 1f48f22b..281fe7c8 100644
--- a/src/app/Utils.php
+++ b/src/app/Utils.php
@@ -1,152 +1,338 @@
<?php
namespace App;
use Carbon\Carbon;
use Illuminate\Support\Facades\Auth;
use Ramsey\Uuid\Uuid;
/**
* Small utility functions for App.
*/
class Utils
{
+ /**
+ * Count the number of lines in a file.
+ *
+ * Useful for progress bars.
+ *
+ * @param string $file The filepath to count the lines of.
+ *
+ * @return int
+ */
+ public static function countLines($file)
+ {
+ $fh = fopen($file, 'rb');
+ $numLines = 0;
+
+ while (!feof($fh)) {
+ $numLines += substr_count(fread($fh, 8192), "\n");
+ }
+
+ fclose($fh);
+
+ return $numLines;
+ }
+
+ /**
+ * Return the country ISO code for an IP address.
+ *
+ * @return string
+ */
+ public static function countryForIP($ip)
+ {
+ if (strpos(':', $ip) === false) {
+ $query = "
+ SELECT country FROM ip4nets
+ WHERE INET_ATON(net_number) <= INET_ATON(?)
+ AND INET_ATON(net_broadcast) >= INET_ATON(?)
+ ORDER BY INET_ATON(net_number), net_mask DESC LIMIT 1
+ ";
+ } else {
+ $query = "
+ SELECT id FROM ip6nets
+ WHERE INET6_ATON(net_number) <= INET6_ATON(?)
+ AND INET6_ATON(net_broadcast) >= INET6_ATON(?)
+ ORDER BY INET6_ATON(net_number), net_mask DESC LIMIT 1
+ ";
+ }
+
+ $nets = \Illuminate\Support\Facades\DB::select($query, [$ip, $ip]);
+
+ if (sizeof($nets) > 0) {
+ return $nets[0]->country;
+ }
+
+ return 'CH';
+ }
+
+ /**
+ * Return the country ISO code for the current request.
+ */
+ public static function countryForRequest()
+ {
+ $request = \request();
+ $ip = $request->ip();
+
+ return self::countryForIP($ip);
+ }
+
+ /**
+ * Shortcut to creating a progress bar of a particular format with a particular message.
+ *
+ * @param \Illuminate\Console\OutputStyle $output Console output object
+ * @param int $count Number of progress steps
+ * @param string $message The description
+ *
+ * @return \Symfony\Component\Console\Helper\ProgressBar
+ */
+ public static function createProgressBar($output, $count, $message = null)
+ {
+ $bar = $output->createProgressBar($count);
+
+ $bar->setFormat(
+ '%current:7s%/%max:7s% [%bar%] %percent:3s%% %elapsed:7s%/%estimated:-7s% %message% '
+ );
+
+ if ($message) {
+ $bar->setMessage($message . " ...");
+ }
+
+ $bar->start();
+
+ return $bar;
+ }
+
/**
* Return the number of days in the month prior to this one.
*
* @return int
*/
public static function daysInLastMonth()
{
$start = new Carbon('first day of last month');
$end = new Carbon('last day of last month');
return $start->diffInDays($end) + 1;
}
+ /**
+ * Download a file from the interwebz and store it locally.
+ *
+ * @param string $source The source location
+ * @param string $target The target location
+ * @param bool $force Force the download (and overwrite target)
+ *
+ * @return void
+ */
+ public static function downloadFile($source, $target, $force = false)
+ {
+ if (is_file($target) && !$force) {
+ return;
+ }
+
+ \Log::info("Retrieving {$source}");
+
+ $fp = fopen($target, 'w');
+
+ $curl = curl_init();
+ curl_setopt($curl, CURLOPT_URL, $source);
+ curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
+ curl_setopt($curl, CURLOPT_FILE, $fp);
+ curl_exec($curl);
+
+ if (curl_errno($curl)) {
+ \Log::error("Request error on {$source}: " . curl_error($curl));
+
+ curl_close($curl);
+ fclose($fp);
+
+ unlink($target);
+ return;
+ }
+
+ curl_close($curl);
+ fclose($fp);
+ }
+
+ /**
+ * Calculate the broadcast address provided a net number and a prefix.
+ *
+ * @param string $net A valid IPv6 network number.
+ * @param int $prefix The network prefix.
+ *
+ * @return string
+ */
+ public static function ip6Broadcast($net, $prefix)
+ {
+ $netHex = bin2hex(inet_pton($net));
+
+ // Overwriting first address string to make sure notation is optimal
+ $net = inet_ntop(hex2bin($netHex));
+
+ // Calculate the number of 'flexible' bits
+ $flexbits = 128 - $prefix;
+
+ // Build the hexadecimal string of the last address
+ $lastAddrHex = $netHex;
+
+ // We start at the end of the string (which is always 32 characters long)
+ $pos = 31;
+ while ($flexbits > 0) {
+ // Get the character at this position
+ $orig = substr($lastAddrHex, $pos, 1);
+
+ // Convert it to an integer
+ $origval = hexdec($orig);
+
+ // OR it with (2^flexbits)-1, with flexbits limited to 4 at a time
+ $newval = $origval | (pow(2, min(4, $flexbits)) - 1);
+
+ // Convert it back to a hexadecimal character
+ $new = dechex($newval);
+
+ // And put that character back in the string
+ $lastAddrHex = substr_replace($lastAddrHex, $new, $pos, 1);
+
+ // We processed one nibble, move to previous position
+ $flexbits -= 4;
+ $pos -= 1;
+ }
+
+ // Convert the hexadecimal string to a binary string
+ # Using pack() here
+ # Newer PHP version can use hex2bin()
+ $lastaddrbin = pack('H*', $lastAddrHex);
+
+ // And create an IPv6 address from the binary string
+ $lastaddrstr = inet_ntop($lastaddrbin);
+
+ return $lastaddrstr;
+ }
+
/**
* Provide all unique combinations of elements in $input, with order and duplicates irrelevant.
*
* @param array $input The input array of elements.
*
* @return array[]
*/
public static function powerSet(array $input): array
{
$output = [];
for ($x = 0; $x < count($input); $x++) {
self::combine($input, $x + 1, 0, [], 0, $output);
}
return $output;
}
/**
* Returns the current user's email address or null.
*
* @return string
*/
public static function userEmailOrNull(): ?string
{
$user = Auth::user();
if (!$user) {
return null;
}
return $user->email;
}
/**
* Returns a UUID in the form of an integer.
*
* @return integer
*/
public static function uuidInt(): int
{
$hex = Uuid::uuid4();
$bin = pack('h*', str_replace('-', '', $hex));
$ids = unpack('L', $bin);
$id = array_shift($ids);
return $id;
}
/**
* Returns a UUID in the form of a string.
*
* @return string
*/
public static function uuidStr(): string
{
return Uuid::uuid4()->toString();
}
private static function combine($input, $r, $index, $data, $i, &$output): void
{
$n = count($input);
// Current cobination is ready
if ($index == $r) {
$output[] = array_slice($data, 0, $r);
return;
}
// When no more elements are there to put in data[]
if ($i >= $n) {
return;
}
// current is included, put next at next location
$data[$index] = $input[$i];
self::combine($input, $r, $index + 1, $data, $i + 1, $output);
// current is excluded, replace it with next (Note that i+1
// is passed, but index is not changed)
self::combine($input, $r, $index, $data, $i + 1, $output);
}
/**
* Create self URL
*
* @param string $route Route/Path
*
* @return string Full URL
*/
public static function serviceUrl(string $route): string
{
$url = \config('app.public_url');
if (!$url) {
$url = \config('app.url');
}
return rtrim(trim($url, '/') . '/' . ltrim($route, '/'), '/');
}
/**
* Create a configuration/environment data to be passed to
* the UI
*
* @todo For a lack of better place this is put here for now
*
* @return array Configuration data
*/
public static function uiEnv(): array
{
$opts = ['app.name', 'app.url', 'app.domain'];
$env = \app('config')->getMany($opts);
$countries = include resource_path('countries.php');
$env['countries'] = $countries ?: [];
$isAdmin = strpos(request()->getHttpHost(), 'admin.') === 0;
$env['jsapp'] = $isAdmin ? 'admin.js' : 'user.js';
$env['paymentProvider'] = \config('services.payment_provider');
$env['stripePK'] = \config('services.stripe.public_key');
return $env;
}
}
diff --git a/src/composer.json b/src/composer.json
index 7e9c78b7..f719d82f 100644
--- a/src/composer.json
+++ b/src/composer.json
@@ -1,89 +1,85 @@
{
"name": "laravel/laravel",
"type": "project",
"description": "The Laravel Framework.",
"keywords": [
"framework",
"laravel"
],
"license": "MIT",
"repositories": [
{
"type": "vcs",
"url": "https://git.kolab.org/diffusion/PNL/php-net_ldap3.git"
}
],
"require": {
"php": "^7.1.3",
"barryvdh/laravel-dompdf": "^0.8.6",
- "doctrine/dbal": "^2.9",
+ "dyrynda/laravel-nullable-fields": "*",
"fideloper/proxy": "^4.0",
- "geoip2/geoip2": "^2.9",
- "iatstuti/laravel-nullable-fields": "*",
"kolab/net_ldap3": "dev-master",
"laravel/framework": "6.*",
"laravel/tinker": "^2.4",
"mollie/laravel-mollie": "^2.9",
"morrislaptop/laravel-queue-clear": "^1.2",
- "silviolleite/laravelpwa": "^1.0",
+ "silviolleite/laravelpwa": "^2.0",
"spatie/laravel-translatable": "^4.2",
"spomky-labs/otphp": "~4.0.0",
"stripe/stripe-php": "^7.29",
"swooletw/laravel-swoole": "^2.6",
- "torann/currency": "^1.0",
- "torann/geoip": "^1.0",
"tymon/jwt-auth": "^1.0"
},
"require-dev": {
"beyondcode/laravel-dump-server": "^1.0",
"beyondcode/laravel-er-diagram-generator": "^1.3",
"code-lts/doctum": "^5.1",
"filp/whoops": "^2.0",
"fzaninotto/faker": "^1.4",
"kirschbaum-development/mail-intercept": "^0.2.4",
"laravel/dusk": "~5.11.0",
"mockery/mockery": "^1.0",
"nunomaduro/larastan": "^0.6",
"phpstan/phpstan": "^0.12",
"phpunit/phpunit": "^8"
},
"config": {
"optimize-autoloader": true,
"preferred-install": "dist",
"sort-packages": true
},
"extra": {
"laravel": {
"dont-discover": []
}
},
"autoload": {
"psr-4": {
"App\\": "app/"
},
"classmap": [
"database/seeds",
"database/factories",
"include"
]
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"minimum-stability": "dev",
"prefer-stable": true,
"scripts": {
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi"
],
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
],
"post-create-project-cmd": [
"@php artisan key:generate --ansi"
]
}
}
diff --git a/src/config/geoip.php b/src/config/geoip.php
deleted file mode 100644
index c47880ad..00000000
--- a/src/config/geoip.php
+++ /dev/null
@@ -1,165 +0,0 @@
-<?php
-
-return [
-
- /*
- |--------------------------------------------------------------------------
- | Logging Configuration
- |--------------------------------------------------------------------------
- |
- | Here you may configure the log settings for when a location is not found
- | for the IP provided.
- |
- */
-
- 'log_failures' => true,
-
- /*
- |--------------------------------------------------------------------------
- | Include Currency in Results
- |--------------------------------------------------------------------------
- |
- | When enabled the system will do it's best in deciding the user's currency
- | by matching their ISO code to a preset list of currencies.
- |
- */
-
- 'include_currency' => true,
-
- /*
- |--------------------------------------------------------------------------
- | Default Service
- |--------------------------------------------------------------------------
- |
- | Here you may specify the default storage driver that should be used
- | by the framework.
- |
- | Supported: "maxmind_database", "maxmind_api", "ipapi"
- |
- */
-
- 'service' => 'maxmind_database',
-
- /*
- |--------------------------------------------------------------------------
- | Storage Specific Configuration
- |--------------------------------------------------------------------------
- |
- | Here you may configure as many storage drivers as you wish.
- |
- */
-
- 'services' => [
-
- 'maxmind_database' => [
- 'class' => \Torann\GeoIP\Services\MaxMindDatabase::class,
- 'database_path' => storage_path('app/geoip.mmdb'),
- 'update_url' => 'https://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz',
- 'locales' => ['en'],
- ],
-
- 'maxmind_api' => [
- 'class' => \Torann\GeoIP\Services\MaxMindWebService::class,
- 'user_id' => env('MAXMIND_USER_ID'),
- 'license_key' => env('MAXMIND_LICENSE_KEY'),
- 'locales' => ['en'],
- ],
-
- 'ipapi' => [
- 'class' => \Torann\GeoIP\Services\IPApi::class,
- 'secure' => true,
- 'key' => env('IPAPI_KEY'),
- 'continent_path' => storage_path('app/continents.json'),
- 'lang' => 'en',
- ],
-
- 'ipgeolocation' => [
- 'class' => \Torann\GeoIP\Services\IPGeoLocation::class,
- 'secure' => true,
- 'key' => env('IPGEOLOCATION_KEY'),
- 'continent_path' => storage_path('app/continents.json'),
- 'lang' => 'en',
- ],
-
- 'ipdata' => [
- 'class' => \Torann\GeoIP\Services\IPData::class,
- 'key' => env('IPDATA_API_KEY'),
- 'secure' => true,
- ],
-
- 'ipfinder' => [
- 'class' => \Torann\GeoIP\Services\IPFinder::class,
- 'key' => env('IPFINDER_API_KEY'),
- 'secure' => true,
- 'locales' => ['en'],
- ],
-
- ],
-
- /*
- |--------------------------------------------------------------------------
- | Default Cache Driver
- |--------------------------------------------------------------------------
- |
- | Here you may specify the type of caching that should be used
- | by the package.
- |
- | Options:
- |
- | all - All location are cached
- | some - Cache only the requesting user
- | none - Disable cached
- |
- */
-
- 'cache' => 'all',
-
- /*
- |--------------------------------------------------------------------------
- | Cache Tags
- |--------------------------------------------------------------------------
- |
- | Cache tags are not supported when using the file or database cache
- | drivers in Laravel. This is done so that only locations can be cleared.
- |
- */
-
- 'cache_tags' => false,
-
- /*
- |--------------------------------------------------------------------------
- | Cache Expiration
- |--------------------------------------------------------------------------
- |
- | Define how long cached location are valid.
- |
- */
-
- 'cache_expires' => 30,
-
- /*
- |--------------------------------------------------------------------------
- | Default Location
- |--------------------------------------------------------------------------
- |
- | Return when a location is not found.
- |
- */
-
- 'default_location' => [
- 'ip' => '127.0.0.0',
- 'iso_code' => 'CH',
- 'country' => 'Switzerland',
- 'city' => 'Zurich',
- 'state' => 'ZH',
- 'state_name' => 'Zurich',
- 'postal_code' => '8703',
- 'lat' => 47.30,
- 'lon' => 8.59,
- 'timezone' => 'Europe/Zurich',
- 'continent' => 'EU',
- 'default' => true,
- 'currency' => 'CHF',
- ],
-
-];
diff --git a/src/database/migrations/2020_06_04_140800_create_ip4nets_table.php b/src/database/migrations/2020_06_04_140800_create_ip4nets_table.php
new file mode 100644
index 00000000..bfd3fb20
--- /dev/null
+++ b/src/database/migrations/2020_06_04_140800_create_ip4nets_table.php
@@ -0,0 +1,43 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+// phpcs:ignore
+class CreateIp4netsTable extends Migration
+{
+ /**
+ * Run the migrations.
+ *
+ * @return void
+ */
+ public function up()
+ {
+ Schema::create(
+ 'ip4nets',
+ function (Blueprint $table) {
+ $table->bigIncrements('id');
+ $table->string('rir_name', 8);
+ $table->string('net_number', 15)->index();
+ $table->tinyInteger('net_mask')->unsigned();
+ $table->string('net_broadcast', 15)->index();
+ $table->string('country', 2)->nullable();
+ $table->bigInteger('serial')->unsigned();
+ $table->timestamps();
+
+ $table->index(['net_number', 'net_mask', 'net_broadcast']);
+ }
+ );
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::dropIfExists('ip4nets');
+ }
+}
diff --git a/src/database/migrations/2020_06_04_140800_create_ip6nets_table.php b/src/database/migrations/2020_06_04_140800_create_ip6nets_table.php
new file mode 100644
index 00000000..91d9531f
--- /dev/null
+++ b/src/database/migrations/2020_06_04_140800_create_ip6nets_table.php
@@ -0,0 +1,43 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+// phpcs:ignore
+class CreateIp6netsTable extends Migration
+{
+ /**
+ * Run the migrations.
+ *
+ * @return void
+ */
+ public function up()
+ {
+ Schema::create(
+ 'ip6nets',
+ function (Blueprint $table) {
+ $table->bigIncrements('id');
+ $table->string('rir_name', 8);
+ $table->string('net_number', 39)->index();
+ $table->tinyInteger('net_mask')->unsigned();
+ $table->string('net_broadcast', 39)->index();
+ $table->string('country', 2)->nullable();
+ $table->bigInteger('serial')->unsigned();
+ $table->timestamps();
+
+ $table->index(['net_number', 'net_mask', 'net_broadcast']);
+ }
+ );
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::dropIfExists('ip6nets');
+ }
+}

File Metadata

Mime Type
text/x-diff
Expires
Tue, Jun 10, 8:53 AM (1 d, 17 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
197025
Default Alt Text
(51 KB)

Event Timeline