diff --git a/resources/js/electron-builder.js b/resources/js/electron-builder.js index 65470356..25d2f4ef 100644 --- a/resources/js/electron-builder.js +++ b/resources/js/electron-builder.js @@ -33,12 +33,8 @@ if (isDarwin) { targetOs = 'mac'; } - let updaterConfig = {}; -// We wouldn't need these since its not representing the target platform -console.log("Arch: ", process.arch) -console.log("Platform: ", process.platform) try { updaterConfig = process.env.NATIVEPHP_UPDATER_CONFIG; updaterConfig = JSON.parse(updaterConfig); @@ -47,6 +43,8 @@ try { } if (isBuilding) { + console.log("Current platform: ", process.platform) + console.log("Current arch: ", process.arch) console.log(); console.log('==================================================================='); @@ -61,53 +59,69 @@ if (isBuilding) { removeSync(appPath); - // As we can't copy into a subdirectory of ourself we need to copy to a temp directory - let tmpDir = mkdtempSync(join(os.tmpdir(), 'nativephp')); - - copySync(process.env.APP_PATH, tmpDir, { - overwrite: true, - dereference: true, - filter: (src, dest) => { - let skip = [ - // Skip .git and Dev directories - join(process.env.APP_PATH, '.git'), - join(process.env.APP_PATH, 'docker'), - join(process.env.APP_PATH, 'packages'), - - // Only needed for local testing - join(process.env.APP_PATH, 'vendor', 'nativephp', 'electron', 'vendor'), - join(process.env.APP_PATH, 'vendor', 'nativephp', 'laravel', 'vendor'), - - join(process.env.APP_PATH, 'vendor', 'nativephp', 'php-bin'), - join(process.env.APP_PATH, 'vendor', 'nativephp', 'electron', 'bin'), - join(process.env.APP_PATH, 'vendor', 'nativephp', 'electron', 'resources'), - join(process.env.APP_PATH, 'node_modules'), - join(process.env.APP_PATH, 'dist'), - ]; - - let shouldSkip = false; - skip.forEach((path) => { - if (src.indexOf(path) === 0) { - shouldSkip = true; - } - }); - - return !shouldSkip; - } - }); - - copySync(tmpDir, appPath); - - // Electron build removes empty folders, so we have to create dummy files - // dotfiles unfortunately don't work. - writeJsonSync(join(appPath, 'storage', 'framework', 'cache', '_native.json'), {}) - writeJsonSync(join(appPath, 'storage', 'framework', 'sessions', '_native.json'), {}) - writeJsonSync(join(appPath, 'storage', 'framework', 'testing', '_native.json'), {}) - writeJsonSync(join(appPath, 'storage', 'framework', 'views', '_native.json'), {}) - writeJsonSync(join(appPath, 'storage', 'app', 'public', '_native.json'), {}) - writeJsonSync(join(appPath, 'storage', 'logs', '_native.json'), {}) - - removeSync(tmpDir); + let bundle = join(process.env.APP_PATH, 'build', '__nativephp_app_bundle'); + + if (existsSync(bundle)) { + copySync(bundle, join(appPath, 'bundle', '__nativephp_app_bundle')); + } else { + // As we can't copy into a subdirectory of ourself we need to copy to a temp directory + let tmpDir = mkdtempSync(join(os.tmpdir(), 'nativephp')); + + console.warn('====================='); + console.warn('* * * INSECURE BUILD * * *'); + console.warn('Secure app bundle not found! Building with exposed source files.'); + console.warn('See https://nativephp.com/docs/publishing/building#security for more details'); + console.warn('====================='); + + copySync(process.env.APP_PATH, tmpDir, { + overwrite: true, + dereference: true, + filter: (src, dest) => { + let skip = [ + // Skip .git and Dev directories + join(process.env.APP_PATH, '.git'), + join(process.env.APP_PATH, 'docker'), + join(process.env.APP_PATH, 'packages'), + + // Only needed for local testing + join(process.env.APP_PATH, 'vendor', 'nativephp', 'electron', 'vendor'), + join(process.env.APP_PATH, 'vendor', 'nativephp', 'laravel', 'vendor'), + + join(process.env.APP_PATH, 'vendor', 'nativephp', 'php-bin'), + join(process.env.APP_PATH, 'vendor', 'nativephp', 'electron', 'bin'), + join(process.env.APP_PATH, 'vendor', 'nativephp', 'electron', 'resources'), + join(process.env.APP_PATH, 'node_modules'), + join(process.env.APP_PATH, 'dist'), + join(process.env.APP_PATH, 'build'), + + join(process.env.APP_PATH, 'storage', 'framework'), + join(process.env.APP_PATH, 'storage', 'logs'), + ]; + + let shouldSkip = false; + skip.forEach((path) => { + if (src.indexOf(path) === 0) { + shouldSkip = true; + } + }); + + return !shouldSkip; + } + }); + + copySync(tmpDir, appPath); + + // Electron build removes empty folders, so we have to create dummy files + // dotfiles unfortunately don't work. + writeJsonSync(join(appPath, 'storage', 'framework', 'cache', '_native.json'), {}) + writeJsonSync(join(appPath, 'storage', 'framework', 'sessions', '_native.json'), {}) + writeJsonSync(join(appPath, 'storage', 'framework', 'testing', '_native.json'), {}) + writeJsonSync(join(appPath, 'storage', 'framework', 'views', '_native.json'), {}) + writeJsonSync(join(appPath, 'storage', 'app', 'public', '_native.json'), {}) + writeJsonSync(join(appPath, 'storage', 'logs', '_native.json'), {}) + + removeSync(tmpDir); + } console.log(); console.log('Copied app to resources'); diff --git a/resources/js/electron-plugin/src/server/php.ts b/resources/js/electron-plugin/src/server/php.ts index 48b08a51..840b7529 100644 --- a/resources/js/electron-plugin/src/server/php.ts +++ b/resources/js/electron-plugin/src/server/php.ts @@ -1,19 +1,19 @@ -import {mkdirSync, statSync, writeFileSync, existsSync} from 'fs' +import { mkdirSync, statSync, writeFileSync, existsSync } from 'fs' import fs_extra from 'fs-extra'; const { copySync } = fs_extra; -import Store from 'electron-store' -import {promisify} from 'util' -import {join} from 'path' -import {app} from 'electron' -import {execFile, spawn} from 'child_process' +import Store from 'electron-store'; +import { promisify } from 'util'; +import { join } from 'path'; +import { app } from 'electron'; +import { exec, execFile, spawn } from 'child_process'; import state from "./state.js"; -import getPort, {portNumbers} from 'get-port'; +import getPort, { portNumbers } from 'get-port'; import { ProcessResult } from "./ProcessResult.js"; -const storagePath = join(app.getPath('userData'), 'storage') -const databasePath = join(app.getPath('userData'), 'database') -const databaseFile = join(databasePath, 'database.sqlite') +const storagePath = join(app.getPath('userData'), 'storage'); +const databasePath = join(app.getPath('userData'), 'database'); +const databaseFile = join(databasePath, 'database.sqlite'); const argumentEnv = getArgumentEnv(); const appPath = getAppPath(); @@ -25,18 +25,24 @@ async function getPhpPort() { } async function retrievePhpIniSettings() { - const env = { - NATIVEPHP_RUNNING: 'true', - NATIVEPHP_STORAGE_PATH: storagePath, - NATIVEPHP_DATABASE_PATH: databaseFile, - }; - - const phpOptions = { - cwd: appPath, - env - }; - - return await promisify(execFile)(state.php, ['artisan', 'native:php-ini'], phpOptions); + const env = { + NATIVEPHP_RUNNING: 'true', + NATIVEPHP_STORAGE_PATH: storagePath, + NATIVEPHP_DATABASE_PATH: databaseFile, + }; + + const phpOptions = { + cwd: appPath, + env, + }; + + let command = ['artisan', 'native:php-ini']; + + if (runningSecureBuild()) { + command.unshift(join(appPath, 'build', '__nativephp_app_bundle')); + } + + return await promisify(execFile)(state.php, command, phpOptions); } async function retrieveNativePHPConfig() { @@ -48,20 +54,34 @@ async function retrieveNativePHPConfig() { const phpOptions = { cwd: appPath, - env + env, }; - return await promisify(execFile)(state.php, ['artisan', 'native:config'], phpOptions); + let command = ['artisan', 'native:config']; + + if (runningSecureBuild()) { + command.unshift(join(appPath, 'build', '__nativephp_app_bundle')); + } + + return await promisify(execFile)(state.php, command, phpOptions); } function callPhp(args, options, phpIniSettings = {}) { + if (args[0] === 'artisan' && runningSecureBuild()) { + args.unshift(join(appPath, 'build', '__nativephp_app_bundle')); + } let iniSettings = Object.assign(getDefaultPhpIniSettings(), phpIniSettings); + Object.keys(iniSettings).forEach(key => { - args.unshift('-d', `${key}=${iniSettings[key]}`); + args.unshift('-d', `${key}=${iniSettings[key]}`); }); + if (parseInt(process.env.SHELL_VERBOSITY) > 0) { + console.log('Calling PHP', state.php, args); + } + return spawn( state.php, args, @@ -69,7 +89,7 @@ function callPhp(args, options, phpIniSettings = {}) { cwd: options.cwd, env: { ...process.env, - ...options.env + ...options.env, }, } ); @@ -84,6 +104,7 @@ function getArgumentEnv() { } = { }; + envArgs.forEach(arg => { const [key, value] = arg.slice(6).split('='); env[key] = value; @@ -98,21 +119,22 @@ function getAppPath() { if (process.env.NODE_ENV === 'development' || argumentEnv.TESTING == 1) { appPath = process.env.APP_PATH || argumentEnv.APP_PATH; } + return appPath; } function ensureAppFoldersAreAvailable() { if (! existsSync(storagePath) || process.env.NODE_ENV === 'development') { - copySync(join(appPath, 'storage'), storagePath) + copySync(join(appPath, 'storage'), storagePath); } - mkdirSync(databasePath, {recursive: true}) + mkdirSync(databasePath, {recursive: true}); // Create a database file if it doesn't exist try { - statSync(databaseFile) + statSync(databaseFile); } catch (error) { - writeFileSync(databaseFile, '') + writeFileSync(databaseFile, ''); } } @@ -121,41 +143,46 @@ function startScheduler(secret, apiPort, phpIniSettings = {}) { const phpOptions = { cwd: appPath, - env + env, }; return callPhp(['artisan', 'schedule:run'], phpOptions, phpIniSettings); } function getPath(name: string) { - try { - // @ts-ignore - return app.getPath(name); - } catch (error) { - return ''; - } + try { + // @ts-ignore + return app.getPath(name); + } catch (error) { + return ''; + } } function getDefaultEnvironmentVariables(secret, apiPort) { - return { - APP_ENV: process.env.NODE_ENV === 'development' ? 'local' : 'production', - APP_DEBUG: process.env.NODE_ENV === 'development' ? 'true' : 'false', - NATIVEPHP_STORAGE_PATH: storagePath, - NATIVEPHP_DATABASE_PATH: databaseFile, - NATIVEPHP_API_URL: `http://localhost:${apiPort}/api/`, - NATIVEPHP_RUNNING: 'true', - NATIVEPHP_SECRET: secret, - NATIVEPHP_USER_HOME_PATH: getPath('home'), - NATIVEPHP_APP_DATA_PATH: getPath('appData'), - NATIVEPHP_USER_DATA_PATH: getPath('userData'), - NATIVEPHP_DESKTOP_PATH: getPath('desktop'), - NATIVEPHP_DOCUMENTS_PATH: getPath('documents'), - NATIVEPHP_DOWNLOADS_PATH: getPath('downloads'), - NATIVEPHP_MUSIC_PATH: getPath('music'), - NATIVEPHP_PICTURES_PATH: getPath('pictures'), - NATIVEPHP_VIDEOS_PATH: getPath('videos'), - NATIVEPHP_RECENT_PATH: getPath('recent'), - }; + return { + APP_ENV: process.env.NODE_ENV === 'development' ? 'local' : 'production', + APP_DEBUG: process.env.NODE_ENV === 'development' ? 'true' : 'false', + LARAVEL_STORAGE_PATH: storagePath, + NATIVEPHP_STORAGE_PATH: storagePath, + NATIVEPHP_DATABASE_PATH: databaseFile, + NATIVEPHP_API_URL: `http://localhost:${apiPort}/api/`, + NATIVEPHP_RUNNING: 'true', + NATIVEPHP_SECRET: secret, + NATIVEPHP_USER_HOME_PATH: getPath('home'), + NATIVEPHP_APP_DATA_PATH: getPath('appData'), + NATIVEPHP_USER_DATA_PATH: getPath('userData'), + NATIVEPHP_DESKTOP_PATH: getPath('desktop'), + NATIVEPHP_DOCUMENTS_PATH: getPath('documents'), + NATIVEPHP_DOWNLOADS_PATH: getPath('downloads'), + NATIVEPHP_MUSIC_PATH: getPath('music'), + NATIVEPHP_PICTURES_PATH: getPath('pictures'), + NATIVEPHP_VIDEOS_PATH: getPath('videos'), + NATIVEPHP_RECENT_PATH: getPath('recent'), + }; +} + +function runningSecureBuild() { + return existsSync(join(appPath, 'build', '__nativephp_app_bundle')) } function getDefaultPhpIniSettings() { @@ -170,58 +197,68 @@ function serveApp(secret, apiPort, phpIniSettings): Promise { return new Promise(async (resolve, reject) => { const appPath = getAppPath(); - console.log('Starting PHP server...', `${state.php} artisan serve`, appPath, phpIniSettings) + console.log('Starting PHP server...', `${state.php} artisan serve`, appPath, phpIniSettings); ensureAppFoldersAreAvailable(); - console.log('Making sure app folders are available') + console.log('Making sure app folders are available'); const env = getDefaultEnvironmentVariables(secret, apiPort); const phpOptions = { cwd: appPath, - env + env, }; const store = new Store(); // Make sure the storage path is linked - as people can move the app around, we // need to run this every time the app starts - callPhp(['artisan', 'storage:link', '--force'], phpOptions, phpIniSettings) + if (! runningSecureBuild()) { + callPhp(['artisan', 'storage:link', '--force'], phpOptions, phpIniSettings); + callPhp(['artisan', 'view:cache'], phpOptions, phpIniSettings); + } // Migrate the database - if (store.get('migrated_version') !== app.getVersion() && process.env.NODE_ENV !== 'development') { - console.log('Migrating database...') - callPhp(['artisan', 'migrate', '--force'], phpOptions, phpIniSettings) - store.set('migrated_version', app.getVersion()) + if (shouldMigrateDatabase(store)) { + console.log('Migrating database...'); + callPhp(['artisan', 'migrate', '--force'], phpOptions, phpIniSettings); + store.set('migrated_version', app.getVersion()); } if (process.env.NODE_ENV === 'development') { - console.log('Skipping Database migration while in development.') - console.log('You may migrate manually by running: php artisan native:migrate') + console.log('Skipping Database migration while in development.'); + console.log('You may migrate manually by running: php artisan native:migrate'); } const phpPort = await getPhpPort(); - const serverPath = join(appPath, 'vendor', 'laravel', 'framework', 'src', 'Illuminate', 'Foundation', 'resources', 'server.php') + let serverPath = join(appPath, 'build', '__nativephp_app_bundle'); + + if (! runningSecureBuild()) { + console.log('* * * Running from source * * *'); + serverPath = join(appPath, 'vendor', 'laravel', 'framework', 'src', 'Illuminate', 'Foundation', 'resources', 'server.php'); + } + const phpServer = callPhp(['-S', `127.0.0.1:${phpPort}`, serverPath], { cwd: join(appPath, 'public'), - env - }, phpIniSettings) + env, + }, phpIniSettings); - const portRegex = /Development Server \(.*:([0-9]+)\) started/gm + const portRegex = /Development Server \(.*:([0-9]+)\) started/gm; phpServer.stdout.on('data', (data) => { - const match = portRegex.exec(data.toString()) + const match = portRegex.exec(data.toString()); + if (match) { - console.log("PHP Server started on port: ", match[1]) - const port = parseInt(match[1]) + console.log("PHP Server started on port: ", match[1]); + const port = parseInt(match[1]); resolve({ port, - process: phpServer - }) + process: phpServer, + }); } - }) + }); phpServer.stderr.on('data', (data) => { const error = data.toString(); @@ -232,7 +269,7 @@ function serveApp(secret, apiPort, phpIniSettings): Promise { console.log("PHP Server started on port: ", port); resolve({ port, - process: phpServer + process: phpServer, }); } else { // 27 is the length of the php -S output preamble @@ -248,9 +285,14 @@ function serveApp(secret, apiPort, phpIniSettings): Promise { }); phpServer.on('error', (error) => { - reject(error) - }) + reject(error); + }); }) } +function shouldMigrateDatabase(store) { + return store.get('migrated_version') !== app.getVersion() + && process.env.NODE_ENV !== 'development'; +} + export {startScheduler, serveApp, getAppPath, retrieveNativePHPConfig, retrievePhpIniSettings, getDefaultEnvironmentVariables, getDefaultPhpIniSettings} diff --git a/src/Commands/BuildCommand.php b/src/Commands/BuildCommand.php index 98b9b722..de0fc728 100644 --- a/src/Commands/BuildCommand.php +++ b/src/Commands/BuildCommand.php @@ -5,16 +5,20 @@ use Illuminate\Console\Command; use Illuminate\Support\Facades\Process; use Illuminate\Support\Str; -use Native\Electron\Concerns\LocatesPhpBinary; use Native\Electron\Facades\Updater; +use Native\Electron\Traits\CleansEnvFile; use Native\Electron\Traits\InstallsAppIcon; +use Native\Electron\Traits\LocatesPhpBinary; use Native\Electron\Traits\OsAndArch; +use Native\Electron\Traits\SetsAppName; class BuildCommand extends Command { + use CleansEnvFile; use InstallsAppIcon; use LocatesPhpBinary; use OsAndArch; + use SetsAppName; protected $signature = 'native:build {os? : The operating system to build for (all, linux, mac, win)} @@ -27,6 +31,10 @@ public function handle(): void { $this->info('Build NativePHP app…'); + $this->prepareNativeEnv(); + + $this->setAppName(slugify: true); + Process::path(__DIR__.'/../../resources/js/') ->env($this->getEnvironmentVariables()) ->forever() @@ -64,6 +72,8 @@ public function handle(): void ->run("npm run {$buildCommand}:{$os}", function (string $type, string $output) { echo $output; }); + + $this->restoreWebEnv(); } protected function getEnvironmentVariables(): array diff --git a/src/Commands/DevelopCommand.php b/src/Commands/DevelopCommand.php index 4a0abc14..8e7da35a 100644 --- a/src/Commands/DevelopCommand.php +++ b/src/Commands/DevelopCommand.php @@ -6,13 +6,17 @@ use Native\Electron\Traits\Developer; use Native\Electron\Traits\Installer; use Native\Electron\Traits\InstallsAppIcon; +use Native\Electron\Traits\SetsAppName; use function Laravel\Prompts\intro; use function Laravel\Prompts\note; class DevelopCommand extends Command { - use Developer, Installer, InstallsAppIcon; + use Developer; + use Installer; + use InstallsAppIcon; + use SetsAppName; protected $signature = 'native:serve {--no-queue} {--D|no-dependencies} {--installer=npm}'; @@ -36,7 +40,7 @@ public function handle(): void $this->patchPlist(); } - $this->patchPackageJson(); + $this->setAppName(); $this->installIcon(); @@ -64,14 +68,4 @@ protected function patchPlist(): void file_put_contents(__DIR__.'/../../resources/js/node_modules/electron/dist/Electron.app/Contents/Info.plist', $pList); } - - protected function patchPackageJson(): void - { - $packageJsonPath = __DIR__.'/../../resources/js/package.json'; - $packageJson = json_decode(file_get_contents($packageJsonPath), true); - - $packageJson['name'] = config('app.name'); - - file_put_contents($packageJsonPath, json_encode($packageJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); - } } diff --git a/src/Commands/PublishCommand.php b/src/Commands/PublishCommand.php index e5b13751..26dbf78b 100644 --- a/src/Commands/PublishCommand.php +++ b/src/Commands/PublishCommand.php @@ -4,7 +4,7 @@ use Illuminate\Console\Command; use Illuminate\Support\Facades\Artisan; -use Native\Electron\Concerns\LocatesPhpBinary; +use Native\Electron\Traits\LocatesPhpBinary; use Native\Electron\Traits\OsAndArch; class PublishCommand extends Command diff --git a/src/Traits/CleansEnvFile.php b/src/Traits/CleansEnvFile.php new file mode 100644 index 00000000..06916723 --- /dev/null +++ b/src/Traits/CleansEnvFile.php @@ -0,0 +1,46 @@ +line('Preparing production .env file…'); + + $envFile = app()->environmentFilePath(); + + if (! file_exists($backup = $this->getBackupEnvFilePath())) { + copy($envFile, $backup); + } + + $this->cleanEnvFile($envFile); + } + + protected function cleanEnvFile(string $path): void + { + $cleanUpKeys = config('nativephp.cleanup_env_keys', []); + + $contents = collect(file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES)) + ->filter(function (string $line) use ($cleanUpKeys) { + $key = str($line)->before('='); + + return ! $key->is($cleanUpKeys) + && ! $key->startsWith('#'); + }) + ->join("\n"); + + file_put_contents($path, $contents); + } + + protected function restoreWebEnv(): void + { + copy($this->getBackupEnvFilePath(), app()->environmentFilePath()); + unlink($this->getBackupEnvFilePath()); + } + + protected function getBackupEnvFilePath(): string + { + return base_path('.env.backup'); + } +} diff --git a/src/Traits/ExecuteCommand.php b/src/Traits/ExecuteCommand.php index 24dd7bf9..26e98c06 100644 --- a/src/Traits/ExecuteCommand.php +++ b/src/Traits/ExecuteCommand.php @@ -3,7 +3,6 @@ namespace Native\Electron\Traits; use Illuminate\Support\Facades\Process; -use Native\Electron\Concerns\LocatesPhpBinary; use function Laravel\Prompts\note; diff --git a/src/Traits/InstallsAppIcon.php b/src/Traits/InstallsAppIcon.php index d8e18e57..1239f08a 100644 --- a/src/Traits/InstallsAppIcon.php +++ b/src/Traits/InstallsAppIcon.php @@ -14,6 +14,7 @@ public function installIcon() @copy(public_path('icon.png'), __DIR__.'/../../resources/js/resources/icon.png'); @copy(public_path('IconTemplate.png'), __DIR__.'/../../resources/js/resources/IconTemplate.png'); @copy(public_path('IconTemplate@2x.png'), __DIR__.'/../../resources/js/resources/IconTemplate@2x.png'); + @copy(public_path('icon.png'), __DIR__.'/../../resources/js/build/icon.png'); note('App icons copied'); } diff --git a/src/Concerns/LocatesPhpBinary.php b/src/Traits/LocatesPhpBinary.php similarity index 93% rename from src/Concerns/LocatesPhpBinary.php rename to src/Traits/LocatesPhpBinary.php index e613ef1f..c5763572 100644 --- a/src/Concerns/LocatesPhpBinary.php +++ b/src/Traits/LocatesPhpBinary.php @@ -1,6 +1,6 @@ lower()->kebab(); + } + + $packageJson['name'] = $name; + + file_put_contents($packageJsonPath, json_encode($packageJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + } +}