diff --git a/src/app/Backends/Roundcube.php b/src/app/Backends/Roundcube.php index 94b1464f..8d35942a 100644 --- a/src/app/Backends/Roundcube.php +++ b/src/app/Backends/Roundcube.php @@ -1,281 +1,282 @@ table(self::FILESTORE_TABLE) ->where('user_id', self::userId($email)) ->where('context', 'enigma') ->delete(); } /** * List all files from the Enigma filestore. * * @param string $email User email address * * @return array List of Enigma filestore records */ public static function enigmaList(string $email): array { return self::dbh()->table(self::FILESTORE_TABLE) ->where('user_id', self::userId($email)) ->where('context', 'enigma') ->orderBy('filename') ->get() ->all(); } /** * Synchronize Enigma filestore from/to specified directory * * @param string $email User email address * @param string $homedir Directory location */ public static function enigmaSync(string $email, string $homedir): void { $db = self::dbh(); $debug = \config('app.debug'); $user_id = self::userId($email); $root = \config('filesystems.disks.pgp.root'); $fs = Storage::disk('pgp'); $files = []; $result = $db->table(self::FILESTORE_TABLE)->select('file_id', 'filename', 'mtime') ->where('user_id', $user_id) ->where('context', 'enigma') ->get(); foreach ($result as $record) { $file = $homedir . '/' . $record->filename; $mtime = $fs->exists($file) ? $fs->lastModified($file) : 0; $files[] = $record->filename; if ($mtime < $record->mtime) { + $file_id = $record->file_id; $record = $db->table(self::FILESTORE_TABLE)->select('file_id', 'data', 'mtime') - ->where('file_id', $record->file_id) + ->where('file_id', $file_id) ->first(); $data = $record ? base64_decode($record->data) : false; if ($data === false) { - \Log::error("Failed to sync $file ({$record->file_id}). Decode error."); + \Log::error("Failed to sync $file ({$file_id}). Decode error."); continue; } if ($fs->put($file, $data, true)) { // Note: Laravel Filesystem API does not provide touch method touch("$root/$file", $record->mtime); if ($debug) { \Log::debug("[SYNC] Fetched file: $file"); } } } } // Remove files not in database foreach (array_diff(self::enigmaFilesList($homedir), $files) as $file) { $file = $homedir . '/' . $file; if ($fs->delete($file)) { if ($debug) { \Log::debug("[SYNC] Removed file: $file"); } } } // No records found, do initial sync if already have the keyring if (empty($file)) { self::enigmaSave(true, $homedir); } } /** * Save the keys database * * @param string $email User email address * @param string $homedir Directory location * @param bool $is_empty Set to Tre if it is a initial save */ public static function enigmaSave(string $email, string $homedir, bool $is_empty = false): void { $db = self::dbh(); $debug = \config('app.debug'); $user_id = self::userId($email); $fs = Storage::disk('pgp'); $records = []; if (!$is_empty) { $records = $db->table(self::FILESTORE_TABLE)->select('file_id', 'filename', 'mtime') ->where('user_id', $user_id) ->where('context', 'enigma') ->get() ->keyBy('filename') ->all(); } foreach (self::enigmaFilesList($homedir) as $filename) { $file = $homedir . '/' . $filename; $mtime = $fs->exists($file) ? $fs->lastModified($file) : 0; $existing = !empty($records[$filename]) ? $records[$filename] : null; unset($records[$filename]); if ($mtime && (empty($existing) || $mtime > $existing->mtime)) { $data = base64_encode($fs->get($file)); /* if (empty($maxsize)) { $maxsize = min($db->get_variable('max_allowed_packet', 1048500), 4*1024*1024) - 2000; } if (strlen($data) > $maxsize) { \Log::error("Failed to save $file. Size exceeds max_allowed_packet."); continue; } */ $result = $db->table(self::FILESTORE_TABLE)->updateOrInsert( ['user_id' => $user_id, 'context' => 'enigma', 'filename' => $filename], ['mtime' => $mtime, 'data' => $data] ); if ($debug) { \Log::debug("[SYNC] Pushed file: $file"); } } } // Delete removed files from database foreach (array_keys($records) as $filename) { $file = $homedir . '/' . $filename; $result = $db->table(self::FILESTORE_TABLE) ->where('user_id', $user_id) ->where('context', 'enigma') ->where('filename', $filename) ->delete(); if ($debug) { \Log::debug("[SYNC] Removed file: $file"); } } } /** * Delete a Roundcube user. * * @param string $email User email address */ public static function deleteUser(string $email): void { $db = self::dbh(); $db->table(self::USERS_TABLE)->where('username', \strtolower($email))->delete(); } /** * Find the Roundcube user identifier for the specified user. * * @param string $email User email address * @param bool $create Make sure the user record exists * * @returns ?int Roundcube user identifier */ public static function userId(string $email, bool $create = true): ?int { $db = self::dbh(); $user = $db->table(self::USERS_TABLE)->select('user_id') ->where('username', \strtolower($email)) ->first(); // Create a user record, without it we can't use the Roundcube storage if (empty($user)) { if (!$create) { return null; } $uri = \parse_url(\config('imap.uri')); $user_id = (int) $db->table(self::USERS_TABLE)->insertGetId( [ 'username' => $email, 'mail_host' => $uri['host'], 'created' => now()->toDateTimeString(), ], 'user_id' ); $username = \App\User::where('email', $email)->first()->name(); $db->table(self::IDENTITIES_TABLE)->insert([ 'user_id' => $user_id, 'email' => $email, 'name' => $username, 'changed' => now()->toDateTimeString(), 'standard' => 1, ]); return $user_id; } return (int) $user->user_id; } /** * Returns list of Enigma user homedir files to backup/sync */ private static function enigmaFilesList(string $homedir) { $files = []; $fs = Storage::disk('pgp'); foreach (self::$enigma_files as $file) { if ($fs->exists($homedir . '/' . $file)) { $files[] = $file; } } foreach ($fs->files($homedir . '/private-keys-v1.d') as $file) { if (preg_match('/\.key$/', $file)) { $files[] = substr($file, strlen($homedir . '/')); } } return $files; } } diff --git a/src/composer.json b/src/composer.json index e3c83664..2541cc98 100644 --- a/src/composer.json +++ b/src/composer.json @@ -1,87 +1,87 @@ { "name": "kolab/kolab4", "type": "project", "description": "Kolab 4", "keywords": [ "framework", "laravel" ], "license": "MIT", "repositories": [ { "type": "vcs", "url": "https://git.kolab.org/diffusion/PNL/php-net_ldap3.git" } ], "require": { "php": "^8.1", "bacon/bacon-qr-code": "^2.0", "barryvdh/laravel-dompdf": "^2.0.1", "doctrine/dbal": "^3.6", "dyrynda/laravel-nullable-fields": "^4.3.0", "guzzlehttp/guzzle": "^7.4.1", "kolab/net_ldap3": "dev-master", "laravel/framework": "^10.15.0", "laravel/horizon": "^5.9", "laravel/octane": "^2.0", "laravel/passport": "^11.3", "laravel/tinker": "^2.8", "league/flysystem-aws-s3-v3": "^3.0", "mlocati/spf-lib": "^3.1", "mollie/laravel-mollie": "^2.22", "pear/crypt_gpg": "^1.6.6", "predis/predis": "^2.0", "sabre/vobject": "^4.5", "spatie/laravel-translatable": "^6.5", "spomky-labs/otphp": "~10.0.0", "stripe/stripe-php": "^10.7", "lcobucci/jwt": "^5.0" }, "require-dev": { "code-lts/doctum": "^5.5.1", - "laravel/dusk": "~7.8.0", + "laravel/dusk": "~7.9.1", "mockery/mockery": "^1.5", - "nunomaduro/larastan": "^2.0", + "larastan/larastan": "^2.0", "phpstan/phpstan": "^1.4", "phpunit/phpunit": "^9", "squizlabs/php_codesniffer": "^3.6" }, "config": { "optimize-autoloader": true, "preferred-install": "dist", "sort-packages": true }, "extra": { "laravel": { "dont-discover": [] } }, "autoload": { "psr-4": { "App\\": "app/" }, "classmap": [ "database/seeds", "include" ] }, "autoload-dev": { "psr-4": { "Tests\\": "tests/" } }, "minimum-stability": "stable", "prefer-stable": true, "scripts": { "post-autoload-dump": [ "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", "@php artisan package:discover --ansi" ], "post-update-cmd": [ "@php artisan vendor:publish --tag=laravel-assets --ansi --force" ], "post-create-project-cmd": [ "@php artisan key:generate --ansi" ] } } diff --git a/src/phpstan.neon b/src/phpstan.neon index 2d21275b..4a6ee260 100644 --- a/src/phpstan.neon +++ b/src/phpstan.neon @@ -1,20 +1,20 @@ includes: - - ./vendor/nunomaduro/larastan/extension.neon + - ./vendor/larastan/larastan/extension.neon parameters: ignoreErrors: - '#Access to an undefined property [a-zA-Z\\]+::\$pivot#' - '#Call to an undefined [a-zA-Z0-9<>\\ ]+::withEnvTenantContext\(\)#' - '#Call to an undefined [a-zA-Z0-9<>\\ ]+::withObjectTenantContext\(\)#' - '#Call to an undefined [a-zA-Z0-9<>\\ ]+::withSubjectTenantContext\(\)#' - '#Call to an undefined method Tests\\Browser::#' level: 4 parallel: processTimeout: 300.0 paths: - app/ - config/ - database/ - resources/ - routes/ - tests/ - resources/