Skip to content

Commit

Permalink
rewrite XHP guides for namespace support (xhp-lib v4)
Browse files Browse the repository at this point in the history
  • Loading branch information
jjergus committed Aug 5, 2020
1 parent 24f6a40 commit 16b4459
Show file tree
Hide file tree
Showing 12 changed files with 334 additions and 189 deletions.
39 changes: 8 additions & 31 deletions guides/hack/21-XHP/01-introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,13 @@ must contain `<title>`.
Using traditional interpolation, a simple page could look like this:

```Hack
<?hh // strict
$user_name = 'Fred';
echo "<tt>Hello <strong>$user_name</strong></tt>";
```

However, with XHP, it looks like this:

```
<?hh // strict
$user_name = 'Fred';
echo <tt>Hello <strong>{$user_name}</strong></tt>;
```
Expand All @@ -26,39 +24,18 @@ fully understood by Hack&mdash;but this does not mean that all you need to do is
- Remove all HTML/attribute escaping - e.g., you don't need to call `htmlspecialchars` before including a variable in your XHP
output; and if you do, it will be double-escaped.

## About Namespaces

XHP currently has several issues with namespaces; we recommend that:
- XHP classes are not declared in namespaces
- code that use XHP classes is not namespaced.

We [plan to support namespaces](https://github.com/facebook/xhp-lib/issues/64) in the future.

## The XHP-Lib Library

While the XHP syntax is part of Hack, a large part of the implementation is in a library called XHP-Lib that needs to be installed via composer;
add `xhp-lib` to your `composer.json`, e.g:

```
$ composer require facebook/xhp-lib
```

This includes the base classes and interfaces, and definitions of standard HTML elements.

Do not use HHVM to execute composer.

## Why use XHP?

The initial reason for most users is because it is *safe by default*: all variables are automatically escaped in a
context-appropriate way (e.g., there are different rules for escaping attribute values vs. text nodes). In addition, XHP
is understood by the typechecker, making sure that you don't pass attribute values. A common example of this is `border="3"`,
is understood by the typechecker, making sure that you don't pass invalid attribute values. A common example of this is `border="3"`,
but `border` is an on/off attribute, so a value of 3 doesn't make sense.

For users experienced with XHP, the biggest advantage is that it is easy to add custom 'elements' with your own behavior,
which can then be used like plain HTML elements. For example, this site defines an `<a:post>` tag that has the same interface
which can then be used like plain HTML elements. For example, this site defines an `<a_post>` tag that has the same interface
as a standard `<a>` tag, but makes a POST request instead of a GET request:

@@ some-basics-examples/a_post.inc.php @@
@@ introduction-examples/a_post.inc.php @@

A little CSS is needed so that the `<form>` doesn't create a block element:

Expand All @@ -70,25 +47,25 @@ form.postLink {

At this point, the new element can be used like any built-in element:

@@ some-basics-examples/a_post_usage.php @@
@@ introduction-examples/a_post_usage.php @@

## Runtime Validation

Since XHP objects are first-class and not just strings, a whole slew of validation can occur to ensure that your UI does not have subtle bugs:

@@ some-basics-examples/tag-matching-validation.php.type-errors @@
@@ introduction-examples/tag-matching-validation.php.type-errors @@

The above code won't typecheck or run because the XHP validator will see that `<span>` and `<naps>` tags are mismatched; however,
the following code will typecheck correctly but fail to run, because while the tags are matched, they are not nested correctly
(according to the HTML specification), and nesting verification only happens at runtime:

@@ some-basics-examples/allowed-tag-validation.inc.php @@
@@ some-basics-examples/allowed-tag-validation.php @@
@@ introduction-examples/allowed-tag-validation.inc.php @@
@@ introduction-examples/allowed-tag-validation.php @@

## Security

String-based entry and validation are prime candidates for cross-site scripting (XSS). You can get around this by using special
functions like [`htmlspecialchars`](http://php.net/manual/en/function.htmlspecialchars.php), but then you have to actually remember
to use those functions. XHP automatically escapes reserved HTML characters to HTML entities before output.

@@ some-basics-examples/avoid-xss.php @@
@@ introduction-examples/avoid-xss.php @@
65 changes: 65 additions & 0 deletions guides/hack/21-XHP/04-setup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
## Namespace Support

Support for namespaced XHP classes (elements) has only recently been added to
HHVM (as of HHVM 4.68). Depending on the exact HHVM version, it may not be
enabled by default, so make sure to add these flags to your `.hhconfig`:

```
enable_xhp_class_modifier=true
disable_xhp_element_mangling=true
disable_xhp_children_declarations=true
check_xhp_attribute=true
```

And to `hhvm.ini` (or provided via `-d` when executing `hhvm`):

```
hhvm.hack.lang.enable_xhp_class_modifier=true
hhvm.hack.lang.disable_xhp_element_mangling=true
```

Using a subset of these flags might work, but is not officially supported and
tested, so we currently recommend using either all or none of them.

If these flags are disabled, or using an older version of HHVM:

- XHP classes cannot be declared in namespaces (only in the global namespace)
- any code that uses XHP classes also cannot be in a namespace, as HHVM
previously didn't have any syntax to reference XHP classes across namespaces
- note that the above two rules are not consistently enforced by the
typechecker or the runtime, but violating them can result in various bugs
- it is, however, possible to use namespaced code from inside XHP class
declarations

Make sure to also use the correct version of XHP-Lib (see below) based on
whether XHP namespace support is enabled in your HHVM version.

## The XHP-Lib Library

While the XHP syntax is part of Hack, a large part of the implementation is in a
library called [XHP-Lib](https://github.com/hhvm/xhp-lib/) that needs to be
installed via composer; add `xhp-lib` to your `composer.json`, e.g:

```
$ composer require facebook/xhp-lib
# note: make sure to use PHP, not HHVM, to execute composer
```

This includes the base classes and interfaces, and definitions of standard HTML elements.

### XHP-Lib Version

There are currently two major supported versions of XHP-Lib:

- **XHP-Lib v4:** to be used when XHP namespace support is enabled. Declares all
base classes, interfaces and elements in namespaces (e.g. standard HTML
elements are in `Facebook\XHP\HTML`). It is also more strict (e.g. disallows
most mutations after an element is rendered) and deprecates some features
(e.g. attribute transfer).
- **XHP-Lib v3:** to be used in older HHVM versions or when XHP namespace
support is disabled. Declares everything in the global namespace, with the
exception of `Facebook\XHP\ChildValidation`.

All the following guides are written with the assumption that XHP namespace
support is enabled and XHP-Lib v4 is used, but there are notes pointing out any
major differences&mdash;look for **Historical note** sections.
74 changes: 63 additions & 11 deletions guides/hack/21-XHP/07-basic-usage.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
First, make sure that you have the [XHP Library](some-basics.md#the-xhp-lib-library) installed a dependency of your project&mdash;this defines
the various core classes of XHP, and the standard HTML components.

XHP is a syntax to create actual Hack objects, called *XHP objects*. They are meant to be used as a tree, where children can either be
other XHP objects or text nodes.
other XHP objects or text nodes (or, rarely, other non-XHP objects).

## Creating a Simple XHP Object

Expand All @@ -12,16 +9,69 @@ Instead of using the `new` operator, creating XHP looks very much like XML:
$my_xhp_object = <p>Hello, world</p>;
```

`$my_xhp_object` now contains an instance of the `:p` class; the initial `:` marks it as an XHP class, but is not needed when instantiating
it. It is a real object, meaning that `is_object` will return `true` and you can call methods on it.
`$my_xhp_object` now contains an instance of the `p` class.
It is a real object, meaning that `is_object` will return `true` and you can call methods on it.

**Historical note:** Before XHP namespace support (in XHP-Lib v3), XHP classes
lived in a separate (but still global) namespace from regular classes, denoted
by a `:` prefix in the typechecker and an `xhp_` prefix at runtime. `<p>` would
therefore instantiate a class named `:p` in Hack code and `xhp_p` at runtime. It
would therefore not conflict with a global non-XHP class named `p`, but would
conflict with a class named `xhp_p`.

The following example utilizes three XHP classes: `:div`, `:strong`, `:i`. Whitespace is insignificant, so you can create a readable
The following example utilizes three XHP classes: `div`, `strong`, `i`. Whitespace is insignificant, so you can create a readable
tree structure in your code.

@@ basic-usage-examples/basic.php @@

The `var_dump` shows that a tree of objects has been created, not an HTML/XML string. An HTML string can be produced either by simply
using `echo`/`print`, or by calling `$xhp_object->toString`.
The `var_dump` shows that a tree of objects has been created, not an HTML/XML string. An HTML string can be produced by calling `await $xhp_object->toStringAsync()`.

## Namespace Syntax

When instantiating an XHP class using the `<ClassName>` syntax, `:` must be used
instead of `\` as a namespace separator (this mirrors XML's namespace syntax).
These are all equivalent ways to instantiate a `Facebook\XHP\HTML\p` object:

```
use type Facebook\XHP\HTML\p;
$xhp = <p>Hello, world</p>;
```

```
use namespace Facebook\XHP\HTML;
$xhp = <HTML:p>Hello, world</HTML:p>;
```

```
use namespace Facebook\XHP\HTML as h;
$xhp = <h:p>Hello, world</h:p>;
```

```
// from global namespace:
$xhp = <Facebook:XHP:HTML:p>Hello, world</Facebook:XHP:HTML:p>;
```

```
namespace CustomNamespace; // from any namespace:
$xhp = <:Facebook:XHP:HTML:p>Hello, world</:Facebook:XHP:HTML:p>;
```

In all other contexts, `\` must be used, for example:

```
if ($obj is HTML\p) { ... }
h\p::staticMethod();
$class_name = Facebook\XHP\HTML\p::class;
final xhp class my_element extends \Facebook\XHP\Core\element { ... }
```

**Historical note:** Before XHP namespace support (in XHP-Lib v3), `:` is
allowed as part of an XHP class name, but it is *not* a namespace separator. It
is simply translated to `__` at runtime (this is called "name mangling"). For
example, `<ui:table>` would instantiate a global class named `xhp_ui__table`. In
all other contexts, XHP classes must be referenced with the `:` prefix (e.g.
`if ($obj is :ui:table) { ... }`).

## Dynamic Content

Expand Down Expand Up @@ -51,9 +101,11 @@ class defines what attributes are available to objects of that class:
echo <input type="button" name="submit" value="OK" />;
```

Here the `:input` class has the attributes `type`, `name` and `value` as part of its class properties.
Here the `input` class has the attributes `type`, `name` and `value` as part of its class properties.

Some attributes are required, and XHP will throw an error if you use an XHP object with a required attribute but without the attribute.
Some attributes are required, and XHP will throw an error if you use an XHP object with a required attribute but without the attribute. Depending on the exact HHVM
version and configuration, this may or may not be a typechecker error, but is
always a runtime error.

## HTML Character References

Expand Down
59 changes: 40 additions & 19 deletions guides/hack/21-XHP/10-interfaces.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,40 @@
There are two important interfaces in XHP, `XHPRoot` and `XHPChild`, that you will need to use these when adding type annotations to functions.
There are two important XHP types, the `\XHPChild` interface (HHVM built-in) and
the `\Facebook\XHP\Core\node` base class (declared in XHP-Lib). You will most
commonly encounter these in functions' return type annotations.

## XHPRoot

The `XHPRoot` interface is a implemented by all XHP objects; in practice, this means:
- `:x:element` subclasses -- these are classes that re-use (and combine) existing XHP classes
- `:x:primitive` subclasses -- these define basic elements, such as `:x:frag`, and all of the basic HTML elements
- implementations of the [`XHPUnsafeRenderable`](#xhpunsaferenderable) interface described below

## XHPChild
## `\XHPChild`

XHP presents a tree structure, and this interface defines what can be valid child nodes of the tree; it includes:
- All implementations of `XHPRoot`
- strings, integers, floats
- arrays of any of the above

- all subclasses of `\Facebook\XHP\Core\node` and the advanced interfaces
described below
- strings, integers, floats
- arrays of any of the above

Despite strings, integers, floats, and arrays not being objects, both the typechecker and HHVM consider them to implement this interface,
for parameter/return types and for `is` checks.

## `\Facebook\XHP\Core\node` (`x\node`)

The `\Facebook\XHP\Core\node` base class is implemented by all XHP objects, via
one of its two subclasses:

- `\Facebook\XHP\Core\element` (`x\element`): most common; subclasses implement a
`renderAsync()` method that returns another `node`, and XHP-Lib automatically
takes care of recursively rendering nested XHP objects
- `\Facebook\XHP\Core\primitive` (`x\primitive`): for very low-level nodes that
need exact control of how the object is rendered to a string, or when the
automatic handling of nested XHP objects is insufficient; subclasses implement
a `stringifyAsync()` method that returns a `string` and must manually deal with
any children

**Historical note:** Before XHP namespace support (in XHP-Lib v3), the names of
`node`, `element` and `primitive` are `\XHPRoot`, `:x:element` and
`:x:primitive` respectively.

The `\Facebook\XHP\Core` namespace is conventionally aliased as `x` (`use Facebook\XHP\Core as x;`), so you might encounter these classes as `x\node`,
`x\element` and `x\primitive`, which also mirrors their historical names.

## Advanced Interfaces

While XHP's safe-by-default features are usually beneficial, occasionally they need to be bypassed; the most common cases are:
Expand All @@ -27,11 +45,14 @@ XHP usually gets in the way of this by:
- Escaping all variables, including your HTML code.
- Enforcing child relationships - and XHP objects can not be marked as allowing HTML string children.

The `XHPUnsafeRenderable` and `XHPAlwaysValidChild` interfaces allow bypassing these safety mechanisms.
The `\Facebook\XHP\UnsafeRenderable` and `\Facebook\XHP\XHPAlwaysValidChild` interfaces allow bypassing these safety mechanisms.

**Historical note:** Before XHP namespace support (in XHP-Lib v3), the names of
these interfaces are `\XHPUnsafeRenderable` and `\XHPAlwaysValidChild`.

## XHPUnsafeRenderable
### `\Facebook\XHP\UnsafeRenderable`

If you need to render raw HTML strings, wrap them in a class that implements this interface and provides a `toHTMLString: string` method:
If you need to render raw HTML strings, wrap them in a class that implements this interface and provides a `toHTMLStringAsync()` method:

@@ interfaces-examples/xss-security-hole.inc.php @@
@@ interfaces-examples/xss-security-hole.php @@
Expand All @@ -42,13 +63,13 @@ implementations:
@@ interfaces-examples/markdown-wrapper.inc.php @@
@@ interfaces-examples/markdown-wrapper.php @@

## XHPAlwaysValidChild
### `\Facebook\XHP\AlwaysValidChild`

XHP's child validation can be bypassed by implementing this interface. Most classes that implement this interface are also implementations of
`XHPUnsafeRenderable`, as the most common need is when a child is produced by another rendering or template system.
`UnsafeRenderable`, as the most common need is when a child is produced by another rendering or template system.

This can also be implemented by XHP objects, but this usually indicates that a child class specification should be replaced with a category. This
interface is intentionally breaking part of XHP's safety, so should be used as sparingly as possible.
This can also be implemented by XHP objects, but this usually indicates that some class in `getChildrenDeclaration()` should be replaced with a more generic interface.
`AlwaysValidChild` is intentionally breaking part of XHP's safety, so should be used as sparingly as possible.

## Example

Expand Down
33 changes: 15 additions & 18 deletions guides/hack/21-XHP/13-methods-examples/list-builder.php
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
<?hh // partial

namespace Hack\UserDocumentation\XHP\Methods;

use namespace Facebook\XHP\Core as x;
use type Facebook\XHP\HTML\{li, ul};
use type Facebook\XHP\HTML\{li, p, ul};

function xhp_object_methods_build_list(Vector<string> $names): x\node {
function build_list(vec<string> $names): x\node {
$list = <ul id="names" />;
foreach ($names as $name) {
$list->appendChild(<li>{$name}</li>);
Expand All @@ -14,27 +16,22 @@ function xhp_object_methods_build_list(Vector<string> $names): x\node {
<<__EntryPoint>>
async function xhp_object_methods_run(): Awaitable<void> {
\init_docs_autoloader();
$names = Vector {'Sara', 'Fred', 'Josh', 'Scott', 'Paul', 'David', 'Matthew'};
$names = vec['Sara', 'Fred', 'Josh', 'Scott', 'Paul', 'David', 'Matthew'];

$list = xhp_object_methods_build_list($names);
foreach ($list->getChildren() as $child) {
$xhp = <ul>{$child}</ul>;
echo await $xhp->toStringAsync()."\n";
foreach (build_list($names)->getChildren() as $child) {
$child as x\node;
echo 'Child: '.await $child->toStringAsync()."\n";
}

$list = xhp_object_methods_build_list($names);
$xhp = <ul>{$list->getFirstChild()}</ul>;
echo await $xhp->toStringAsync()."\n";
echo 'First child: '.
await (build_list($names)->getFirstChild() as x\node->toStringAsync())."\n";

$list = xhp_object_methods_build_list($names);
$xhp = <ul>{$list->getLastChild()}</ul>;
echo await $xhp->toStringAsync()."\n";
echo 'Last child: '.
await (build_list($names)->getLastChild() as x\node->toStringAsync())."\n";

foreach ($list->getAttributes() as $attr) {
$xhp = <ul><li>{(string)$attr}</li></ul>;
echo await $xhp->toStringAsync()."\n";
foreach (build_list($names)->getAttributes() as $name => $value) {
echo 'Attribute '.$name.' = '.$value as string."\n";
}

$xhp = <ul><li>{$list->getAttribute('id') as string}</li></ul>;
echo await $xhp->toStringAsync()."\n";
echo 'ID: '.build_list($names)->getAttribute('id') as string."\n";
}
Loading

0 comments on commit 16b4459

Please sign in to comment.