diff --git a/.editorconfig b/.editorconfig index e8717176..07c6fd18 100644 --- a/.editorconfig +++ b/.editorconfig @@ -11,7 +11,7 @@ trim_trailing_whitespace = true [*.md] trim_trailing_whitespace = false indent_style = space -indent_size = 2 +indent_size = 4 [*.yml] indent_size = 2 diff --git a/README.md b/README.md index 9f6f2a17..83d17794 100644 --- a/README.md +++ b/README.md @@ -203,7 +203,7 @@ Also make sure to open your preferred terminal (Windows Terminal, CMD, Git Bash, --- -- If you don't have PHP installed, make sure to [install](https://windows.php.net/download) it. +- If you don't have PHP installed, make sure to [install](https://windows.php.net/download) it. Download the Zip file and unzip into a directory of your choosing. The recommended directory is: `C:/php/`. @@ -211,9 +211,9 @@ Also make sure to open your preferred terminal (Windows Terminal, CMD, Git Bash, > For NTS binaries the widespread use case is interaction with a web server through the FastCGI protocol, utilizing no multithreading (but also for example CLI). -- If you don't have Composer installed, make sure to [install](https://getcomposer.org/doc/00-intro.md#installation-windows) it. +- If you don't have Composer installed, make sure to [install](https://getcomposer.org/doc/00-intro.md#installation-windows) it. -- Install Valet with Composer via `composer global require ycodetech/valet-windows`. +- Install Valet with Composer via `composer global require ycodetech/valet-windows`.

@@ -221,9 +221,9 @@ Also make sure to open your preferred terminal (Windows Terminal, CMD, Git Bash, > > **If you're coming from cretueusebiu/valet-windows, then you need to make sure to fully uninstall it from your computer, deleting all configs, and removing from composer with `composer global remove cretueusebiu/valet-windows`, before installing this 3.0 version.** -- Install Valet by running the `valet install` command, or alternatively `valet sudo install` with administrator elevation. This will configure and install Valet and register Valet's daemon to launch when your system starts. Once installed, Valet will automatically start it's services. +- Install Valet by running the `valet install` command, or alternatively `valet sudo install` with administrator elevation. This will configure and install Valet and register Valet's daemon to launch when your system starts. Once installed, Valet will automatically start it's services. -- If you're installing on Windows 10/11, you may need to [manually configure](https://mayakron.altervista.org/support/acrylic/Windows10Configuration.htm) Windows to use the [Acrylic DNS Proxy](https://mayakron.altervista.org/support/acrylic/Home.htm). +- If you're installing on Windows 10/11, you may need to [manually configure](https://mayakron.altervista.org/support/acrylic/Windows10Configuration.htm) Windows to use the [Acrylic DNS Proxy](https://mayakron.altervista.org/support/acrylic/Home.htm). Valet will automatically start its daemon each time your machine boots. There is no need to run `valet start` or `valet install` ever again once the initial Valet installation is complete. @@ -248,11 +248,11 @@ Valet installed and started successfully! This installs all Valet services: -- Nginx -- PHP CGI -- PHP Xdebug CGI [optional] -- Acrylic DNS -- Ansicon +- Nginx +- PHP CGI +- PHP Xdebug CGI [optional] +- Acrylic DNS +- Ansicon And it's configs in `C:/Users/Username/.config/valet`. @@ -1108,7 +1108,15 @@ Before sharing a site with ngrok, you must first set the authtoken using Valet's > [!NOTE] > -> The public URL won't be displayed, however, in a separate terminal, you can use the [`fetch-share-url` command](#fetch-share-url) to get the url and copy it to the clipboard. +> Prior to v3.4.4, the public URL wasn't displayed and the [`fetch-share-url` command](#fetch-share-url) had to be used instead to get the URL and copy it to the clipboard. +> +> As of v3.4.4, the public URL **will** be displayed and automatically copied to your clipboard (requires ngrok's real-time logging). You can still use the `fetch-share-url` command in a separate terminal though. + +> [!IMPORTANT] +> +> ngrok will now output logging information directly to the terminal in real-time by default using the `--log=stdout` option. Valet redirects stderr to stdout, so all logging information will be outputted to the terminal. This is useful for debugging and seeing what ngrok is doing in real-time. For this reason the `--log=stderr` is exactly the same output as `--log=stdout`. +> +> If you wish to disable real-time logging, you can use the valet's `--options` argument to pass ngrok's `log=false` option; this will disable all logging output to the terminal, including the public URL. ###### share --options @@ -1454,13 +1462,13 @@ The nginx error pages are `on` (enabled) by default, which means that Valet's cu These are **important notes** for the commands that have the `--options` or `--valetOptions`. -- The `--options`, `--valetOptions` (shortcut `-o`) options can be used to pass options/flags to the service related to that command. +- The `--options`, `--valetOptions` (shortcut `-o`) options can be used to pass options/flags to the service related to that command. Just pass the option name without the `--` prefix eg. `--options=config=C:/path/ngrok.yml` (example for the `ngrok` command). This is so that Valet doesn't get confused with it's own options. All options/flags that are passed will be prefixed with `--` after Valet has processed the command, unless it's a shortcut of a single character, then it will be prefixed with `-`. The example above will run as `--config=C:/path/ngrok.yml`. -- The `=` immediately after the command option is optional, if it's omitted, you must use a space instead. +- The `=` immediately after the command option is optional, if it's omitted, you must use a space instead. ``` --options=option1 @@ -1469,7 +1477,7 @@ These are **important notes** for the commands that have the `--options` or `--v --valetOptions option1 ``` -- The options also have `-o` shortcuts and it cannot have the `=` character, it must use a space for separation. +- The options also have `-o` shortcuts and it cannot have the `=` character, it must use a space for separation. ```console -o option1 @@ -1481,7 +1489,7 @@ These are **important notes** for the commands that have the `--options` or `--v > ###### Note that to comply with the docopt standard, long options can specify their values after a whitespace or an `=` sign (e.g. `--iterations 5` or `--iterations=5`), but short options can only use whitespaces or no separation at all (e.g. `-i 5` or `-i5`). -- The options also allows multiple options to be passed, they just need to be separated with double slashes `//`. +- The options also allows multiple options to be passed, they just need to be separated with double slashes `//`. ```console --valetOptions=option1//option2//option3 @@ -1493,46 +1501,46 @@ These are **important notes** for the commands that have the `--options` or `--v Commands that have been tested and made parity: -- [ ] composer - not applicable -- [x] diagnose -- [x] directory-listing -- [x] fetch-share-url -- [x] forget -- [x] install -- [x] isolate -- [x] isolated -- [x] link -- [x] links -- [x] log -- [ ] loopback (the localhost IP) - not applicable -- [x] on-latest-version -- [x] open -- [x] park -- [x] parked -- [x] paths -- [x] php (proxying commands to PHP CLI) - renamed to `php:proxy` with alias of `php` -- [x] proxies -- [x] proxy -- [ ] renew (Renews all domains with a trusted TLS certificate) - TBD -- [x] restart -- [x] secure -- [x] secured -- [x] set-ngrok-token -- [x] share -- [x] share-tool -- [x] start -- [x] status - renamed to `services` -- [x] stop -- [x] tld -- [ ] trust - not applicable -- [x] uninstall -- [x] unisolate -- [x] unlink -- [x] unproxy -- [x] unsecure -- [x] use -- [x] which -- [x] which-php - renamed to `php:which` +- [ ] composer - not applicable +- [x] diagnose +- [x] directory-listing +- [x] fetch-share-url +- [x] forget +- [x] install +- [x] isolate +- [x] isolated +- [x] link +- [x] links +- [x] log +- [ ] loopback (the localhost IP) - not applicable +- [x] on-latest-version +- [x] open +- [x] park +- [x] parked +- [x] paths +- [x] php (proxying commands to PHP CLI) - renamed to `php:proxy` with alias of `php` +- [x] proxies +- [x] proxy +- [ ] renew (Renews all domains with a trusted TLS certificate) - TBD +- [x] restart +- [x] secure +- [x] secured +- [x] set-ngrok-token +- [x] share +- [x] share-tool +- [x] start +- [x] status - renamed to `services` +- [x] stop +- [x] tld +- [ ] trust - not applicable +- [x] uninstall +- [x] unisolate +- [x] unlink +- [x] unproxy +- [x] unsecure +- [x] use +- [x] which +- [x] which-php - renamed to `php:which` To see a calculation of how much parity has been achieved, see the [parity command](#parity). @@ -1588,56 +1596,56 @@ All services will have been stopped and removed and you can then be able to run Upon installation, Valet creates the following directories and config files: -- `~/.config/valet` - Contains all of Valet's config files. This resides in the home directory (`C:/Users/Username/`) indicated by `~`. +- `~/.config/valet` + Contains all of Valet's config files. This resides in the home directory (`C:/Users/Username/`) indicated by `~`. -- `~/.config/valet/CA` - Contains Valet's generated self-signed Root CA certificate, of which all site TLS certificates are signed with. +- `~/.config/valet/CA` + Contains Valet's generated self-signed Root CA certificate, of which all site TLS certificates are signed with. -- `~/.config/valet/Certificates` - Contains all the TLS certificates for the secured sites. These files are rebuilt when running the `install` and `secure` commands. +- `~/.config/valet/Certificates` + Contains all the TLS certificates for the secured sites. These files are rebuilt when running the `install` and `secure` commands. -- `~/.config/valet/Drivers` - Contains any user-defined custom Valet drivers. Drivers determine how a particular framework / CMS is served. See the [Valet Docs](https://laravel.com/docs/12.x/valet#custom-valet-drivers) for information on how to create a custom driver. +- `~/.config/valet/Drivers` + Contains any user-defined custom Valet drivers. Drivers determine how a particular framework / CMS is served. See the [Valet Docs](https://laravel.com/docs/12.x/valet#custom-valet-drivers) for information on how to create a custom driver. -- `~/.config/valet/Drivers/SampleValetDriver.php` - A sample custom driver. +- `~/.config/valet/Drivers/SampleValetDriver.php` + A sample custom driver. -- `~/.config/valet/Extensions` - Contains custom Valet extensions/commands. You can extend Valet and add your own commands or change existing ones. See [this comment](https://github.com/laravel/valet/issues/804#issuecomment-569731561) for more info and links to examples. +- `~/.config/valet/Extensions` + Contains custom Valet extensions/commands. You can extend Valet and add your own commands or change existing ones. See [this comment](https://github.com/laravel/valet/issues/804#issuecomment-569731561) for more info and links to examples. -- `~/.config/valet/Log` - Contains all error logs. +- `~/.config/valet/Log` + Contains all error logs. -- `~/.config/valet/Nginx` - Contains site-specific Nginx configs for any site that is isolated or secured. These files are rebuilt when running the `install`, `tld`, and `secure` commands. +- `~/.config/valet/Nginx` + Contains site-specific Nginx configs for any site that is isolated or secured. These files are rebuilt when running the `install`, `tld`, and `secure` commands. -- `~/.config/valet/Ngrok` - Contains the `ngrok.yml` config file for the ngrok executable to be able to `share` sites. This directory and file will only be created when the `set-ngrok-token` command is run. +- `~/.config/valet/Ngrok` + Contains the `ngrok.yml` config file for the ngrok executable to be able to `share` sites. This directory and file will only be created when the `set-ngrok-token` command is run. -- `~/.config/valet/Services` - Contains the Nginx and PHP config and executable files to be able to run them as Windows services. These files are rebuilt when running the `install` command. +- `~/.config/valet/Services` + Contains the Nginx and PHP config and executable files to be able to run them as Windows services. These files are rebuilt when running the `install` command. -- `~/.config/valet/Sites` - Contains all of the symbolic links for any `link`ed sites. +- `~/.config/valet/Sites` + Contains all of the symbolic links for any `link`ed sites. -- `~/.config/valet/stubs` - A user-created directory to contain custom stubs. Only used in Valet if it exists, and overrides Valet's internal stubs. +- `~/.config/valet/stubs` + A user-created directory to contain custom stubs. Only used in Valet if it exists, and overrides Valet's internal stubs. -- `~/.config/valet/Xdebug` - Contains the output files of Xdebug profiling. +- `~/.config/valet/Xdebug` + Contains the output files of Xdebug profiling. -- `~/.config/valet/config.json` - This is the main Valet config file. +- `~/.config/valet/config.json` + This is the main Valet config file. -- `~/.config/valet/Emergency Uninstall` - Contains the emergency uninstall files. +- `~/.config/valet/Emergency Uninstall` + Contains the emergency uninstall files. -- `~/.config/valet/Emergency Uninstall/ansicon` - Contains the Ansicon files and executable to help uninstall it. +- `~/.config/valet/Emergency Uninstall/ansicon` + Contains the Ansicon files and executable to help uninstall it. -- `~/.config/valet/Emergency Uninstall/emergency_uninstall_services.bat` - This is an batch file to do an emergency stop and uninstall of all services. See the [Emergency Stop and Uninstall Services section](#emergency-stop-and-uninstall-services). +- `~/.config/valet/Emergency Uninstall/emergency_uninstall_services.bat` + This is an batch file to do an emergency stop and uninstall of all services. See the [Emergency Stop and Uninstall Services section](#emergency-stop-and-uninstall-services). > [!WARNING] > @@ -1649,12 +1657,12 @@ Upon installation, Valet creates the following directories and config files: ## Known Issues -- WSL2 distros fail because of Acrylic DNS Proxy ([microsoft/wsl#4929](https://github.com/microsoft/WSL/issues/4929)). Use `valet stop`, start WSL2 then `valet start`. -- The PHP-CGI process uses port 9001. If it's already used change it in `~/.config/valet/config.json` and run `valet install` again. -- When sharing sites the url will not be copied to the clipboard. -- ~~You must run the `valet` commands from the drive where Valet is installed, except for park and link. See [#12](https://github.com/cretueusebiu/valet-windows/issues/12#issuecomment-283111834).~~ All commands seem to work fine on all drives. -- If your machine is not connected to the internet you'll have to manually add the domains in your `hosts` file or you can install the [Microsoft Loopback Adapter](https://docs.microsoft.com/en-us/troubleshoot/windows-server/networking/install-microsoft-loopback-adapter) as this simulates an active local network interface that Valet can bind too. -- When trying to run Valet on PHP 7.4 and you get this error: +- WSL2 distros fail because of Acrylic DNS Proxy ([microsoft/wsl#4929](https://github.com/microsoft/WSL/issues/4929)). Use `valet stop`, start WSL2 then `valet start`. +- The PHP-CGI process uses port 9001. If it's already used change it in `~/.config/valet/config.json` and run `valet install` again. +- ~~When sharing sites the url will not be copied to the clipboard.~~ The URL will now be copied to the clipboard automatically as of v3.4.4; see the [share command](#share) for more information. +- ~~You must run the `valet` commands from the drive where Valet is installed, except for park and link. See [#12](https://github.com/cretueusebiu/valet-windows/issues/12#issuecomment-283111834).~~ All commands seem to work fine on all drives. +- If your machine is not connected to the internet you'll have to manually add the domains in your `hosts` file or you can install the [Microsoft Loopback Adapter](https://docs.microsoft.com/en-us/troubleshoot/windows-server/networking/install-microsoft-loopback-adapter) as this simulates an active local network interface that Valet can bind too. +- When trying to run Valet on PHP 7.4 and you get this error: > Composer detected issues in your platform: > @@ -1676,13 +1684,13 @@ Upon installation, Valet creates the following directories and config files: > > Make sure you uninstall Valet before `composer global update`, to make sure all services have been stopped and uninstalled before composer removes and updates them. Otherwise errors occur and composer can't update in-use files. If this does happen please refer to the [Emergency Stop and Uninstall Services](#emergency-stop-and-uninstall-services) section. -- If you're using a framework that uses a .env file and sets the domain name, such as `WP_HOME` for Laravel Bedrock, then make sure the TLD is the same as the one set for Valet. Otherwise, when trying to reach a site, the site will auto redirect to use the TLD in set in the .env. +- If you're using a framework that uses a .env file and sets the domain name, such as `WP_HOME` for Laravel Bedrock, then make sure the TLD is the same as the one set for Valet. Otherwise, when trying to reach a site, the site will auto redirect to use the TLD in set in the .env. Example: `WP_HOME='http://mySite.test'`, Valet gets a request to `http://mySite.dev`, the site will auto redirect to `http://mySite.test`. If this still happens after changing the TLD, then it has been cached by the browser, despite NGINX specifying headers not to cache. To rectify try `"Empty cache and hard reload"` option of the page reload button. -- On rare occasions, you may encounter a WMI error: +- On rare occasions, you may encounter a WMI error: > FATAL - WMI Operation failure: InvalidServiceControl >
WMI.WmiException: InvalidServiceControl @@ -1696,9 +1704,9 @@ Upon installation, Valet creates the following directories and config files: If the WMI error does occur, try running the command again. If different WMI errors occur, please submit an issue with all relevant details. -- If there is a large error and it's file trace output to the terminal, the top of the error may be cut off/overwritten. Apparently Symfony can only write to the terminal that is viewable, if it goes outside of the viewable area (ie. you need to scroll up to view) then the output is overwritten and the most important part of the error, the description at the start is cut off. (See https://github.com/symfony/symfony/issues/35012). If this happens, make the terminal larger in height and try the command again to try and view the full error. +- If there is a large error and it's file trace output to the terminal, the top of the error may be cut off/overwritten. Apparently Symfony can only write to the terminal that is viewable, if it goes outside of the viewable area (ie. you need to scroll up to view) then the output is overwritten and the most important part of the error, the description at the start is cut off. (See https://github.com/symfony/symfony/issues/35012). If this happens, make the terminal larger in height and try the command again to try and view the full error. -- When trying to install Valet after updating via Composer, and you receive this error or similiar: `The system cannot find the path specified`; it could be that Ansicon hasn't uninstalled properly and left it's path in the registry. Because it happens unpredictably, there is no way of debugging or fixing it. +- When trying to install Valet after updating via Composer, and you receive this error or similiar: `The system cannot find the path specified`; it could be that Ansicon hasn't uninstalled properly and left it's path in the registry. Because it happens unpredictably, there is no way of debugging or fixing it. The only "workaround" is to use the emergency uninstall script (`emergency_uninstall_services.bat`) in the `~/.config/valet/Emergency Uninstall` directory to allow Ansicon another chance to uninstall itself properly. (See [issue 28](https://github.com/yCodeTech/valet-windows/issues/28)). See the [Emergency Stop and Uninstall Services section](#emergency-stop-and-uninstall-services). diff --git a/cli/Valet/CommandLine.php b/cli/Valet/CommandLine.php index f719867a..2124465c 100644 --- a/cli/Valet/CommandLine.php +++ b/cli/Valet/CommandLine.php @@ -27,6 +27,69 @@ public function shellExec($command) { return shell_exec($command); } + /** + * Stream command output in real time and optionally collect matching lines. + * + * @param string $command + * @param array $callbacks Optional callbacks: + * - onLine (callable): receives every raw line after it is written. + * - matches (callable): return true to collect line for post-run analysis. + * - isError (callable): return true to render line as an error. Defaults to matches. + * + * @return array The collected output lines or an empty array if no lines were collected. + */ + public function streamCommandOutput($command, array $callbacks = []): array { + $lineHandler = $callbacks['onLine'] ?? null; + $lineMatches = $callbacks['matches'] ?? null; + $lineIsError = $callbacks['isError'] ?? $lineMatches; + + $capturedLines = []; + + // Open a process to execute the command and read its output. + // 2>&1 redirects stderr to stdout so we can capture both. + $handle = popen("$command 2>&1", 'r'); + + // If the process failed to start, throw an error. + if ($handle === false) { + error('Failed to start command for streaming output.', true); + } + + while ($handle && !feof($handle)) { + $line = fgets($handle); + if ($line === false) { + break; + } + + // If the line is an error, output it as an error. + if ($lineIsError && $lineIsError($line)) { + error($line, false, false, true); + } + // Otherwise, output it normally. + else { + output($line, false); + } + + // Invoke the optional line handler after writing output so callers can append + // follow-up messages in display order. + if ($lineHandler) { + $lineHandler($line); + } + + // If a callback is provided and the line matches the condition, + // then collect the line for post-run analysis. + if ($lineMatches && $lineMatches($line)) { + $capturedLines[] = trim($line); + } + } + + // Close the process. + if ($handle) { + pclose($handle); + } + + return $capturedLines; + } + /** * Pass the given Valet command to the command line with elevated privileges using gsudo. * @@ -151,4 +214,4 @@ public function runCommand($command, ?callable $onError = null, $realTimeOutput return new ProcessOutput($process); } } -} \ No newline at end of file +} diff --git a/cli/Valet/ShareTools/Ngrok.php b/cli/Valet/ShareTools/Ngrok.php index 010cecba..37774918 100644 --- a/cli/Valet/ShareTools/Ngrok.php +++ b/cli/Valet/ShareTools/Ngrok.php @@ -22,16 +22,31 @@ class Ngrok extends ShareTool { */ public function start(string $site, int $port, array $options = []) { if ($port === 443 && !$this->hasAuthToken()) { - output('Forwarding to local port 443 or a local https:// URL is only available after you sign up. -Sign up at: https://ngrok.com/signup -Then use: valet set-ngrok-token [token]'); + output("Forwarding to local port 443 or a local https:// URL is only available after you sign up.\nSign up at: https://ngrok.com/signup\nThen use: valet set-ngrok-token [token]"); exit(1); } - // If host-header is not specified, - // then set it into the array with a default value of rewrite. - if (!stripos(json_encode($options), 'host-header')) { - array_push($options, "host-header=rewrite"); + // Apply defaults for various options the user has not already specified. + $defaults = [ + 'host-header' => 'rewrite', + // Logging options: log to stdout at info level, enables real-time output + // and post-run error analysis. + // Logging options are undocumented for the http command, but is defined as + // API flags but still works for the http command. See ngrok docs for more details: + // https://ngrok.com/docs/agent/cli-api#flags-2 + // + // (Note: Both `stdout` and `stderr` values capture the same output since + // `CommandLine::streamCommandOutput` method uses `2>&1` to redirect stderr to stdout.) + 'log' => 'stdout', + 'log-level' => 'info', + 'log-format' => 'term' + ]; + + // Merge defaults with user-specified options, giving precedence to user-specified options. + foreach ($defaults as $key => $value) { + if (!array_filter($options, fn($opt) => strpos($opt, "$key=") === 0)) { + $options[] = "$key=$value"; + } } $options = prefixOptions($options); @@ -41,16 +56,49 @@ public function start(string $site, int $port, array $options = []) { $ngrokCommand = "\"$ngrok\" http $site:$port " . $this->getConfig() . " $options"; info("Sharing $site...\n"); - info("To output the public URL, please open a new terminal and run `valet fetch-share-url $site`"); - $output = $this->cli->shellExec("$ngrokCommand 2>&1"); + // If the options string doesn't contain the `--log` option with values of either `stdout` + // or `stderr`,then inform the user that they can fetch the public URL in a new terminal. + if (strpos($options, '--log=stdout') === false && strpos($options, '--log=stderr') === false) { + info("To output the public URL, please open a new terminal and run `valet fetch-share-url $site`"); + } + + // Stream ngrok output in real time and collect error lines for post-run analysis. + // Shared matcher: use the same rule for live error styling and for post-run capture. + $isErrorLine = function ($line) { + return strpos($line, 'ERROR:') !== false || strpos($line, 'ERR_NGROK_') !== false; + }; + + $didOutputShareUrl = false; - if ($errors = strstr($output, "ERROR")) { - error($errors . PHP_EOL); + // Line handler: check each line for the "started tunnel" log line to find and + // extract the public URL. + $lineHandler = function ($line) use ($site, &$didOutputShareUrl) { + // If the share URL has already been output, skip further processing. + if ($didOutputShareUrl) { + return; + } + + // If the line contains the 'msg="started tunnel"' message AND has a 'url=' key... + if (strpos($line, 'msg="started tunnel"') !== false && preg_match('/\burl=(\S+)/', $line, $matches)) { + // Set the flag to true to avoid further processing of lines. + $didOutputShareUrl = true; + // Output an info message with extracted public URL. + info("The public URL for $site is: $matches[1]"); - if (strpos($errors, 'ERR_NGROK_121') !== false) { - info("To update ngrok yourself, please run `valet ngrok update` and then upgrade the config file by running `valet ngrok config upgrade`\n"); + // Copy the public URL to the clipboard for ease. + $this->copyUrlToClipboard($matches[1]); } + }; + + // Stream ngrok output in real time and collect error lines for post-run analysis. + $errorLines = $this->cli->streamCommandOutput($ngrokCommand, [ + 'onLine' => $lineHandler, + 'matches' => $isErrorLine + ]); + + if (!empty($errorLines) && strpos(implode("\n", $errorLines), 'ERR_NGROK_121') !== false) { + info("\nTo update ngrok yourself, please run `valet ngrok update` and then upgrade the config file by running `valet ngrok config upgrade`\n"); } } diff --git a/cli/Valet/ShareTools/ShareTool.php b/cli/Valet/ShareTools/ShareTool.php index e29e914d..7ad10a16 100644 --- a/cli/Valet/ShareTools/ShareTool.php +++ b/cli/Valet/ShareTools/ShareTool.php @@ -90,8 +90,7 @@ public function currentTunnelUrl(string $site) { if (isset($body->tunnels) && count($body->tunnels) > 0) { // If the tunnel URL is NOT null, return the URL. if ($tunnelUrl = $this->findHttpTunnelUrl($body->tunnels, $site)) { - // Use | clip to copy the URL to the clipboard. - $this->cli->passthru("echo $tunnelUrl | clip"); + $this->copyUrlToClipboard($tunnelUrl); return $tunnelUrl; } @@ -132,4 +131,19 @@ public function findHttpTunnelUrl(array $tunnels, ?string $site = null) { } return null; } + + /** + * Copy the public URL to the clipboard. + * + * @param string $url The public URL to copy. + */ + public function copyUrlToClipboard(string $url) { + // Escape single quotes in the URL for PowerShell. + $escapedUrl = str_replace("'", "''", $url); + // The single quotes around the URL are necessary to ensure that PowerShell treats it as + // a literal string, even if it contains spaces or special shell characters and prevents + // command injection. + $this->cli->powershell("'{$escapedUrl}' | Set-Clipboard"); + info("It has been copied to your clipboard."); + } } diff --git a/cli/includes/helpers.php b/cli/includes/helpers.php index 667780c4..1f8d5ee1 100644 --- a/cli/includes/helpers.php +++ b/cli/includes/helpers.php @@ -11,6 +11,7 @@ use Symfony\Component\Console\Helper\TableSeparator; use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Output\ConsoleOutput; +use Symfony\Component\Console\Formatter\OutputFormatter; if (!isset($_SERVER['HOME'])) { $_SERVER['HOME'] = $_SERVER['USERPROFILE']; @@ -62,11 +63,16 @@ function warning($output) { * * @param string $output * @param bool $exception Optionally pass a boolean to indicate whether to throw an exception. If `true`, the error will be thrown as a `ValetException`. [default: `false`] + * @param bool $newline Whether to append a newline after the error output. [default: `true`] + * @param bool $escapeOutput Whether to escape the output to prevent formatting issues. [default: `false`] * * @throws RuntimeException * @throws ValetException */ -function error(string $output, $exception = false) { +function error(string $output, bool $exception = false, bool $newline = true, bool $escapeOutput = false) { + + $errorOutput = (new ConsoleOutput())->getErrorOutput(); + if (isset($_ENV['APP_ENV']) && $_ENV['APP_ENV'] === 'testing') { throw new RuntimeException($output); } @@ -78,12 +84,17 @@ function error(string $output, $exception = false) { usleep(1); // Print the error message to the console. - (new ConsoleOutput())->getErrorOutput()->writeln("\n\n$errors"); + $errorOutput->write("\n\n$errors", $newline); exit(); } else { - (new ConsoleOutput())->getErrorOutput()->writeln("$output"); + // If escapeOutput is true, then escape the output to prevent any formatting issues. + if ($escapeOutput) { + $output = OutputFormatter::escape($output); + } + + $errorOutput->write("$output", $newline); } } @@ -91,12 +102,13 @@ function error(string $output, $exception = false) { * Output the given text to the console. * * @param string $output + * @param bool $newline Whether to append a newline after the output. [default: `true`] */ -function output($output) { +function output($output, bool $newline = true) { if (isset($_ENV['APP_ENV']) && $_ENV['APP_ENV'] === 'testing') { return; } - (new ConsoleOutput())->writeln($output); + (new ConsoleOutput())->write($output, $newline); } /** diff --git a/cli/valet.php b/cli/valet.php index 1a23e90a..2c49eaf9 100644 --- a/cli/valet.php +++ b/cli/valet.php @@ -978,7 +978,6 @@ $url = Share::shareTool()->currentTunnelUrl($site); info("The public URL for $site is: $url"); - info("It has been copied to your clipboard."); })->setAliases(["url"])->descriptions('Get and copy the public URL of the current working directory site that is currently being shared', [ "site" => "Optionally, specify a site"