Skip to content
Merged
2 changes: 0 additions & 2 deletions .github/workflows/php83.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ jobs:
name: Code Quality
needs: test
uses: WebFiori/workflows/.github/workflows/quality-sonarcloud.yaml@main
with:
coverage-file: 'php-8.3-coverage.xml'
secrets:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

Expand Down
38 changes: 38 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Changelog

## [2.0.1](https://github.com/WebFiori/cli/compare/v2.0.0...v2.0.1) (2025-10-06)


### Bug Fixes

* Default Value for `select` ([6acd1fa](https://github.com/WebFiori/cli/commit/6acd1fac5f3b9e89b41b4d39a654c23321de5720))

## [2.0.0](https://github.com/WebFiori/cli/compare/v1.3.1...v2.0.0) (2025-09-27)


### Features

* Aliasing of Commands ([660a179](https://github.com/WebFiori/cli/commit/660a1790ead3a7e0fc9d052422d376e038583f6e))
* Auto-Discovery of Commands ([72c7fff](https://github.com/WebFiori/cli/commit/72c7fff4f37f42452534be8642cfc390e1e31214))
* Help Command for All ([9d8772a](https://github.com/WebFiori/cli/commit/9d8772ac797f38d8790706667392e88428ef672c))
* Table Display ([857ed5a](https://github.com/WebFiori/cli/commit/857ed5a38f78972934f301b58fc1a6ea3a4e616f))
* Tables Display ([1cfbb48](https://github.com/WebFiori/cli/commit/1cfbb486ed6ee95994c60530e92dc4d05f1cae80))


### Bug Fixes

* App Path ([bdbbc6a](https://github.com/WebFiori/cli/commit/bdbbc6a7d68c3ccad98ba5bb129dcd3d763fcc6a))
* Help Command ([e97ac83](https://github.com/WebFiori/cli/commit/e97ac83f1e2a0b39024d5c62861a6f19b168424d))
* Namespaces Correction ([a07c08e](https://github.com/WebFiori/cli/commit/a07c08ea6bfa16879f88d1f2f004288f625f85bc))
* Use of Self ([4bff72b](https://github.com/WebFiori/cli/commit/4bff72b218154f6d36957d8c67acdd09c31b2d7e))


### Miscellaneous Chores

* Added More Code Samples ([af30558](https://github.com/WebFiori/cli/commit/af30558522ba780a63fb3eb23c3cd20206178f8e))
* Release 2.0.0 ([cb763c5](https://github.com/WebFiori/cli/commit/cb763c556bdbbd8538935eacf6936b233ff271d1))
* Release 2.0.0 ([2a29b9d](https://github.com/WebFiori/cli/commit/2a29b9d53b6887ea8fb3157529b51d1fb05c00e4))
* Update README.md ([5c940a1](https://github.com/WebFiori/cli/commit/5c940a1a287ea8633d9aab9e0634b8a2fc40a406))
* Update README.md ([b4f1dcf](https://github.com/WebFiori/cli/commit/b4f1dcfa277fc0adc097e9244007ef3528a6b466))
* Updated Config ([1df09ae](https://github.com/WebFiori/cli/commit/1df09ae140497270a65335db2b6b35c1d78d8cfc))
* Updated README ([53c7471](https://github.com/WebFiori/cli/commit/53c7471629be117e61bb8b8c85e1a5d2cb0ccc83))
108 changes: 107 additions & 1 deletion WebFiori/Cli/Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,56 @@ public function getInput(string $prompt, ?string $default = null, ?InputValidato
return null;
}
/**
* Reads user input with characters masked by a specified character.
*
* This method is similar to getInput() but masks the input characters as the user types,
* making it suitable for sensitive information like passwords, tokens, or secrets.
* The actual input value is captured but only mask characters are displayed in the terminal.
*
* @param string $prompt The prompt message to display to the user. Must be non-empty.
*
* @param string $mask The character to display instead of the actual input characters.
* Default is '*'. Can be any single character or string.
*
* @param string|null $default An optional default value to use if the user provides
* empty input. If provided, it will be shown in the prompt.
*
* @param InputValidator|null $validator An optional validator to validate the input.
* If validation fails, the user will be prompted again.
*
* @return string|null Returns the actual input value (not masked) if valid input is provided,
* or null if the prompt is empty.
*
* @since 1.1.0
*/
public function getMaskedInput(string $prompt, string $mask = '*', ?string $default = null, ?InputValidator $validator = null): ?string {
$trimmed = trim($prompt);

if (strlen($trimmed) > 0) {
do {
$this->prints($trimmed, [
'color' => 'gray',
'bold' => true
]);

if ($default !== null) {
$this->prints(" Enter = '".$default."'", [
'color' => 'light-blue'
]);
}
$this->println();
$input = trim($this->readMaskedLine($mask));

$check = $this->getInputHelper($input, $validator, $default);

if ($check['valid']) {
return $check['value'];
}
} while (true);
}

return null;
} /**
* Returns the stream at which the command is sing to read inputs.
*
* @return null|InputStream If the stream is set, it will be returned as
Expand Down Expand Up @@ -964,7 +1014,63 @@ public function readInteger(string $prompt, ?int $default = null) : int {
public function readln() : string {
return $this->getInputStream()->readLine();
}

/**
* Reads a line from input stream with character masking.
*
* This method reads input character by character and displays mask characters
* instead of the actual input. It handles backspace for character deletion
* and ignores special keys like ESC and arrow keys.
*
* @param string $mask The character to display instead of actual input characters.
*
* @return string The actual input string (unmasked).
*
* @since 1.1.0
*/
private function readMaskedLine(string $mask = '*'): string {
$input = '';

// For testing with ArrayInputStream, read the whole line at once
if ($this->getInputStream() instanceof \WebFiori\Cli\Streams\ArrayInputStream) {
$input = $this->getInputStream()->readLine();
// Simulate masking output for testing
$this->prints(str_repeat($mask, strlen($input)));
$this->println();
return $input;
}

// Set terminal to raw mode with echo disabled for real-time character reading
$sttyMode = null;
if (function_exists('shell_exec') && PHP_OS_FAMILY !== 'Windows') {
$sttyMode = shell_exec('stty -g 2>/dev/null');
shell_exec('stty -echo -icanon 2>/dev/null');
}

try {
// For real terminal input, read character by character
while (true) {
$char = KeysMap::readAndTranslate($this->getInputStream());

if ($char === 'LF' || $char === 'CR' || $char === '') {
break;
} elseif ($char === 'BACKSPACE' && strlen($input) > 0) {
$input = substr($input, 0, -1);
$this->prints("\x08 \x08"); // Backspace, space, backspace
} elseif ($char !== 'BACKSPACE' && $char !== 'ESC' && $char !== 'DOWN' && $char !== 'UP' && $char !== 'LEFT' && $char !== 'RIGHT') {
$input .= $char === 'SPACE' ? ' ' : $char;
$this->prints($mask);
}
}
} finally {
// Restore terminal settings
if ($sttyMode !== null) {
shell_exec('stty ' . $sttyMode . ' 2>/dev/null');
}
}

$this->println();
return $input;
}
/**
* Reads a string that represents class namespace.
*
Expand Down
22 changes: 11 additions & 11 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,35 @@
"email": "[email protected]"
}
],
"license":"MIT",
"keywords":[
"license": "MIT",
"keywords": [
"cli",
"command line",
"php",
"terminal"
],
"require": {
"php": "^8.1",
"webfiori/file":"2.0.*"
"webfiori/file": "2.0.*"
},
"require-dev": {
"phpunit/phpunit": "^10.0",
"friendsofphp/php-cs-fixer": "^3.86"
},
"autoload" :{
"psr-4":{
"WebFiori\\Cli\\":"WebFiori/Cli"
"autoload": {
"psr-4": {
"WebFiori\\Cli\\": "WebFiori/Cli"
}
},
"autoload-dev" :{
"psr-4":{
"WebFiori\\Tests\\":"tests/WebFiori/Tests"
"autoload-dev": {
"psr-4": {
"WebFiori\\Tests\\": "tests/WebFiori/Tests"
}
},
"scripts" : {
"scripts": {
"test": "vendor/bin/phpunit -c tests/phpunit.xml",
"test10": "vendor/bin/phpunit -c tests/phpunit10.xml",
"wfcli":"bin/wfc",
"wfcli": "bin/wfc",
"check-cs": "bin/ecs check --ansi",
"fix-cs": "vendor/bin/php-cs-fixer fix --config=php_cs.php.dist",
"phpstan": "vendor/bin/phpstan analyse --ansi --error-format symplify"
Expand Down
165 changes: 165 additions & 0 deletions examples/11-masked-input/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# Masked Input Example

This example demonstrates the **masked input functionality** in WebFiori CLI, which allows secure entry of sensitive data like passwords, PINs, and tokens.

## Features Demonstrated

- **Basic Password Input**: Default asterisk (*) masking with validation
- **Custom Mask Characters**: Use different characters (•, #, X, -) for masking
- **Input Validation**: Enforce security requirements and format validation
- **Default Values**: Optional default values for sensitive fields
- **Confirmation Prompts**: Verify critical inputs by asking twice

## Running the Example

### Basic Usage
```bash
php main.php secure-input
```

### Run Specific Demos
```bash
# Password demo only
php main.php secure-input --demo=password

# PIN demo with custom mask
php main.php secure-input --demo=pin

# Token demo with default value
php main.php secure-input --demo=token

# All demos (default)
php main.php secure-input --demo=all
```

## Code Examples

### Basic Masked Input
```php
// Simple password input with default * masking
$password = $this->getMaskedInput('Enter password: ');
```

### Custom Mask Character
```php
// Use # characters for PIN masking
$pin = $this->getMaskedInput('Enter PIN: ', null, null, '#');
```

### With Validation
```php
$validator = new InputValidator(function($password) {
return strlen($password) >= 8 &&
preg_match('/[A-Z]/', $password) &&
preg_match('/[0-9]/', $password);
}, 'Password must be 8+ chars with uppercase and number!');

$password = $this->getMaskedInput('Password: ', null, $validator);
```

### With Default Value
```php
// Provide a default token value
$token = $this->getMaskedInput('API Token: ', 'default-token', null, '•');
```

## Method Signature

```php
public function getMaskedInput(
string $prompt, // The prompt to display
?string $default = null, // Optional default value
?InputValidator $validator = null, // Optional input validator
string $mask = '*' // Mask character (default: *)
): ?string
```

## Security Features

### Input Masking
- Characters are masked as you type
- Only mask characters are displayed in terminal
- Actual input is captured securely
- Supports backspace for corrections

### Validation Support
- Enforce minimum length requirements
- Validate character patterns (uppercase, numbers, symbols)
- Custom validation logic
- Automatic retry on validation failure

### Safe Handling
- Input is trimmed automatically
- Empty prompts return null safely
- Works with existing stream abstraction
- Compatible with testing framework

## Use Cases

### 1. User Authentication
```php
$password = $this->getMaskedInput('Login Password: ');
$confirmPassword = $this->getMaskedInput('Confirm Password: ');

if ($password !== $confirmPassword) {
$this->error('Passwords do not match!');
return 1;
}
```

### 2. API Configuration
```php
$apiKey = $this->getMaskedInput('API Key: ', null, null, '•');
$secret = $this->getMaskedInput('API Secret: ', null, null, '-');
```

### 3. Database Setup
```php
$dbPassword = $this->getMaskedInput('Database Password: ');

$validator = new InputValidator(function($host) {
return filter_var($host, FILTER_VALIDATE_IP) ||
filter_var($host, FILTER_VALIDATE_DOMAIN);
}, 'Invalid host format!');

$dbHost = $this->getInput('Database Host: ', 'localhost', $validator);
```

### 4. Secure Token Entry
```php
$jwtSecret = $this->getMaskedInput('JWT Secret: ', null,
new InputValidator(function($secret) {
return strlen($secret) >= 32;
}, 'JWT secret must be at least 32 characters!')
);
```

## Interactive Demo Features

The example includes several interactive demonstrations:

1. **Password Demo**: Shows validation with security requirements
2. **PIN Demo**: Demonstrates custom mask characters (#)
3. **Token Demo**: Shows default values with bullet (•) masking
4. **Advanced Demo**: Multiple scenarios including confirmation prompts

## Testing

The masked input functionality is fully testable using the existing `CommandTestCase` framework:

```php
$output = $this->executeSingleCommand($command, [], ['secret123']);
$this->assertContains('Password received: secret123', $output);
```

## Best Practices

1. **Always validate sensitive input** for security requirements
2. **Use appropriate mask characters** for different data types
3. **Implement confirmation prompts** for critical operations
4. **Never log or display** the actual sensitive values
5. **Provide clear error messages** for validation failures

---

**Ready to secure your CLI applications?** Try the different demo modes to see masked input in action!
Loading
Loading