diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 0190255a22..61887a64e1 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -146,6 +146,7 @@ return array( 'rsrc/css/phui/phui-image-mask.css' => 'a8498f9c', 'rsrc/css/phui/phui-info-panel.css' => '27ea50a1', 'rsrc/css/phui/phui-info-view.css' => '28efab79', + 'rsrc/css/phui/phui-invisible-character-view.css' => '6993d9f0', 'rsrc/css/phui/phui-list.css' => '9da2aa00', 'rsrc/css/phui/phui-object-box.css' => '6b487c57', 'rsrc/css/phui/phui-object-item-list-view.css' => '87278fa0', @@ -922,6 +923,7 @@ return array( 'phui-info-panel-css' => '27ea50a1', 'phui-info-view-css' => '28efab79', 'phui-inline-comment-view-css' => '5953c28e', + 'phui-invisible-character-view-css' => '6993d9f0', 'phui-list-view-css' => '9da2aa00', 'phui-object-box-css' => '6b487c57', 'phui-object-item-list-view-css' => '87278fa0', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index a38984339b..bbd0bbab2b 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1652,6 +1652,8 @@ phutil_register_library_map(array( 'PHUIInfoPanelExample' => 'applications/uiexample/examples/PHUIInfoPanelExample.php', 'PHUIInfoPanelView' => 'view/phui/PHUIInfoPanelView.php', 'PHUIInfoView' => 'view/form/PHUIInfoView.php', + 'PHUIInvisibleCharacterTestCase' => 'view/phui/__tests__/PHUIInvisibleCharacterTestCase.php', + 'PHUIInvisibleCharacterView' => 'view/phui/PHUIInvisibleCharacterView.php', 'PHUIListExample' => 'applications/uiexample/examples/PHUIListExample.php', 'PHUIListItemView' => 'view/phui/PHUIListItemView.php', 'PHUIListView' => 'view/phui/PHUIListView.php', @@ -6320,6 +6322,8 @@ phutil_register_library_map(array( 'PHUIInfoPanelExample' => 'PhabricatorUIExample', 'PHUIInfoPanelView' => 'AphrontView', 'PHUIInfoView' => 'AphrontView', + 'PHUIInvisibleCharacterTestCase' => 'PhabricatorTestCase', + 'PHUIInvisibleCharacterView' => 'AphrontView', 'PHUIListExample' => 'PhabricatorUIExample', 'PHUIListItemView' => 'AphrontTagView', 'PHUIListView' => 'AphrontTagView', diff --git a/src/applications/auth/controller/PhabricatorAuthRegisterController.php b/src/applications/auth/controller/PhabricatorAuthRegisterController.php index 1d036b13a9..3e4135fff5 100644 --- a/src/applications/auth/controller/PhabricatorAuthRegisterController.php +++ b/src/applications/auth/controller/PhabricatorAuthRegisterController.php @@ -89,13 +89,14 @@ final class PhabricatorAuthRegisterController // user expectation and it's not clear the cases it enables are valuable. // See discussion in T3472. if (!PhabricatorUserEmail::isAllowedAddress($default_email)) { + $debug_email = new PHUIInvisibleCharacterView($default_email); return $this->renderError( array( pht( 'The account you are attempting to register with has an invalid '. 'email address (%s). This Phabricator install only allows '. 'registration with specific email addresses:', - $default_email), + $debug_email), phutil_tag('br'), phutil_tag('br'), PhabricatorUserEmail::describeAllowedAddresses(), diff --git a/src/applications/phurl/editor/PhabricatorPhurlURLEditor.php b/src/applications/phurl/editor/PhabricatorPhurlURLEditor.php index 64cfe6046b..9fe55bcbeb 100644 --- a/src/applications/phurl/editor/PhabricatorPhurlURLEditor.php +++ b/src/applications/phurl/editor/PhabricatorPhurlURLEditor.php @@ -123,18 +123,23 @@ final class PhabricatorPhurlURLEditor foreach ($xactions as $xaction) { if ($xaction->getOldValue() != $xaction->getNewValue()) { $new_alias = $xaction->getNewValue(); + $debug_alias = new PHUIInvisibleCharacterView($new_alias); if (!preg_match('/[a-zA-Z]/', $new_alias)) { $errors[] = new PhabricatorApplicationTransactionValidationError( $type, pht('Invalid Alias'), - pht('The alias must contain at least one letter.'), + pht('The alias you provided (%s) must contain at least one '. + 'letter.', + $debug_alias), $xaction); } if (preg_match('/[^a-z0-9]/i', $new_alias)) { $errors[] = new PhabricatorApplicationTransactionValidationError( $type, pht('Invalid Alias'), - pht('The alias may only contain letters and numbers.'), + pht('The alias you provided (%s) may only contain letters and '. + 'numbers.', + $debug_alias), $xaction); } } diff --git a/src/view/phui/PHUIInvisibleCharacterView.php b/src/view/phui/PHUIInvisibleCharacterView.php new file mode 100644 index 0000000000..dda00f81a1 --- /dev/null +++ b/src/view/phui/PHUIInvisibleCharacterView.php @@ -0,0 +1,94 @@ + 'NULL', + "\t" => 'TAB', + "\n" => 'NEWLINE', + "\x20" => 'SPACE', + ); + + public function __construct($input_text) { + $this->inputText = $input_text; + } + + public function setPlainText($plain_text) { + $this->plainText = $plain_text; + return $this; + } + + public function getStringParts() { + $input_text = $this->inputText; + $text_array = phutil_utf8v($input_text); + + for ($ii = 0; $ii < count($text_array); $ii++) { + $char = $text_array[$ii]; + $char_hex = bin2hex($char); + if (array_key_exists($char, self::$invisibleChars)) { + $text_array[$ii] = array( + 'special' => true, + 'value' => '<'.self::$invisibleChars[$char].'>', + ); + } else if (ord($char) < 32) { + $text_array[$ii] = array( + 'special' => true, + 'value' => '<0x'.$char_hex.'>', + ); + } else { + $text_array[$ii] = array( + 'special' => false, + 'value' => $char, + ); + } + } + return $text_array; + } + + private function renderHtmlArray() { + $html_array = array(); + $parts = $this->getStringParts(); + foreach ($parts as $part) { + if ($part['special']) { + $html_array[] = phutil_tag( + 'span', + array('class' => 'invisible-special'), + $part['value']); + } else { + $html_array[] = $part['value']; + } + } + return $html_array; + } + + private function renderPlainText() { + $parts = $this->getStringParts(); + $res = ''; + foreach ($parts as $part) { + $res .= $part['value']; + } + return $res; + } + + public function render() { + require_celerity_resource('phui-invisible-character-view-css'); + if ($this->plainText) { + return $this->renderPlainText(); + } else { + return $this->renderHtmlArray(); + } + } + +} diff --git a/src/view/phui/__tests__/PHUIInvisibleCharacterTestCase.php b/src/view/phui/__tests__/PHUIInvisibleCharacterTestCase.php new file mode 100644 index 0000000000..09f8c0934b --- /dev/null +++ b/src/view/phui/__tests__/PHUIInvisibleCharacterTestCase.php @@ -0,0 +1,52 @@ +render(); + $this->assertEqual($res, array()); + } + + public function testEmptyPlainText() { + $view = (new PHUIInvisibleCharacterView('')) + ->setPlainText(true); + $res = $view->render(); + $this->assertEqual($res, ''); + } + + public function testWithNamedChars() { + $test_input = "\x00\n\t "; + $view = (new PHUIInvisibleCharacterView($test_input)) + ->setPlainText(true); + $res = $view->render(); + $this->assertEqual($res, ''); + } + + public function testWithHexChars() { + $test_input = "abc\x01"; + $view = (new PHUIInvisibleCharacterView($test_input)) + ->setPlainText(true); + $res = $view->render(); + $this->assertEqual($res, 'abc<0x01>'); + } + + public function testWithNamedAsHex() { + $test_input = "\x00\x0a\x09\x20"; + $view = (new PHUIInvisibleCharacterView($test_input)) + ->setPlainText(true); + $res = $view->render(); + $this->assertEqual($res, ''); + } + + public function testHtmlDecoration() { + $test_input = "a\x00\n\t "; + $view = new PHUIInvisibleCharacterView($test_input); + $res = $view->render(); + $this->assertFalse($res[0] instanceof PhutilSafeHTML); + $this->assertTrue($res[1] instanceof PhutilSafeHTML); + $this->assertTrue($res[2] instanceof PhutilSafeHTML); + $this->assertTrue($res[3] instanceof PhutilSafeHTML); + $this->assertTrue($res[4] instanceof PhutilSafeHTML); + } +} diff --git a/webroot/rsrc/css/phui/phui-invisible-character-view.css b/webroot/rsrc/css/phui/phui-invisible-character-view.css new file mode 100644 index 0000000000..b8a848fa9a --- /dev/null +++ b/webroot/rsrc/css/phui/phui-invisible-character-view.css @@ -0,0 +1,12 @@ +/** + * @provides phui-invisible-character-view-css + */ + +.invisible-special { + font-family: monospace; + color: #000; + background: rgba({$alphablue},0.1); + padding: 1px 4px; + border-radius: 3px; + white-space: pre-wrap; +}