Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F256972
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
51 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
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)
Attached To
Mode
R2 kolab
Attached
Detach File
Event Timeline
Log In to Comment