Merge branch '3.4' into 4.1

* 3.4:
  [Routing] fix trailing slash redirection when using RedirectableUrlMatcher
  [PropertyAccessor] fix encoding of cache keys
  [WebProfiler] Detect empty file paths in file viewer
  fixed CS
  Changes for upcoming Travis' infra migration
  Doc fix: clarify isMethodCacheable() returns true only for GET & HEAD
  [DomCrawler] exclude fields inside "template" tags
  Use XLIFF source rather than resname when there's no target
  [DoctrineBridge] catch errors while converting to db values in data collector
  [DoctrineBridge] fix case sensitivity issue in RememberMe\DoctrineTokenProvider
  [EventDispatcher] Unwrap wrapped listeners internally
  Indentation error
  [HttpFoundation] Fix trailing space for mime-type with parameters
  [HttpFoundation] Fixed absolute Request URI with default port
  properly parse backslashes in unquoted env vars
  Use intersection type when referring to ParentNodeDefinitionInterface
  [BrowserKit] fixed BC Break for HTTP_HOST header; implemented same behaviour for HTTPS server parameter
This commit is contained in:
Nicolas Grekas 2018-11-26 11:26:29 +01:00
commit 9dc9d7e0b7
23 changed files with 197 additions and 45 deletions

View File

@ -1,7 +1,6 @@
language: php
dist: trusty
sudo: false
git:
depth: 2

View File

@ -13,6 +13,7 @@ namespace Symfony\Bridge\Doctrine\DataCollector;
use Doctrine\Common\Persistence\ManagerRegistry;
use Doctrine\DBAL\Logging\DebugStack;
use Doctrine\DBAL\Types\ConversionException;
use Doctrine\DBAL\Types\Type;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
@ -146,7 +147,14 @@ class DoctrineDataCollector extends DataCollector
}
if ($type instanceof Type) {
$query['types'][$j] = $type->getBindingType();
$param = $type->convertToDatabaseValue($param, $this->registry->getConnection($connectionName)->getDatabasePlatform());
try {
$param = $type->convertToDatabaseValue($param, $this->registry->getConnection($connectionName)->getDatabasePlatform());
} catch (\TypeError $e) {
// Error thrown while processing params, query is not explainable.
$query['explainable'] = false;
} catch (ConversionException $e) {
$query['explainable'] = false;
}
}
}

View File

