Merge branch '3.4' into 4.0
* 3.4: [HttpKernel] fix PHP 5.4 compat Fix surrogate not using original request [Finder] Update RealIteratorTestCase [Routing] remove unneeded dev dep on doctrine/common [minor] SCA [Validator] Remove BOM in some xlf files Fix #27378: Error when rendering a DateIntervalType form with exactly 0 weeks [HttpKernel] fix session tracking in surrogate master requests
This commit is contained in:
commit
b85f70e3bc
@ -204,9 +204,7 @@ class DoctrineExtensionTest extends TestCase
|
||||
$definition = $container->getDefinition('doctrine.orm.default_metadata_cache');
|
||||
$defCalls = $definition->getMethodCalls();
|
||||
$expectedCalls[] = 'setNamespace';
|
||||
$actualCalls = array_map(function ($call) {
|
||||
return $call[0];
|
||||
}, $defCalls);
|
||||
$actualCalls = array_column($defCalls, 0);
|
||||
|
||||
$this->assertFalse($definition->isPublic());
|
||||
$this->assertEquals("%$class%", $definition->getClass());
|
||||
|
@ -311,7 +311,7 @@ class TextDescriptor extends Descriptor
|
||||
$tableRows[] = array('Autoconfigured', $definition->isAutoconfigured() ? 'yes' : 'no');
|
||||
|
||||
if ($definition->getFile()) {
|
||||
$tableRows[] = array('Required File', $definition->getFile() ? $definition->getFile() : '-');
|
||||
$tableRows[] = array('Required File', $definition->getFile() ?: '-');
|
||||
}
|
||||
|
||||
if ($factory = $definition->getFactory()) {
|
||||
|
@ -86,7 +86,7 @@ class UserPasswordEncoderCommandTest extends WebTestCase
|
||||
$this->assertContains('Password encoding succeeded', $output);
|
||||
|
||||
$encoder = new Argon2iPasswordEncoder();
|
||||
preg_match('# Encoded password\s+(\$argon2id?\$[\w\d,=\$+\/]+={0,2})\s+#', $output, $matches);
|
||||
preg_match('# Encoded password\s+(\$argon2id?\$[\w,=\$+\/]+={0,2})\s+#', $output, $matches);
|
||||
$hash = $matches[1];
|
||||
$this->assertTrue($encoder->isPasswordValid($hash, 'password', null));
|
||||
}
|
||||
|
@ -126,7 +126,7 @@ class IntegrationTest extends TestCase
|
||||
public function testYamlContainerCompiles($directory, $actualServiceId, $expectedServiceId, ContainerBuilder $mainContainer = null)
|
||||
{
|
||||
// allow a container to be passed in, which might have autoconfigure settings
|
||||
$container = $mainContainer ? $mainContainer : new ContainerBuilder();
|
||||
$container = $mainContainer ?: new ContainerBuilder();
|
||||
$container->setResourceTracking(false);
|
||||
$loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../Fixtures/yaml/integration/'.$directory));
|
||||
$loader->load('main.yml');
|
||||
|
@ -60,11 +60,20 @@ abstract class RealIteratorTestCase extends IteratorTestCase
|
||||
|
||||
public static function tearDownAfterClass()
|
||||
{
|
||||
foreach (array_reverse(self::$files) as $file) {
|
||||
if (DIRECTORY_SEPARATOR === $file[strlen($file) - 1]) {
|
||||
@rmdir($file);
|
||||
$paths = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator(self::$tmpDir, \RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
\RecursiveIteratorIterator::CHILD_FIRST
|
||||
);
|
||||
|
||||
foreach ($paths as $path) {
|
||||
if ($path->isDir()) {
|
||||
if ($path->isLink()) {
|
||||
@unlink($path);
|
||||
} else {
|
||||
@rmdir($path);
|
||||
}
|
||||
} else {
|
||||
@unlink($file);
|
||||
@unlink($path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -89,7 +89,7 @@ class DateIntervalToArrayTransformer implements DataTransformerInterface
|
||||
$result[$field] = $dateInterval->format('%'.($this->pad ? strtoupper($char) : $char));
|
||||
}
|
||||
if (in_array('weeks', $this->fields, true)) {
|
||||
$result['weeks'] = 0;
|
||||
$result['weeks'] = '0';
|
||||
if (isset($result['days']) && (int) $result['days'] >= 7) {
|
||||
$result['weeks'] = (string) floor($result['days'] / 7);
|
||||
$result['days'] = (string) ($result['days'] % 7);
|
||||
|
@ -1,4 +1,4 @@
|
||||
<?xml version="1.0"?>
|
||||
<?xml version="1.0"?>
|
||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<file source-language="en" datatype="plaintext" original="file.ext">
|
||||
<body>
|
||||
@ -16,4 +16,4 @@
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
</xliff>
|
||||
|
@ -1,4 +1,4 @@
|
||||
<?xml version="1.0"?>
|
||||
<?xml version="1.0"?>
|
||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<file source-language="en" datatype="plaintext" original="file.ext">
|
||||
<body>
|
||||
@ -16,4 +16,4 @@
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
</xliff>
|
||||
|
@ -1,4 +1,4 @@
|
||||
<?xml version="1.0"?>
|
||||
<?xml version="1.0"?>
|
||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<file source-language="en" datatype="plaintext" original="file.ext">
|
||||
<body>
|
||||
|
@ -1,4 +1,4 @@
|
||||
<?xml version="1.0"?>
|
||||
<?xml version="1.0"?>
|
||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<file source-language="en" datatype="plaintext" original="file.ext">
|
||||
<body>
|
||||
@ -16,4 +16,4 @@
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
</xliff>
|
||||
|
@ -90,6 +90,21 @@ class DateIntervalToArrayTransformerTest extends DateIntervalTestCase
|
||||
$this->assertSame($output, $input);
|
||||
}
|
||||
|
||||
public function testTransformWithZeroWeek()
|
||||
{
|
||||
$transformer = new DateIntervalToArrayTransformer(array('weeks', 'minutes', 'seconds'));
|
||||
$input = new \DateInterval('P1Y2M0WT4H5M6S');
|
||||
$output = array(
|
||||
'weeks' => '0',
|
||||
'minutes' => '5',
|
||||
'seconds' => '6',
|
||||
);
|
||||
$input = $transformer->transform($input);
|
||||
ksort($input);
|
||||
ksort($output);
|
||||
$this->assertSame($output, $input);
|
||||
}
|
||||
|
||||
public function testTransformDaysToWeeks()
|
||||
{
|
||||
$transformer = new DateIntervalToArrayTransformer(array('weeks', 'minutes', 'seconds'));
|
||||
|
@ -29,7 +29,7 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
|
||||
private $flashName;
|
||||
private $attributeName;
|
||||
private $data = array();
|
||||
private $hasBeenStarted;
|
||||
private $usageIndex = 0;
|
||||
|
||||
/**
|
||||
* @param SessionStorageInterface $storage A SessionStorageInterface instance
|
||||
@ -54,6 +54,8 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
|
||||
*/
|
||||
public function start()
|
||||
{
|
||||
++$this->usageIndex;
|
||||
|
||||
return $this->storage->start();
|
||||
}
|
||||
|
||||
@ -142,13 +144,13 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
* @return int
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function hasBeenStarted()
|
||||
public function getUsageIndex()
|
||||
{
|
||||
return $this->hasBeenStarted;
|
||||
return $this->usageIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -158,6 +160,7 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
|
||||
*/
|
||||
public function isEmpty()
|
||||
{
|
||||
++$this->usageIndex;
|
||||
foreach ($this->data as &$data) {
|
||||
if (!empty($data)) {
|
||||
return false;
|
||||
@ -182,6 +185,8 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
|
||||
*/
|
||||
public function migrate($destroy = false, $lifetime = null)
|
||||
{
|
||||
++$this->usageIndex;
|
||||
|
||||
return $this->storage->regenerate($destroy, $lifetime);
|
||||
}
|
||||
|
||||
@ -190,6 +195,8 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
|
||||
*/
|
||||
public function save()
|
||||
{
|
||||
++$this->usageIndex;
|
||||
|
||||
$this->storage->save();
|
||||
}
|
||||
|
||||
@ -230,6 +237,8 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
|
||||
*/
|
||||
public function getMetadataBag()
|
||||
{
|
||||
++$this->usageIndex;
|
||||
|
||||
return $this->storage->getMetadataBag();
|
||||
}
|
||||
|
||||
@ -238,7 +247,7 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
|
||||
*/
|
||||
public function registerBag(SessionBagInterface $bag)
|
||||
{
|
||||
$this->storage->registerBag(new SessionBagProxy($bag, $this->data, $this->hasBeenStarted));
|
||||
$this->storage->registerBag(new SessionBagProxy($bag, $this->data, $this->usageIndex));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -20,13 +20,13 @@ final class SessionBagProxy implements SessionBagInterface
|
||||
{
|
||||
private $bag;
|
||||
private $data;
|
||||
private $hasBeenStarted;
|
||||
private $usageIndex;
|
||||
|
||||
public function __construct(SessionBagInterface $bag, array &$data, &$hasBeenStarted)
|
||||
public function __construct(SessionBagInterface $bag, array &$data, &$usageIndex)
|
||||
{
|
||||
$this->bag = $bag;
|
||||
$this->data = &$data;
|
||||
$this->hasBeenStarted = &$hasBeenStarted;
|
||||
$this->usageIndex = &$usageIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -34,6 +34,8 @@ final class SessionBagProxy implements SessionBagInterface
|
||||
*/
|
||||
public function getBag()
|
||||
{
|
||||
++$this->usageIndex;
|
||||
|
||||
return $this->bag;
|
||||
}
|
||||
|
||||
@ -42,6 +44,8 @@ final class SessionBagProxy implements SessionBagInterface
|
||||
*/
|
||||
public function isEmpty()
|
||||
{
|
||||
++$this->usageIndex;
|
||||
|
||||
return empty($this->data[$this->bag->getStorageKey()]);
|
||||
}
|
||||
|
||||
@ -58,7 +62,7 @@ final class SessionBagProxy implements SessionBagInterface
|
||||
*/
|
||||
public function initialize(array &$array)
|
||||
{
|
||||
$this->hasBeenStarted = true;
|
||||
++$this->usageIndex;
|
||||
$this->data[$this->bag->getStorageKey()] = &$array;
|
||||
|
||||
$this->bag->initialize($array);
|
||||
@ -77,6 +81,8 @@ final class SessionBagProxy implements SessionBagInterface
|
||||
*/
|
||||
public function clear()
|
||||
{
|
||||
++$this->usageIndex;
|
||||
|
||||
return $this->bag->clear();
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ namespace Symfony\Component\HttpKernel\EventListener;
|
||||
use Symfony\Component\HttpFoundation\Session\Session;
|
||||
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
|
||||
use Symfony\Component\HttpKernel\Event\FinishRequestEvent;
|
||||
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
@ -25,6 +26,8 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
*/
|
||||
abstract class AbstractSessionListener implements EventSubscriberInterface
|
||||
{
|
||||
private $sessionUsageStack = array();
|
||||
|
||||
public function onKernelRequest(GetResponseEvent $event)
|
||||
{
|
||||
if (!$event->isMasterRequest()) {
|
||||
@ -33,6 +36,7 @@ abstract class AbstractSessionListener implements EventSubscriberInterface
|
||||
|
||||
$request = $event->getRequest();
|
||||
$session = $this->getSession();
|
||||
$this->sessionUsageStack[] = $session instanceof Session ? $session->getUsageIndex() : null;
|
||||
if (null === $session || $request->hasSession()) {
|
||||
return;
|
||||
}
|
||||
@ -50,7 +54,7 @@ abstract class AbstractSessionListener implements EventSubscriberInterface
|
||||
return;
|
||||
}
|
||||
|
||||
if ($session->isStarted() || ($session instanceof Session && $session->hasBeenStarted())) {
|
||||
if ($session instanceof Session ? $session->getUsageIndex() !== end($this->sessionUsageStack) : $session->isStarted()) {
|
||||
$event->getResponse()
|
||||
->setPrivate()
|
||||
->setMaxAge(0)
|
||||
@ -58,12 +62,23 @@ abstract class AbstractSessionListener implements EventSubscriberInterface
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function onFinishRequest(FinishRequestEvent $event)
|
||||
{
|
||||
if ($event->isMasterRequest()) {
|
||||
array_pop($this->sessionUsageStack);
|
||||
}
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents()
|
||||
{
|
||||
return array(
|
||||
KernelEvents::REQUEST => array('onKernelRequest', 128),
|
||||
// low priority to come after regular response listeners, same as SaveSessionListener
|
||||
KernelEvents::RESPONSE => array('onKernelResponse', -1000),
|
||||
KernelEvents::FINISH_REQUEST => array('onFinishRequest'),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -165,7 +165,11 @@ class HttpCache implements HttpKernelInterface, TerminableInterface
|
||||
// FIXME: catch exceptions and implement a 500 error page here? -> in Varnish, there is a built-in error page mechanism
|
||||
if (HttpKernelInterface::MASTER_REQUEST === $type) {
|
||||
$this->traces = array();
|
||||
$this->request = $request;
|
||||
// Keep a clone of the original request for surrogates so they can access it.
|
||||
// We must clone here to get a separate instance because the application will modify the request during
|
||||
// the application flow (we know it always does because we do ourselves by setting REMOTE_ADDR to 127.0.0.1
|
||||
// and adding the X-Forwarded-For header, see HttpCache::forward()).
|
||||
$this->request = clone $request;
|
||||
if (null !== $this->surrogate) {
|
||||
$this->surrogateCacheStrategy = $this->surrogate->createCacheStrategy();
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\Session\Session;
|
||||
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
|
||||
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
|
||||
use Symfony\Component\HttpKernel\Event\FinishRequestEvent;
|
||||
use Symfony\Component\HttpKernel\EventListener\AbstractSessionListener;
|
||||
use Symfony\Component\HttpKernel\EventListener\SessionListener;
|
||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||
@ -58,8 +59,7 @@ class SessionListenerTest extends TestCase
|
||||
public function testResponseIsPrivate()
|
||||
{
|
||||
$session = $this->getMockBuilder(Session::class)->disableOriginalConstructor()->getMock();
|
||||
$session->expects($this->once())->method('isStarted')->willReturn(false);
|
||||
$session->expects($this->once())->method('hasBeenStarted')->willReturn(true);
|
||||
$session->expects($this->exactly(2))->method('getUsageIndex')->will($this->onConsecutiveCalls(0, 1));
|
||||
|
||||
$container = new Container();
|
||||
$container->set('session', $session);
|
||||
@ -76,4 +76,38 @@ class SessionListenerTest extends TestCase
|
||||
$this->assertTrue($response->headers->hasCacheControlDirective('must-revalidate'));
|
||||
$this->assertSame('0', $response->headers->getCacheControlDirective('max-age'));
|
||||
}
|
||||
|
||||
public function testSurrogateMasterRequestIsPublic()
|
||||
{
|
||||
$session = $this->getMockBuilder(Session::class)->disableOriginalConstructor()->getMock();
|
||||
$session->expects($this->exactly(4))->method('getUsageIndex')->will($this->onConsecutiveCalls(0, 1, 1, 1));
|
||||
|
||||
$container = new Container();
|
||||
$container->set('session', $session);
|
||||
|
||||
$listener = new SessionListener($container);
|
||||
$kernel = $this->getMockBuilder(HttpKernelInterface::class)->disableOriginalConstructor()->getMock();
|
||||
|
||||
$request = new Request();
|
||||
$response = new Response();
|
||||
$response->setCache(array('public' => true, 'max_age' => '30'));
|
||||
$listener->onKernelRequest(new GetResponseEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST));
|
||||
$this->assertTrue($request->hasSession());
|
||||
|
||||
$subRequest = clone $request;
|
||||
$this->assertSame($request->getSession(), $subRequest->getSession());
|
||||
$listener->onKernelRequest(new GetResponseEvent($kernel, $subRequest, HttpKernelInterface::MASTER_REQUEST));
|
||||
$listener->onKernelResponse(new FilterResponseEvent($kernel, $subRequest, HttpKernelInterface::MASTER_REQUEST, $response));
|
||||
$listener->onFinishRequest(new FinishRequestEvent($kernel, $subRequest, HttpKernelInterface::MASTER_REQUEST));
|
||||
|
||||
$this->assertFalse($response->headers->hasCacheControlDirective('private'));
|
||||
$this->assertFalse($response->headers->hasCacheControlDirective('must-revalidate'));
|
||||
$this->assertSame('30', $response->headers->getCacheControlDirective('max-age'));
|
||||
|
||||
$listener->onKernelResponse(new FilterResponseEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST, $response));
|
||||
|
||||
$this->assertTrue($response->headers->hasCacheControlDirective('private'));
|
||||
$this->assertTrue($response->headers->hasCacheControlDirective('must-revalidate'));
|
||||
$this->assertSame('0', $response->headers->getCacheControlDirective('max-age'));
|
||||
}
|
||||
}
|
||||
|
@ -11,9 +11,11 @@
|
||||
|
||||
namespace Symfony\Component\HttpKernel\Tests\HttpCache;
|
||||
|
||||
use Symfony\Component\HttpKernel\HttpCache\Esi;
|
||||
use Symfony\Component\HttpKernel\HttpCache\HttpCache;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\HttpCache\Store;
|
||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||
|
||||
/**
|
||||
@ -1467,6 +1469,42 @@ class HttpCacheTest extends HttpCacheTestCase
|
||||
$this->assertHttpKernelIsNotCalled();
|
||||
$this->assertSame('get', $this->response->getContent());
|
||||
}
|
||||
|
||||
public function testUsesOriginalRequestForSurrogate()
|
||||
{
|
||||
$kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\HttpKernelInterface')->getMock();
|
||||
$store = $this->getMockBuilder('Symfony\Component\HttpKernel\HttpCache\StoreInterface')->getMock();
|
||||
|
||||
$kernel
|
||||
->expects($this->exactly(2))
|
||||
->method('handle')
|
||||
->willReturnCallback(function (Request $request) {
|
||||
$this->assertSame('127.0.0.1', $request->server->get('REMOTE_ADDR'));
|
||||
|
||||
return new Response();
|
||||
});
|
||||
|
||||
$cache = new HttpCache($kernel,
|
||||
$store,
|
||||
new Esi()
|
||||
);
|
||||
|
||||
$request = Request::create('/');
|
||||
$request->server->set('REMOTE_ADDR', '10.0.0.1');
|
||||
|
||||
// Main request
|
||||
$cache->handle($request, HttpKernelInterface::MASTER_REQUEST);
|
||||
|
||||
// Main request was now modified by HttpCache
|
||||
// The surrogate will ask for the request using $this->cache->getRequest()
|
||||
// which MUST return the original request so the surrogate
|
||||
// can actually behave like a reverse proxy like e.g. Varnish would.
|
||||
$this->assertSame('10.0.0.1', $cache->getRequest()->getClientIp());
|
||||
$this->assertSame('10.0.0.1', $cache->getRequest()->server->get('REMOTE_ADDR'));
|
||||
|
||||
// Surrogate request
|
||||
$cache->handle($request, HttpKernelInterface::SUB_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
class TestKernel implements HttpKernelInterface
|
||||
|
@ -18,7 +18,7 @@
|
||||
"require": {
|
||||
"php": "^7.1.3",
|
||||
"symfony/event-dispatcher": "~3.4|~4.0",
|
||||
"symfony/http-foundation": "~3.4.4|~4.0.4",
|
||||
"symfony/http-foundation": "~3.4.12|~4.0.12|^4.1.1",
|
||||
"symfony/debug": "~3.4|~4.0",
|
||||
"symfony/polyfill-ctype": "~1.8",
|
||||
"psr/log": "~1.0"
|
||||
|
@ -157,9 +157,7 @@ class LanguageDataGenerator extends AbstractDataGenerator
|
||||
return array(
|
||||
'Version' => $rootBundle['Version'],
|
||||
'Languages' => $this->languageCodes,
|
||||
'Aliases' => array_map(function (\ResourceBundle $bundle) {
|
||||
return $bundle['replacement'];
|
||||
}, iterator_to_array($metadataBundle['alias']['language'])),
|
||||
'Aliases' => array_column(iterator_to_array($metadataBundle['alias']['language']), 'replacement'),
|
||||
'Alpha2ToAlpha3' => $this->generateAlpha2ToAlpha3Mapping($metadataBundle),
|
||||
);
|
||||
}
|
||||
|
@ -145,7 +145,7 @@ class Connection extends AbstractConnection
|
||||
private function disconnect()
|
||||
{
|
||||
if ($this->connection && is_resource($this->connection)) {
|
||||
ldap_close($this->connection);
|
||||
ldap_unbind($this->connection);
|
||||
}
|
||||
|
||||
$this->connection = null;
|
||||
|
@ -25,7 +25,6 @@
|
||||
"symfony/expression-language": "~3.4|~4.0",
|
||||
"symfony/dependency-injection": "~3.4|~4.0",
|
||||
"doctrine/annotations": "~1.0",
|
||||
"doctrine/common": "~2.2",
|
||||
"psr/log": "~1.0"
|
||||
},
|
||||
"conflict": {
|
||||
|
@ -1,4 +1,4 @@
|
||||
<?xml version="1.0"?>
|
||||
<?xml version="1.0"?>
|
||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<file source-language="en" datatype="plaintext" original="file.ext">
|
||||
<body>
|
||||
|
@ -1,4 +1,4 @@
|
||||
<?xml version="1.0"?>
|
||||
<?xml version="1.0"?>
|
||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<file source-language="en" datatype="plaintext" original="file.ext">
|
||||
<body>
|
||||
|
@ -1,4 +1,4 @@
|
||||
<?xml version="1.0"?>
|
||||
<?xml version="1.0"?>
|
||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<file source-language="en" datatype="plaintext" original="file.ext">
|
||||
<body>
|
||||
|
@ -1,4 +1,4 @@
|
||||
<?xml version="1.0"?>
|
||||
<?xml version="1.0"?>
|
||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<file source-language="en" datatype="plaintext" original="file.ext">
|
||||
<body>
|
||||
|
Reference in New Issue
Block a user