Skip to content

Commit e4ee130

Browse files
authored
chore: Merge pull request #37 from WebFiori/feat-input-mask
Feat input mask
2 parents 6acd1fa + e4b9b4e commit e4ee130

File tree

9 files changed

+678
-16
lines changed

9 files changed

+678
-16
lines changed

.github/workflows/php83.yaml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,6 @@ jobs:
3232
name: Code Quality
3333
needs: test
3434
uses: WebFiori/workflows/.github/workflows/quality-sonarcloud.yaml@main
35-
with:
36-
coverage-file: 'php-8.3-coverage.xml'
3735
secrets:
3836
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
3937

CHANGELOG.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Changelog
2+
3+
## [2.0.1](https://github.com/WebFiori/cli/compare/v2.0.0...v2.0.1) (2025-10-06)
4+
5+
6+
### Bug Fixes
7+
8+
* Default Value for `select` ([6acd1fa](https://github.com/WebFiori/cli/commit/6acd1fac5f3b9e89b41b4d39a654c23321de5720))
9+
10+
## [2.0.0](https://github.com/WebFiori/cli/compare/v1.3.1...v2.0.0) (2025-09-27)
11+
12+
13+
### Features
14+
15+
* Aliasing of Commands ([660a179](https://github.com/WebFiori/cli/commit/660a1790ead3a7e0fc9d052422d376e038583f6e))
16+
* Auto-Discovery of Commands ([72c7fff](https://github.com/WebFiori/cli/commit/72c7fff4f37f42452534be8642cfc390e1e31214))
17+
* Help Command for All ([9d8772a](https://github.com/WebFiori/cli/commit/9d8772ac797f38d8790706667392e88428ef672c))
18+
* Table Display ([857ed5a](https://github.com/WebFiori/cli/commit/857ed5a38f78972934f301b58fc1a6ea3a4e616f))
19+
* Tables Display ([1cfbb48](https://github.com/WebFiori/cli/commit/1cfbb486ed6ee95994c60530e92dc4d05f1cae80))
20+
21+
22+
### Bug Fixes
23+
24+
* App Path ([bdbbc6a](https://github.com/WebFiori/cli/commit/bdbbc6a7d68c3ccad98ba5bb129dcd3d763fcc6a))
25+
* Help Command ([e97ac83](https://github.com/WebFiori/cli/commit/e97ac83f1e2a0b39024d5c62861a6f19b168424d))
26+
* Namespaces Correction ([a07c08e](https://github.com/WebFiori/cli/commit/a07c08ea6bfa16879f88d1f2f004288f625f85bc))
27+
* Use of Self ([4bff72b](https://github.com/WebFiori/cli/commit/4bff72b218154f6d36957d8c67acdd09c31b2d7e))
28+
29+
30+
### Miscellaneous Chores
31+
32+
* Added More Code Samples ([af30558](https://github.com/WebFiori/cli/commit/af30558522ba780a63fb3eb23c3cd20206178f8e))
33+
* Release 2.0.0 ([cb763c5](https://github.com/WebFiori/cli/commit/cb763c556bdbbd8538935eacf6936b233ff271d1))
34+
* Release 2.0.0 ([2a29b9d](https://github.com/WebFiori/cli/commit/2a29b9d53b6887ea8fb3157529b51d1fb05c00e4))
35+
* Update README.md ([5c940a1](https://github.com/WebFiori/cli/commit/5c940a1a287ea8633d9aab9e0634b8a2fc40a406))
36+
* Update README.md ([b4f1dcf](https://github.com/WebFiori/cli/commit/b4f1dcfa277fc0adc097e9244007ef3528a6b466))
37+
* Updated Config ([1df09ae](https://github.com/WebFiori/cli/commit/1df09ae140497270a65335db2b6b35c1d78d8cfc))
38+
* Updated README ([53c7471](https://github.com/WebFiori/cli/commit/53c7471629be117e61bb8b8c85e1a5d2cb0ccc83))

WebFiori/Cli/Command.php

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,56 @@ public function getInput(string $prompt, ?string $default = null, ?InputValidato
579579
return null;
580580
}
581581
/**
582+
* Reads user input with characters masked by a specified character.
583+
*
584+
* This method is similar to getInput() but masks the input characters as the user types,
585+
* making it suitable for sensitive information like passwords, tokens, or secrets.
586+
* The actual input value is captured but only mask characters are displayed in the terminal.
587+
*
588+
* @param string $prompt The prompt message to display to the user. Must be non-empty.
589+
*
590+
* @param string $mask The character to display instead of the actual input characters.
591+
* Default is '*'. Can be any single character or string.
592+
*
593+
* @param string|null $default An optional default value to use if the user provides
594+
* empty input. If provided, it will be shown in the prompt.
595+
*
596+
* @param InputValidator|null $validator An optional validator to validate the input.
597+
* If validation fails, the user will be prompted again.
598+
*
599+
* @return string|null Returns the actual input value (not masked) if valid input is provided,
600+
* or null if the prompt is empty.
601+
*
602+
* @since 1.1.0
603+
*/
604+
public function getMaskedInput(string $prompt, string $mask = '*', ?string $default = null, ?InputValidator $validator = null): ?string {
605+
$trimmed = trim($prompt);
606+
607+
if (strlen($trimmed) > 0) {
608+
do {
609+
$this->prints($trimmed, [
610+
'color' => 'gray',
611+
'bold' => true
612+
]);
613+
614+
if ($default !== null) {
615+
$this->prints(" Enter = '".$default."'", [
616+
'color' => 'light-blue'
617+
]);
618+
}
619+
$this->println();
620+
$input = trim($this->readMaskedLine($mask));
621+
622+
$check = $this->getInputHelper($input, $validator, $default);
623+
624+
if ($check['valid']) {
625+
return $check['value'];
626+
}
627+
} while (true);
628+
}
629+
630+
return null;
631+
} /**
582632
* Returns the stream at which the command is sing to read inputs.
583633
*
584634
* @return null|InputStream If the stream is set, it will be returned as
@@ -964,7 +1014,63 @@ public function readInteger(string $prompt, ?int $default = null) : int {
9641014
public function readln() : string {
9651015
return $this->getInputStream()->readLine();
9661016
}
967-
1017+
/**
1018+
* Reads a line from input stream with character masking.
1019+
*
1020+
* This method reads input character by character and displays mask characters
1021+
* instead of the actual input. It handles backspace for character deletion
1022+
* and ignores special keys like ESC and arrow keys.
1023+
*
1024+
* @param string $mask The character to display instead of actual input characters.
1025+
*
1026+
* @return string The actual input string (unmasked).
1027+
*
1028+
* @since 1.1.0
1029+
*/
1030+
private function readMaskedLine(string $mask = '*'): string {
1031+
$input = '';
1032+
1033+
// For testing with ArrayInputStream, read the whole line at once
1034+
if ($this->getInputStream() instanceof \WebFiori\Cli\Streams\ArrayInputStream) {
1035+
$input = $this->getInputStream()->readLine();
1036+
// Simulate masking output for testing
1037+
$this->prints(str_repeat($mask, strlen($input)));
1038+
$this->println();
1039+
return $input;
1040+
}
1041+
1042+
// Set terminal to raw mode with echo disabled for real-time character reading
1043+
$sttyMode = null;
1044+
if (function_exists('shell_exec') && PHP_OS_FAMILY !== 'Windows') {
1045+
$sttyMode = shell_exec('stty -g 2>/dev/null');
1046+
shell_exec('stty -echo -icanon 2>/dev/null');
1047+
}
1048+
1049+
try {
1050+
// For real terminal input, read character by character
1051+
while (true) {
1052+
$char = KeysMap::readAndTranslate($this->getInputStream());
1053+
1054+
if ($char === 'LF' || $char === 'CR' || $char === '') {
1055+
break;
1056+
} elseif ($char === 'BACKSPACE' && strlen($input) > 0) {
1057+
$input = substr($input, 0, -1);
1058+
$this->prints("\x08 \x08"); // Backspace, space, backspace
1059+
} elseif ($char !== 'BACKSPACE' && $char !== 'ESC' && $char !== 'DOWN' && $char !== 'UP' && $char !== 'LEFT' && $char !== 'RIGHT') {
1060+
$input .= $char === 'SPACE' ? ' ' : $char;
1061+
$this->prints($mask);
1062+
}
1063+
}
1064+
} finally {
1065+
// Restore terminal settings
1066+
if ($sttyMode !== null) {
1067+
shell_exec('stty ' . $sttyMode . ' 2>/dev/null');
1068+
}
1069+
}
1070+
1071+
$this->println();
1072+
return $input;
1073+
}
9681074
/**
9691075
* Reads a string that represents class namespace.
9701076
*

composer.json

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,35 +7,35 @@
77
"email": "ibrahim@webfiori.com"
88
}
99
],
10-
"license":"MIT",
11-
"keywords":[
10+
"license": "MIT",
11+
"keywords": [
1212
"cli",
1313
"command line",
1414
"php",
1515
"terminal"
1616
],
1717
"require": {
1818
"php": "^8.1",
19-
"webfiori/file":"2.0.*"
19+
"webfiori/file": "2.0.*"
2020
},
2121
"require-dev": {
2222
"phpunit/phpunit": "^10.0",
2323
"friendsofphp/php-cs-fixer": "^3.86"
2424
},
25-
"autoload" :{
26-
"psr-4":{
27-
"WebFiori\\Cli\\":"WebFiori/Cli"
25+
"autoload": {
26+
"psr-4": {
27+
"WebFiori\\Cli\\": "WebFiori/Cli"
2828
}
2929
},
30-
"autoload-dev" :{
31-
"psr-4":{
32-
"WebFiori\\Tests\\":"tests/WebFiori/Tests"
30+
"autoload-dev": {
31+
"psr-4": {
32+
"WebFiori\\Tests\\": "tests/WebFiori/Tests"
3333
}
3434
},
35-
"scripts" : {
35+
"scripts": {
3636
"test": "vendor/bin/phpunit -c tests/phpunit.xml",
3737
"test10": "vendor/bin/phpunit -c tests/phpunit10.xml",
38-
"wfcli":"bin/wfc",
38+
"wfcli": "bin/wfc",
3939
"check-cs": "bin/ecs check --ansi",
4040
"fix-cs": "vendor/bin/php-cs-fixer fix --config=php_cs.php.dist",
4141
"phpstan": "vendor/bin/phpstan analyse --ansi --error-format symplify"

examples/11-masked-input/README.md

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
# Masked Input Example
2+
3+
This example demonstrates the **masked input functionality** in WebFiori CLI, which allows secure entry of sensitive data like passwords, PINs, and tokens.
4+
5+
## Features Demonstrated
6+
7+
- **Basic Password Input**: Default asterisk (*) masking with validation
8+
- **Custom Mask Characters**: Use different characters (•, #, X, -) for masking
9+
- **Input Validation**: Enforce security requirements and format validation
10+
- **Default Values**: Optional default values for sensitive fields
11+
- **Confirmation Prompts**: Verify critical inputs by asking twice
12+
13+
## Running the Example
14+
15+
### Basic Usage
16+
```bash
17+
php main.php secure-input
18+
```
19+
20+
### Run Specific Demos
21+
```bash
22+
# Password demo only
23+
php main.php secure-input --demo=password
24+
25+
# PIN demo with custom mask
26+
php main.php secure-input --demo=pin
27+
28+
# Token demo with default value
29+
php main.php secure-input --demo=token
30+
31+
# All demos (default)
32+
php main.php secure-input --demo=all
33+
```
34+
35+
## Code Examples
36+
37+
### Basic Masked Input
38+
```php
39+
// Simple password input with default * masking
40+
$password = $this->getMaskedInput('Enter password: ');
41+
```
42+
43+
### Custom Mask Character
44+
```php
45+
// Use # characters for PIN masking
46+
$pin = $this->getMaskedInput('Enter PIN: ', null, null, '#');
47+
```
48+
49+
### With Validation
50+
```php
51+
$validator = new InputValidator(function($password) {
52+
return strlen($password) >= 8 &&
53+
preg_match('/[A-Z]/', $password) &&
54+
preg_match('/[0-9]/', $password);
55+
}, 'Password must be 8+ chars with uppercase and number!');
56+
57+
$password = $this->getMaskedInput('Password: ', null, $validator);
58+
```
59+
60+
### With Default Value
61+
```php
62+
// Provide a default token value
63+
$token = $this->getMaskedInput('API Token: ', 'default-token', null, '•');
64+
```
65+
66+
## Method Signature
67+
68+
```php
69+
public function getMaskedInput(
70+
string $prompt, // The prompt to display
71+
?string $default = null, // Optional default value
72+
?InputValidator $validator = null, // Optional input validator
73+
string $mask = '*' // Mask character (default: *)
74+
): ?string
75+
```
76+
77+
## Security Features
78+
79+
### Input Masking
80+
- Characters are masked as you type
81+
- Only mask characters are displayed in terminal
82+
- Actual input is captured securely
83+
- Supports backspace for corrections
84+
85+
### Validation Support
86+
- Enforce minimum length requirements
87+
- Validate character patterns (uppercase, numbers, symbols)
88+
- Custom validation logic
89+
- Automatic retry on validation failure
90+
91+
### Safe Handling
92+
- Input is trimmed automatically
93+
- Empty prompts return null safely
94+
- Works with existing stream abstraction
95+
- Compatible with testing framework
96+
97+
## Use Cases
98+
99+
### 1. User Authentication
100+
```php
101+
$password = $this->getMaskedInput('Login Password: ');
102+
$confirmPassword = $this->getMaskedInput('Confirm Password: ');
103+
104+
if ($password !== $confirmPassword) {
105+
$this->error('Passwords do not match!');
106+
return 1;
107+
}
108+
```
109+
110+
### 2. API Configuration
111+
```php
112+
$apiKey = $this->getMaskedInput('API Key: ', null, null, '•');
113+
$secret = $this->getMaskedInput('API Secret: ', null, null, '-');
114+
```
115+
116+
### 3. Database Setup
117+
```php
118+
$dbPassword = $this->getMaskedInput('Database Password: ');
119+
120+
$validator = new InputValidator(function($host) {
121+
return filter_var($host, FILTER_VALIDATE_IP) ||
122+
filter_var($host, FILTER_VALIDATE_DOMAIN);
123+
}, 'Invalid host format!');
124+
125+
$dbHost = $this->getInput('Database Host: ', 'localhost', $validator);
126+
```
127+
128+
### 4. Secure Token Entry
129+
```php
130+
$jwtSecret = $this->getMaskedInput('JWT Secret: ', null,
131+
new InputValidator(function($secret) {
132+
return strlen($secret) >= 32;
133+
}, 'JWT secret must be at least 32 characters!')
134+
);
135+
```
136+
137+
## Interactive Demo Features
138+
139+
The example includes several interactive demonstrations:
140+
141+
1. **Password Demo**: Shows validation with security requirements
142+
2. **PIN Demo**: Demonstrates custom mask characters (#)
143+
3. **Token Demo**: Shows default values with bullet (•) masking
144+
4. **Advanced Demo**: Multiple scenarios including confirmation prompts
145+
146+
## Testing
147+
148+
The masked input functionality is fully testable using the existing `CommandTestCase` framework:
149+
150+
```php
151+
$output = $this->executeSingleCommand($command, [], ['secret123']);
152+
$this->assertContains('Password received: secret123', $output);
153+
```
154+
155+
## Best Practices
156+
157+
1. **Always validate sensitive input** for security requirements
158+
2. **Use appropriate mask characters** for different data types
159+
3. **Implement confirmation prompts** for critical operations
160+
4. **Never log or display** the actual sensitive values
161+
5. **Provide clear error messages** for validation failures
162+
163+
---
164+
165+
**Ready to secure your CLI applications?** Try the different demo modes to see masked input in action!

0 commit comments

Comments
 (0)