@ -50,7 +50,8 @@ class DoctrineTokenProvider implements TokenProviderInterface
*/
public function loadTokenBySeries($series)
{
$sql = 'SELECT class, username, value, lastUsed'
// the alias for lastUsed works around case insensitivity in PostgreSQL
$sql = 'SELECT class, username, value, lastUsed AS last_used'
.' FROM rememberme_token WHERE series=:series';
$paramValues = array('series' => $series);
$paramTypes = array('series' => \PDO::PARAM_STR);
@ -58,7 +59,7 @@ class DoctrineTokenProvider implements TokenProviderInterface
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
if ($row) {
return new PersistentToken($row['class'], $row['username'], $series, $row['value'], new \DateTime($row['lastUsed']));
return new PersistentToken($row['class'], $row['username'], $series, $row['value'], new \DateTime($row['last_used']));
}
throw new TokenNotFoundException('No token found.');

View File

@ -12,6 +12,7 @@
namespace Symfony\Bridge\Doctrine\Tests\DataCollector;
use Doctrine\DBAL\Platforms\MySqlPlatform;
use Doctrine\DBAL\Version;
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\Doctrine\DataCollector\DoctrineDataCollector;
use Symfony\Component\HttpFoundation\Request;
@ -134,7 +135,7 @@ class DoctrineDataCollectorTest extends TestCase
public function paramProvider()
{
return array(
$tests = array(
array('some value', array(), 'some value', true),
array(1, array(), 1, true),
array(true, array(), true, true),
@ -149,6 +150,13 @@ class DoctrineDataCollectorTest extends TestCase
false,
),
);
if (version_compare(Version::VERSION, '2.6', '>=')) {
$tests[] = array('this is not a date', array('date'), 'this is not a date', false);
$tests[] = array(new \stdClass(), array('date'), 'Object(stdClass)', false);
}
return $tests;
}
private function createCollector($queries)

View File

@ -134,7 +134,7 @@ class CodeExtension extends AbstractExtension
*/
public function fileExcerpt($file, $line, $srcContext = 3)
{
if (is_readable($file)) {
if (is_file($file) && is_readable($file)) {
// highlight_file could throw warnings
// see https://bugs.php.net/bug.php?id=25725
$code = @highlight_file($file, true);
@ -157,6 +157,8 @@ class CodeExtension extends AbstractExtension
return '<ol start="'.max($line - $srcContext, 1).'">'.implode("\n", $lines).'</ol>';
}
return null;
}
/**

View File

@ -54,6 +54,11 @@ a.doc:hover {
text-decoration: underline;
}
.empty {
padding: 10px;
color: #555;
}
.source {
margin-top: 41px;
}

View File

@ -7,11 +7,16 @@
{% endblock %}
{% block body %}
<div class="header">
<h1>{{ file }}{% if 0 < line %} <small>line {{ line }}</small>{% endif %}</h1>
<a class="doc" href="https://symfony.com/doc/{{ constant('Symfony\\Component\\HttpKernel\\Kernel::VERSION') }}/reference/configuration/framework.html#ide" rel="help">Open in your IDE?</a>
</div>
<div class="source">
{{ filename|file_excerpt(line, -1) }}
</div>
{% set source = filename|file_excerpt(line, -1) %}
<div class="header">
<h1>{{ file }}{% if 0 < line %} <small>line {{ line }}</small>{% endif %}</h1>
<a class="doc" href="https://symfony.com/doc/{{ constant('Symfony\\Component\\HttpKernel\\Kernel::VERSION') }}/reference/configuration/framework.html#ide" rel="help">Open in your IDE?</a>
</div>
<div class="source">
{% if source is null %}
<p class="empty">The file is not readable.</p>
{% else %}
{{ source|raw }}
{% endif %}
</div>
{% endblock %}

View File

@ -319,11 +319,17 @@ abstract class Client
++$this->redirectCount;
}
$originalUri = $uri;
$uri = $this->getAbsoluteUri($uri);
$server = array_merge($this->server, $server);
if (isset($server['HTTPS'])) {
if (!empty($server['HTTP_HOST']) && null === parse_url($originalUri, PHP_URL_HOST)) {
$uri = preg_replace('{^(https?\://)'.preg_quote($this->extractHost($uri)).'}', '${1}'.$server['HTTP_HOST'], $uri);
}
if (isset($server['HTTPS']) && null === parse_url($originalUri, PHP_URL_SCHEME)) {
$uri = preg_replace('{^'.parse_url($uri, PHP_URL_SCHEME).'}', $server['HTTPS'] ? 'https' : 'http', $uri);
}

View File

@ -741,7 +741,7 @@ class ClientTest extends TestCase
$this->assertEquals('', $client->getServerParameter('HTTP_HOST'));
$this->assertEquals('Symfony BrowserKit', $client->getServerParameter('HTTP_USER_AGENT'));
$this->assertEquals('http://www.example.com/https/www.example.com', $client->getRequest()->getUri());
$this->assertEquals('https://www.example.com/https/www.example.com', $client->getRequest()->getUri());
$server = $client->getRequest()->getServer();
@ -755,7 +755,24 @@ class ClientTest extends TestCase
$this->assertEquals('new-server-key-value', $server['NEW_SERVER_KEY']);
$this->assertArrayHasKey('HTTPS', $server);
$this->assertFalse($server['HTTPS']);
$this->assertTrue($server['HTTPS']);
}
public function testRequestWithRelativeUri()
{
$client = new TestClient();
$client->request('GET', '/', array(), array(), array(
'HTTP_HOST' => 'testhost',
'HTTPS' => true,
));
$this->assertEquals('https://testhost/', $client->getRequest()->getUri());
$client->request('GET', 'https://www.example.com/', array(), array(), array(
'HTTP_HOST' => 'testhost',
'HTTPS' => false,
));
$this->assertEquals('https://www.example.com/', $client->getRequest()->getUri());
}
public function testInternalRequest()

View File

@ -133,7 +133,7 @@ class NodeBuilder implements NodeParentInterface
/**
* Returns the parent node.
*
* @return ParentNodeDefinitionInterface|NodeDefinition The parent node
* @return NodeDefinition&ParentNodeDefinitionInterface The parent node
*/
public function end()
{

View File

@ -443,14 +443,14 @@ class Form extends Link implements \ArrayAccess
// corresponding elements are either descendants or have a matching HTML5 form attribute
$formId = Crawler::xpathLiteral($this->node->getAttribute('id'));
$fieldNodes = $xpath->query(sprintf('descendant::input[@form=%s] | descendant::button[@form=%1$s] | descendant::textarea[@form=%1$s] | descendant::select[@form=%1$s] | //form[@id=%1$s]//input[not(@form)] | //form[@id=%1$s]//button[not(@form)] | //form[@id=%1$s]//textarea[not(@form)] | //form[@id=%1$s]//select[not(@form)]', $formId));
$fieldNodes = $xpath->query(sprintf('( descendant::input[@form=%s] | descendant::button[@form=%1$s] | descendant::textarea[@form=%1$s] | descendant::select[@form=%1$s] | //form[@id=%1$s]//input[not(@form)] | //form[@id=%1$s]//button[not(@form)] | //form[@id=%1$s]//textarea[not(@form)] | //form[@id=%1$s]//select[not(@form)] )[not(ancestor::template)]', $formId));
foreach ($fieldNodes as $node) {
$this->addField($node);
}
} else {
// do the xpath query with $this->node as the context node, to only find descendant elements
// however, descendant elements with form attribute are not part of this form
$fieldNodes = $xpath->query('descendant::input[not(@form)] | descendant::button[not(@form)] | descendant::textarea[not(@form)] | descendant::select[not(@form)]', $this->node);
$fieldNodes = $xpath->query('( descendant::input[not(@form)] | descendant::button[not(@form)] | descendant::textarea[not(@form)] | descendant::select[not(@form)] )[not(ancestor::template)]', $this->node);
foreach ($fieldNodes as $node) {
$this->addField($node);
}

View File

@ -400,6 +400,10 @@ class FormTest extends TestCase
$form = $this->createForm('<form><input type="text" name="foo" value="foo" disabled="disabled" /><input type="text" name="bar" value="bar" /><input type="submit" /></form>');
$this->assertEquals(array('bar' => 'bar'), $form->getValues(), '->getValues() does not include disabled fields');
$form = $this->createForm('<form><template><input type="text" name="foo" value="foo" /></template><input type="text" name="bar" value="bar" /><input type="submit" /></form>');
$this->assertEquals(array('bar' => 'bar'), $form->getValues(), '->getValues() does not include template fields');
$this->assertFalse($form->has('foo'));
}
public function testSetValues()
@ -450,6 +454,10 @@ class FormTest extends TestCase
$form = $this->createForm('<form method="post"><input type="file" name="foo[bar]" disabled="disabled" /><input type="submit" /></form>');
$this->assertEquals(array(), $form->getFiles(), '->getFiles() does not include disabled file fields');
$form = $this->createForm('<form method="post"><template><input type="file" name="foo"/></template><input type="text" name="bar" value="bar"/><input type="submit"/></form>');
$this->assertEquals(array(), $form->getFiles(), '->getFiles() does not include template file fields');
$this->assertFalse($form->has('foo'));
}
public function testGetPhpFiles()
@ -869,7 +877,7 @@ class FormTest extends TestCase
protected function createForm($form, $method = null, $currentUri = null)
{
$dom = new \DOMDocument();
$dom->loadHTML('<html>'.$form.'</html>');
@$dom->loadHTML('<html>'.$form.'</html>');
$xPath = new \DOMXPath($dom);
$nodes = $xPath->query('//input | //button');

View File

@ -225,10 +225,11 @@ final class Dotenv
throw $this->createFormatException('Missing quote to end the value');
}
++$this->cursor;
$value = str_replace(array('\\\\', '\\"', '\r', '\n'), array('\\', '"', "\r", "\n"), $value);
$value = str_replace(array('\\"', '\r', '\n'), array('"', "\r", "\n"), $value);
$resolvedValue = $value;
$resolvedValue = $this->resolveVariables($resolvedValue);
$resolvedValue = $this->resolveCommands($resolvedValue);
$resolvedValue = str_replace('\\\\', '\\', $resolvedValue);
$v .= $resolvedValue;
} else {
$value = '';
@ -251,6 +252,7 @@ final class Dotenv
$resolvedValue = $value;
$resolvedValue = $this->resolveVariables($resolvedValue);
$resolvedValue = $this->resolveCommands($resolvedValue);
$resolvedValue = str_replace('\\\\', '\\', $resolvedValue);
if ($resolvedValue === $value && preg_match('/\s+/', $value)) {
throw $this->createFormatException('A value containing spaces must be surrounded by quotes');
@ -351,24 +353,31 @@ final class Dotenv
}
$regex = '/
(\\\\)? # escaped with a backslash?
(?<!\\\\)
(?P<backslashes>\\\\*) # escaped with a backslash?
\$
(?!\() # no opening parenthesis
(\{)? # optional brace
('.self::VARNAME_REGEX.') # var name
(\})? # optional closing brace
(?!\() # no opening parenthesis
(?P<opening_brace>\{)? # optional brace
(?P<name>'.self::VARNAME_REGEX.')? # var name
(?P<closing_brace>\})? # optional closing brace
/x';
$value = preg_replace_callback($regex, function ($matches) {
if ('\\' === $matches[1]) {
// odd number of backslashes means the $ character is escaped
if (1 === \strlen($matches['backslashes']) % 2) {
return substr($matches[0], 1);
}
if ('{' === $matches[2] && !isset($matches[4])) {
// unescaped $ not followed by variable name
if (!isset($matches['name'])) {
return $matches[0];
}
if ('{' === $matches['opening_brace'] && !isset($matches['closing_brace'])) {
throw $this->createFormatException('Unclosed braces on variable expansion');
}
$name = $matches[3];
$name = $matches['name'];
if (isset($this->values[$name])) {
$value = $this->values[$name];
} elseif (isset($_SERVER[$name]) && 0 !== strpos($name, 'HTTP_')) {
@ -379,15 +388,14 @@ final class Dotenv
$value = (string) getenv($name);
}
if (!$matches[2] && isset($matches[4])) {
if (!$matches['opening_brace'] && isset($matches['closing_brace'])) {
$value .= '}';
}
return $value;
return $matches['backslashes'].$value;
}, $value);
// unescape $
return str_replace('\\$', '$', $value);
return $value;
}
private function moveCursor($text)

View File

@ -66,6 +66,20 @@ class DotenvTest extends TestCase
$_ENV['REMOTE'] = 'remote';
$tests = array(
// backslashes
array('FOO=foo\\\\bar', array('FOO' => 'foo\\bar')),
array("FOO='foo\\\\bar'", array('FOO' => 'foo\\\\bar')),
array('FOO="foo\\\\bar"', array('FOO' => 'foo\\bar')),
// escaped backslash in front of variable
array("BAR=bar\nFOO=foo\\\\\$BAR", array('BAR' => 'bar', 'FOO' => 'foo\\bar')),
array("BAR=bar\nFOO='foo\\\\\$BAR'", array('BAR' => 'bar', 'FOO' => 'foo\\\\$BAR')),
array("BAR=bar\nFOO=\"foo\\\\\$BAR\"", array('BAR' => 'bar', 'FOO' => 'foo\\bar')),
array('FOO=foo\\\\\\$BAR', array('FOO' => 'foo\\$BAR')),
array('FOO=\'foo\\\\\\$BAR\'', array('FOO' => 'foo\\\\\\$BAR')),
array('FOO="foo\\\\\\$BAR"', array('FOO' => 'foo\\$BAR')),
// spaces
array('FOO=bar', array('FOO' => 'bar')),
array(' FOO=bar ', array('FOO' => 'bar')),

View File

@ -263,7 +263,7 @@ class TraceableEventDispatcher implements TraceableEventDispatcherInterface
foreach ($this->dispatcher->getListeners($eventName) as $listener) {
$priority = $this->getListenerPriority($eventName, $listener);
$wrappedListener = new WrappedListener($listener, null, $this->stopwatch, $this);
$wrappedListener = new WrappedListener($listener instanceof WrappedListener ? $listener->getWrappedListener() : $listener, null, $this->stopwatch, $this);
$this->wrappedListeners[$eventName][] = $wrappedListener;
$this->dispatcher->removeListener($eventName, $listener);
$this->dispatcher->addListener($eventName, $wrappedListener, $priority);

View File

@ -426,7 +426,7 @@ class TestEventSubscriberWithPriorities implements EventSubscriberInterface
return array(
'pre.foo' => array('preFoo', 10),
'post.foo' => array('postFoo'),
);
);
}
}

View File

@ -1284,7 +1284,7 @@ class Request
{
$canonicalMimeType = null;
if (false !== $pos = strpos($mimeType, ';')) {
$canonicalMimeType = substr($mimeType, 0, $pos);
$canonicalMimeType = trim(substr($mimeType, 0, $pos));
}
if (null === static::$formats) {
@ -1448,7 +1448,7 @@ class Request
*
* @see https://tools.ietf.org/html/rfc7231#section-4.2.3
*
* @return bool
* @return bool True for GET and HEAD, false otherwise
*/
public function isMethodCacheable()
{
@ -1695,10 +1695,16 @@ class Request
$this->server->remove('IIS_WasUrlRewritten');
} elseif ($this->server->has('REQUEST_URI')) {
$requestUri = $this->server->get('REQUEST_URI');
// HTTP proxy reqs setup request URI with scheme and host [and port] + the URL path, only use URL path
$schemeAndHttpHost = $this->getSchemeAndHttpHost();
if (0 === strpos($requestUri, $schemeAndHttpHost)) {
$requestUri = substr($requestUri, \strlen($schemeAndHttpHost));
$uriComponents = parse_url($requestUri);
if (isset($uriComponents['path'])) {
$requestUri = $uriComponents['path'];
}
if (isset($uriComponents['query'])) {
$requestUri .= '?'.$uriComponents['query'];
}
} elseif ($this->server->has('ORIG_PATH_INFO')) {
// IIS 5.0, PHP as CGI

View File

@ -232,6 +232,55 @@ class RequestTest extends TestCase
$this->assertEquals(80, $request->getPort());
$this->assertEquals('test.com', $request->getHttpHost());
$this->assertFalse($request->isSecure());
// Fragment should not be included in the URI
$request = Request::create('http://test.com/foo#bar');
$this->assertEquals('http://test.com/foo', $request->getUri());
}
public function testCreateWithRequestUri()
{
$request = Request::create('http://test.com:80/foo');
$request->server->set('REQUEST_URI', 'http://test.com:80/foo');
$this->assertEquals('http://test.com/foo', $request->getUri());
$this->assertEquals('/foo', $request->getPathInfo());
$this->assertEquals('test.com', $request->getHost());
$this->assertEquals('test.com', $request->getHttpHost());
$this->assertEquals(80, $request->getPort());
$this->assertFalse($request->isSecure());
$request = Request::create('http://test.com:8080/foo');
$request->server->set('REQUEST_URI', 'http://test.com:8080/foo');
$this->assertEquals('http://test.com:8080/foo', $request->getUri());
$this->assertEquals('/foo', $request->getPathInfo());
$this->assertEquals('test.com', $request->getHost());
$this->assertEquals('test.com:8080', $request->getHttpHost());
$this->assertEquals(8080, $request->getPort());
$this->assertFalse($request->isSecure());
$request = Request::create('http://test.com/foo?bar=foo', 'GET', array('bar' => 'baz'));
$request->server->set('REQUEST_URI', 'http://test.com/foo?bar=foo');
$this->assertEquals('http://test.com/foo?bar=baz', $request->getUri());
$this->assertEquals('/foo', $request->getPathInfo());
$this->assertEquals('bar=baz', $request->getQueryString());
$this->assertEquals('test.com', $request->getHost());
$this->assertEquals('test.com', $request->getHttpHost());
$this->assertEquals(80, $request->getPort());
$this->assertFalse($request->isSecure());
$request = Request::create('https://test.com:443/foo');
$request->server->set('REQUEST_URI', 'https://test.com:443/foo');
$this->assertEquals('https://test.com/foo', $request->getUri());
$this->assertEquals('/foo', $request->getPathInfo());
$this->assertEquals('test.com', $request->getHost());
$this->assertEquals('test.com', $request->getHttpHost());
$this->assertEquals(443, $request->getPort());
$this->assertTrue($request->isSecure());
// Fragment should not be included in the URI
$request = Request::create('http://test.com/foo#bar');
$request->server->set('REQUEST_URI', 'http://test.com/foo#bar');
$this->assertEquals('http://test.com/foo', $request->getUri());
}
public function testCreateCheckPrecedence()
@ -332,6 +381,9 @@ class RequestTest extends TestCase
{
$request = new Request();
$this->assertEquals('json', $request->getFormat('application/json; charset=utf-8'));
$this->assertEquals('json', $request->getFormat('application/json;charset=utf-8'));
$this->assertEquals('json', $request->getFormat('application/json ; charset=utf-8'));
$this->assertEquals('json', $request->getFormat('application/json ;charset=utf-8'));
}
/**

View File

@ -412,7 +412,7 @@ class PropertyAccessor implements PropertyAccessorInterface
*/
private function getReadAccessInfo($class, $property)
{
$key = (false !== strpos($class, '@') ? rawurlencode($class) : $class).'..'.$property;
$key = false !== strpbrk($key = $class.'..'.$property, '{}()/@:') ? rawurlencode($key) : $key;
if (isset($this->readPropertyCache[$key])) {
return $this->readPropertyCache[$key];
@ -587,7 +587,7 @@ class PropertyAccessor implements PropertyAccessorInterface
*/
private function getWriteAccessInfo(string $class, string $property, $value): array
{
$key = (false !== strpos($class, '@') ? rawurlencode($class) : $class).'..'.$property;
$key = false !== strpbrk($key = $class.'..'.$property, '{}()/@:') ? rawurlencode($key) : $key;
if (isset($this->writePropertyCache[$key])) {
return $this->writePropertyCache[$key];
@ -753,7 +753,8 @@ class PropertyAccessor implements PropertyAccessorInterface
}
if ($this->cacheItemPool) {
$item = $this->cacheItemPool->getItem(self::CACHE_PREFIX_PROPERTY_PATH.$propertyPath);
$key = false !== strpbrk($propertyPath, '{}()/@:') ? rawurlencode($propertyPath) : $propertyPath;
$item = $this->cacheItemPool->getItem(self::CACHE_PREFIX_PROPERTY_PATH.str_replace('\\', '.', $key));
if ($item->isHit()) {
return $this->propertyPathCache[$propertyPath] = $item->get();
}

View File

@ -582,6 +582,15 @@ class PropertyAccessorTest extends TestCase
$this->assertEquals('baz', $propertyAccessor->getValue($obj, 'publicGetSetter'));
}
public function testAttributeWithSpecialChars()
{
$obj = new \stdClass();
$obj->{'@foo'} = 'bar';
$propertyAccessor = new PropertyAccessor(false, false, new ArrayAdapter());
$this->assertSame('bar', $propertyAccessor->getValue($obj, '@foo'));
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\InvalidArgumentException
* @expectedExceptionMessage Expected argument of type "Countable", "string" given

View File

@ -91,7 +91,7 @@ class XliffFileLoader implements LoaderInterface
$source = isset($attributes['resname']) && $attributes['resname'] ? $attributes['resname'] : $translation->source;
// If the xlf file has another encoding specified, try to convert it because
// simple_xml will always return utf-8 encoded values
$target = $this->utf8ToCharset((string) (isset($translation->target) ? $translation->target : $source), $encoding);
$target = $this->utf8ToCharset((string) (isset($translation->target) ? $translation->target : $translation->source), $encoding);
$catalogue->set((string) $source, $target, $domain);

View File

@ -66,7 +66,7 @@ class XliffFileLoaderTest extends TestCase
$loader = new XliffFileLoader();
$catalogue = $loader->load(__DIR__.'/../fixtures/resname.xlf', 'en', 'domain1');
$this->assertEquals(array('foo' => 'bar', 'bar' => 'baz', 'baz' => 'foo'), $catalogue->all('domain1'));
$this->assertEquals(array('foo' => 'bar', 'bar' => 'baz', 'baz' => 'foo', 'qux' => 'qux source'), $catalogue->all('domain1'));
}
public function testIncompleteResource()

View File

@ -14,6 +14,9 @@
<source>baz</source>
<target>foo</target>
</trans-unit>
<trans-unit id="4" resname="qux">
<source>qux source</source>
</trans-unit>
</body>
</file>
</xliff>