2017-11-15 12:02:40 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace MediaWiki\Tests\Storage;
|
|
|
|
|
|
2018-03-06 18:46:13 +00:00
|
|
|
use InvalidArgumentException;
|
|
|
|
|
use LogicException;
|
|
|
|
|
use MediaWiki\Storage\IncompleteRevisionException;
|
2017-11-15 12:02:40 +00:00
|
|
|
use MediaWiki\Storage\SlotRecord;
|
2018-03-06 18:46:13 +00:00
|
|
|
use MediaWiki\Storage\SuppressedDataException;
|
2017-11-15 12:02:40 +00:00
|
|
|
use MediaWikiTestCase;
|
|
|
|
|
use WikitextContent;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers \MediaWiki\Storage\SlotRecord
|
|
|
|
|
*/
|
|
|
|
|
class SlotRecordTest extends MediaWikiTestCase {
|
|
|
|
|
|
2018-03-06 18:46:13 +00:00
|
|
|
private function makeRow( $data = [] ) {
|
|
|
|
|
$data = $data + [
|
|
|
|
|
'slot_id' => 1234,
|
|
|
|
|
'slot_content_id' => 33,
|
|
|
|
|
'content_size' => '5',
|
|
|
|
|
'content_sha1' => 'someHash',
|
|
|
|
|
'content_address' => 'tt:456',
|
|
|
|
|
'model_name' => CONTENT_MODEL_WIKITEXT,
|
|
|
|
|
'format_name' => CONTENT_FORMAT_WIKITEXT,
|
|
|
|
|
'slot_revision_id' => '2',
|
2018-03-06 14:42:43 +00:00
|
|
|
'slot_origin' => '1',
|
2017-11-15 12:02:40 +00:00
|
|
|
'role_name' => 'myRole',
|
|
|
|
|
];
|
2018-03-06 18:46:13 +00:00
|
|
|
return (object)$data;
|
|
|
|
|
}
|
2017-11-15 12:02:40 +00:00
|
|
|
|
2018-03-06 18:46:13 +00:00
|
|
|
public function testCompleteConstruction() {
|
|
|
|
|
$row = $this->makeRow();
|
|
|
|
|
$record = new SlotRecord( $row, new WikitextContent( 'A' ) );
|
2017-11-15 12:02:40 +00:00
|
|
|
|
2018-03-06 18:46:13 +00:00
|
|
|
$this->assertTrue( $record->hasAddress() );
|
|
|
|
|
$this->assertTrue( $record->hasRevision() );
|
|
|
|
|
$this->assertTrue( $record->isInherited() );
|
2017-11-15 12:02:40 +00:00
|
|
|
$this->assertSame( 'A', $record->getContent()->getNativeData() );
|
2018-03-06 18:46:13 +00:00
|
|
|
$this->assertSame( 5, $record->getSize() );
|
2017-11-15 12:02:40 +00:00
|
|
|
$this->assertSame( 'someHash', $record->getSha1() );
|
2018-03-06 18:46:13 +00:00
|
|
|
$this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() );
|
2017-11-15 12:02:40 +00:00
|
|
|
$this->assertSame( 2, $record->getRevision() );
|
2018-03-06 14:42:43 +00:00
|
|
|
$this->assertSame( 1, $record->getOrigin() );
|
2017-11-15 12:02:40 +00:00
|
|
|
$this->assertSame( 'tt:456', $record->getAddress() );
|
2018-03-06 18:46:13 +00:00
|
|
|
$this->assertSame( 33, $record->getContentId() );
|
|
|
|
|
$this->assertSame( CONTENT_FORMAT_WIKITEXT, $record->getFormat() );
|
2017-11-15 12:02:40 +00:00
|
|
|
$this->assertSame( 'myRole', $record->getRole() );
|
2018-03-06 18:46:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testConstructionDeferred() {
|
|
|
|
|
$row = $this->makeRow( [
|
|
|
|
|
'content_size' => null, // to be computed
|
|
|
|
|
'content_sha1' => null, // to be computed
|
|
|
|
|
'format_name' => function () {
|
|
|
|
|
return CONTENT_FORMAT_WIKITEXT;
|
|
|
|
|
},
|
2018-03-06 14:42:43 +00:00
|
|
|
'slot_revision_id' => '2',
|
|
|
|
|
'slot_origin' => '2',
|
2018-03-06 18:46:13 +00:00
|
|
|
] );
|
|
|
|
|
|
|
|
|
|
$content = function () {
|
|
|
|
|
return new WikitextContent( 'A' );
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
$record = new SlotRecord( $row, $content );
|
|
|
|
|
|
2017-11-15 12:02:40 +00:00
|
|
|
$this->assertTrue( $record->hasAddress() );
|
|
|
|
|
$this->assertTrue( $record->hasRevision() );
|
2018-03-06 18:46:13 +00:00
|
|
|
$this->assertFalse( $record->isInherited() );
|
|
|
|
|
$this->assertSame( 'A', $record->getContent()->getNativeData() );
|
|
|
|
|
$this->assertSame( 1, $record->getSize() );
|
|
|
|
|
$this->assertNotNull( $record->getSha1() );
|
|
|
|
|
$this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() );
|
|
|
|
|
$this->assertSame( 2, $record->getRevision() );
|
2018-03-06 14:42:43 +00:00
|
|
|
$this->assertSame( 2, $record->getRevision() );
|
2018-03-06 18:46:13 +00:00
|
|
|
$this->assertSame( 'tt:456', $record->getAddress() );
|
|
|
|
|
$this->assertSame( 33, $record->getContentId() );
|
|
|
|
|
$this->assertSame( CONTENT_FORMAT_WIKITEXT, $record->getFormat() );
|
|
|
|
|
$this->assertSame( 'myRole', $record->getRole() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testNewUnsaved() {
|
|
|
|
|
$record = SlotRecord::newUnsaved( 'myRole', new WikitextContent( 'A' ) );
|
|
|
|
|
|
|
|
|
|
$this->assertFalse( $record->hasAddress() );
|
|
|
|
|
$this->assertFalse( $record->hasRevision() );
|
|
|
|
|
$this->assertFalse( $record->isInherited() );
|
|
|
|
|
$this->assertSame( 'A', $record->getContent()->getNativeData() );
|
|
|
|
|
$this->assertSame( 1, $record->getSize() );
|
|
|
|
|
$this->assertNotNull( $record->getSha1() );
|
|
|
|
|
$this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() );
|
|
|
|
|
$this->assertSame( 'myRole', $record->getRole() );
|
2017-11-15 12:02:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function provideInvalidConstruction() {
|
|
|
|
|
yield 'both null' => [ null, null ];
|
|
|
|
|
yield 'null row' => [ null, new WikitextContent( 'A' ) ];
|
2018-03-06 18:46:13 +00:00
|
|
|
yield 'array row' => [ [], new WikitextContent( 'A' ) ];
|
|
|
|
|
yield 'empty row' => [ (object)[], new WikitextContent( 'A' ) ];
|
2017-11-15 12:02:40 +00:00
|
|
|
yield 'null content' => [ (object)[], null ];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider provideInvalidConstruction
|
|
|
|
|
*/
|
|
|
|
|
public function testInvalidConstruction( $row, $content ) {
|
2018-03-06 18:46:13 +00:00
|
|
|
$this->setExpectedException( InvalidArgumentException::class );
|
2017-11-15 12:02:40 +00:00
|
|
|
new SlotRecord( $row, $content );
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-06 18:46:13 +00:00
|
|
|
public function testGetContentId_fails() {
|
|
|
|
|
$record = SlotRecord::newUnsaved( 'main', new WikitextContent( 'A' ) );
|
|
|
|
|
$this->setExpectedException( IncompleteRevisionException::class );
|
|
|
|
|
|
|
|
|
|
$record->getContentId();
|
2017-11-15 12:02:40 +00:00
|
|
|
}
|
|
|
|
|
|
2018-03-06 18:46:13 +00:00
|
|
|
public function testGetAddress_fails() {
|
|
|
|
|
$record = SlotRecord::newUnsaved( 'main', new WikitextContent( 'A' ) );
|
|
|
|
|
$this->setExpectedException( IncompleteRevisionException::class );
|
|
|
|
|
|
|
|
|
|
$record->getAddress();
|
2017-11-15 12:02:40 +00:00
|
|
|
}
|
|
|
|
|
|
2018-03-06 14:42:43 +00:00
|
|
|
public function provideIncomplete() {
|
|
|
|
|
$unsaved = SlotRecord::newUnsaved( 'main', new WikitextContent( 'A' ) );
|
|
|
|
|
yield 'unsaved' => [ $unsaved ];
|
|
|
|
|
|
|
|
|
|
$parent = new SlotRecord( $this->makeRow(), new WikitextContent( 'A' ) );
|
|
|
|
|
$inherited = SlotRecord::newInherited( $parent );
|
|
|
|
|
yield 'inherited' => [ $inherited ];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider provideIncomplete
|
|
|
|
|
*/
|
|
|
|
|
public function testGetRevision_fails( SlotRecord $record ) {
|
2018-03-06 18:46:13 +00:00
|
|
|
$record = SlotRecord::newUnsaved( 'main', new WikitextContent( 'A' ) );
|
|
|
|
|
$this->setExpectedException( IncompleteRevisionException::class );
|
|
|
|
|
|
|
|
|
|
$record->getRevision();
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-06 14:42:43 +00:00
|
|
|
/**
|
|
|
|
|
* @dataProvider provideIncomplete
|
|
|
|
|
*/
|
|
|
|
|
public function testGetOrigin_fails( SlotRecord $record ) {
|
|
|
|
|
$record = SlotRecord::newUnsaved( 'main', new WikitextContent( 'A' ) );
|
|
|
|
|
$this->setExpectedException( IncompleteRevisionException::class );
|
|
|
|
|
|
|
|
|
|
$record->getOrigin();
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-06 18:46:13 +00:00
|
|
|
public function provideHashStability() {
|
|
|
|
|
yield [ '', 'phoiac9h4m842xq45sp7s6u21eteeq1' ];
|
|
|
|
|
yield [ 'Lorem ipsum', 'hcr5u40uxr81d3nx89nvwzclfz6r9c5' ];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider provideHashStability
|
|
|
|
|
*/
|
|
|
|
|
public function testHashStability( $text, $hash ) {
|
|
|
|
|
// Changing the output of the hash function will break things horribly!
|
|
|
|
|
|
|
|
|
|
$this->assertSame( $hash, SlotRecord::base36Sha1( $text ) );
|
|
|
|
|
|
|
|
|
|
$record = SlotRecord::newUnsaved( 'main', new WikitextContent( $text ) );
|
|
|
|
|
$this->assertSame( $hash, $record->getSha1() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testNewWithSuppressedContent() {
|
|
|
|
|
$input = new SlotRecord( $this->makeRow(), new WikitextContent( 'A' ) );
|
|
|
|
|
$output = SlotRecord::newWithSuppressedContent( $input );
|
|
|
|
|
|
|
|
|
|
$this->setExpectedException( SuppressedDataException::class );
|
|
|
|
|
$output->getContent();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testNewInherited() {
|
2018-03-06 14:42:43 +00:00
|
|
|
$row = $this->makeRow( [ 'slot_revision_id' => 7, 'slot_origin' => 7 ] );
|
2018-03-06 18:46:13 +00:00
|
|
|
$parent = new SlotRecord( $row, new WikitextContent( 'A' ) );
|
|
|
|
|
|
|
|
|
|
// This would happen while doing an edit, before saving revision meta-data.
|
|
|
|
|
$inherited = SlotRecord::newInherited( $parent );
|
|
|
|
|
|
|
|
|
|
$this->assertSame( $parent->getContentId(), $inherited->getContentId() );
|
|
|
|
|
$this->assertSame( $parent->getAddress(), $inherited->getAddress() );
|
|
|
|
|
$this->assertSame( $parent->getContent(), $inherited->getContent() );
|
|
|
|
|
$this->assertTrue( $inherited->isInherited() );
|
|
|
|
|
$this->assertFalse( $inherited->hasRevision() );
|
|
|
|
|
|
|
|
|
|
// make sure we didn't mess with the internal state of $parent
|
|
|
|
|
$this->assertFalse( $parent->isInherited() );
|
|
|
|
|
$this->assertSame( 7, $parent->getRevision() );
|
|
|
|
|
|
|
|
|
|
// This would happen while doing an edit, after saving the revision meta-data
|
|
|
|
|
// and content meta-data.
|
|
|
|
|
$saved = SlotRecord::newSaved(
|
|
|
|
|
10,
|
|
|
|
|
$inherited->getContentId(),
|
|
|
|
|
$inherited->getAddress(),
|
|
|
|
|
$inherited
|
|
|
|
|
);
|
|
|
|
|
$this->assertSame( $parent->getContentId(), $saved->getContentId() );
|
|
|
|
|
$this->assertSame( $parent->getAddress(), $saved->getAddress() );
|
|
|
|
|
$this->assertSame( $parent->getContent(), $saved->getContent() );
|
|
|
|
|
$this->assertTrue( $saved->isInherited() );
|
|
|
|
|
$this->assertTrue( $saved->hasRevision() );
|
|
|
|
|
$this->assertSame( 10, $saved->getRevision() );
|
|
|
|
|
|
|
|
|
|
// make sure we didn't mess with the internal state of $parent or $inherited
|
|
|
|
|
$this->assertSame( 7, $parent->getRevision() );
|
|
|
|
|
$this->assertFalse( $inherited->hasRevision() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testNewSaved() {
|
|
|
|
|
// This would happen while doing an edit, before saving revision meta-data.
|
|
|
|
|
$unsaved = SlotRecord::newUnsaved( 'main', new WikitextContent( 'A' ) );
|
|
|
|
|
|
|
|
|
|
// This would happen while doing an edit, after saving the revision meta-data
|
|
|
|
|
// and content meta-data.
|
|
|
|
|
$saved = SlotRecord::newSaved( 10, 20, 'theNewAddress', $unsaved );
|
|
|
|
|
$this->assertFalse( $saved->isInherited() );
|
|
|
|
|
$this->assertTrue( $saved->hasRevision() );
|
|
|
|
|
$this->assertTrue( $saved->hasAddress() );
|
|
|
|
|
$this->assertSame( 'theNewAddress', $saved->getAddress() );
|
|
|
|
|
$this->assertSame( 20, $saved->getContentId() );
|
|
|
|
|
$this->assertSame( 'A', $saved->getContent()->getNativeData() );
|
|
|
|
|
$this->assertSame( 10, $saved->getRevision() );
|
2018-03-06 14:42:43 +00:00
|
|
|
$this->assertSame( 10, $saved->getOrigin() );
|
2018-03-06 18:46:13 +00:00
|
|
|
|
|
|
|
|
// make sure we didn't mess with the internal state of $unsaved
|
|
|
|
|
$this->assertFalse( $unsaved->hasAddress() );
|
|
|
|
|
$this->assertFalse( $unsaved->hasRevision() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function provideNewSaved_LogicException() {
|
|
|
|
|
$freshRow = $this->makeRow( [
|
|
|
|
|
'content_id' => 10,
|
|
|
|
|
'content_address' => 'address:1',
|
2018-03-06 14:42:43 +00:00
|
|
|
'slot_origin' => 1,
|
2018-03-06 18:46:13 +00:00
|
|
|
'slot_revision_id' => 1,
|
|
|
|
|
] );
|
|
|
|
|
|
|
|
|
|
$freshSlot = new SlotRecord( $freshRow, new WikitextContent( 'A' ) );
|
|
|
|
|
yield 'mismatching address' => [ 1, 10, 'address:BAD', $freshSlot ];
|
|
|
|
|
yield 'mismatching revision' => [ 5, 10, 'address:1', $freshSlot ];
|
|
|
|
|
yield 'mismatching content ID' => [ 1, 17, 'address:1', $freshSlot ];
|
|
|
|
|
|
|
|
|
|
$inheritedRow = $this->makeRow( [
|
|
|
|
|
'content_id' => null,
|
|
|
|
|
'content_address' => null,
|
2018-03-06 14:42:43 +00:00
|
|
|
'slot_origin' => 0,
|
|
|
|
|
'slot_revision_id' => 1,
|
2018-03-06 18:46:13 +00:00
|
|
|
] );
|
|
|
|
|
|
|
|
|
|
$inheritedSlot = new SlotRecord( $inheritedRow, new WikitextContent( 'A' ) );
|
|
|
|
|
yield 'inherited, but no address' => [ 1, 10, 'address:2', $inheritedSlot ];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider provideNewSaved_LogicException
|
|
|
|
|
*/
|
|
|
|
|
public function testNewSaved_LogicException(
|
|
|
|
|
$revisionId,
|
|
|
|
|
$contentId,
|
|
|
|
|
$contentAddress,
|
|
|
|
|
SlotRecord $protoSlot
|
|
|
|
|
) {
|
|
|
|
|
$this->setExpectedException( LogicException::class );
|
|
|
|
|
SlotRecord::newSaved( $revisionId, $contentId, $contentAddress, $protoSlot );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function provideNewSaved_InvalidArgumentException() {
|
|
|
|
|
$unsaved = SlotRecord::newUnsaved( 'main', new WikitextContent( 'A' ) );
|
|
|
|
|
|
|
|
|
|
yield 'bad revision id' => [ 'xyzzy', 5, 'address', $unsaved ];
|
|
|
|
|
yield 'bad content id' => [ 7, 'xyzzy', 'address', $unsaved ];
|
|
|
|
|
yield 'bad content address' => [ 7, 5, 77, $unsaved ];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider provideNewSaved_InvalidArgumentException
|
|
|
|
|
*/
|
|
|
|
|
public function testNewSaved_InvalidArgumentException(
|
|
|
|
|
$revisionId,
|
|
|
|
|
$contentId,
|
|
|
|
|
$contentAddress,
|
|
|
|
|
SlotRecord $protoSlot
|
|
|
|
|
) {
|
|
|
|
|
$this->setExpectedException( InvalidArgumentException::class );
|
|
|
|
|
SlotRecord::newSaved( $revisionId, $contentId, $contentAddress, $protoSlot );
|
2017-11-15 12:02:40 +00:00
|
|
|
}
|
|
|
|
|
|
2018-03-30 11:29:33 +00:00
|
|
|
public function provideHasSameContent() {
|
|
|
|
|
$fail = function () {
|
|
|
|
|
self::fail( 'There should be no need to actually load the content.' );
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
$a100a1 = new SlotRecord(
|
|
|
|
|
$this->makeRow(
|
|
|
|
|
[
|
|
|
|
|
'model_name' => 'A',
|
|
|
|
|
'content_size' => 100,
|
|
|
|
|
'content_sha1' => 'hash-a',
|
|
|
|
|
'content_address' => 'xxx:a1',
|
|
|
|
|
]
|
|
|
|
|
),
|
|
|
|
|
$fail
|
|
|
|
|
);
|
|
|
|
|
$a100a1b = new SlotRecord(
|
|
|
|
|
$this->makeRow(
|
|
|
|
|
[
|
|
|
|
|
'model_name' => 'A',
|
|
|
|
|
'content_size' => 100,
|
|
|
|
|
'content_sha1' => 'hash-a',
|
|
|
|
|
'content_address' => 'xxx:a1',
|
|
|
|
|
]
|
|
|
|
|
),
|
|
|
|
|
$fail
|
|
|
|
|
);
|
|
|
|
|
$a100null = new SlotRecord(
|
|
|
|
|
$this->makeRow(
|
|
|
|
|
[
|
|
|
|
|
'model_name' => 'A',
|
|
|
|
|
'content_size' => 100,
|
|
|
|
|
'content_sha1' => 'hash-a',
|
|
|
|
|
'content_address' => null,
|
|
|
|
|
]
|
|
|
|
|
),
|
|
|
|
|
$fail
|
|
|
|
|
);
|
|
|
|
|
$a100a2 = new SlotRecord(
|
|
|
|
|
$this->makeRow(
|
|
|
|
|
[
|
|
|
|
|
'model_name' => 'A',
|
|
|
|
|
'content_size' => 100,
|
|
|
|
|
'content_sha1' => 'hash-a',
|
|
|
|
|
'content_address' => 'xxx:a2',
|
|
|
|
|
]
|
|
|
|
|
),
|
|
|
|
|
$fail
|
|
|
|
|
);
|
|
|
|
|
$b100a1 = new SlotRecord(
|
|
|
|
|
$this->makeRow(
|
|
|
|
|
[
|
|
|
|
|
'model_name' => 'B',
|
|
|
|
|
'content_size' => 100,
|
|
|
|
|
'content_sha1' => 'hash-a',
|
|
|
|
|
'content_address' => 'xxx:a1',
|
|
|
|
|
]
|
|
|
|
|
),
|
|
|
|
|
$fail
|
|
|
|
|
);
|
|
|
|
|
$a200a1 = new SlotRecord(
|
|
|
|
|
$this->makeRow(
|
|
|
|
|
[
|
|
|
|
|
'model_name' => 'A',
|
|
|
|
|
'content_size' => 200,
|
|
|
|
|
'content_sha1' => 'hash-a',
|
|
|
|
|
'content_address' => 'xxx:a2',
|
|
|
|
|
]
|
|
|
|
|
),
|
|
|
|
|
$fail
|
|
|
|
|
);
|
|
|
|
|
$a100x1 = new SlotRecord(
|
|
|
|
|
$this->makeRow(
|
|
|
|
|
[
|
|
|
|
|
'model_name' => 'A',
|
|
|
|
|
'content_size' => 100,
|
|
|
|
|
'content_sha1' => 'hash-x',
|
|
|
|
|
'content_address' => 'xxx:x1',
|
|
|
|
|
]
|
|
|
|
|
),
|
|
|
|
|
$fail
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
yield 'same instance' => [ $a100a1, $a100a1, true ];
|
|
|
|
|
yield 'no address' => [ $a100a1, $a100null, true ];
|
|
|
|
|
yield 'same address' => [ $a100a1, $a100a1b, true ];
|
|
|
|
|
yield 'different address' => [ $a100a1, $a100a2, true ];
|
|
|
|
|
yield 'different model' => [ $a100a1, $b100a1, false ];
|
|
|
|
|
yield 'different size' => [ $a100a1, $a200a1, false ];
|
|
|
|
|
yield 'different hash' => [ $a100a1, $a100x1, false ];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider provideHasSameContent
|
|
|
|
|
*/
|
|
|
|
|
public function testHasSameContent( SlotRecord $a, SlotRecord $b, $sameContent ) {
|
|
|
|
|
$this->assertSame( $sameContent, $a->hasSameContent( $b ) );
|
|
|
|
|
$this->assertSame( $sameContent, $b->hasSameContent( $a ) );
|
|
|
|
|
}
|
|
|
|
|
|
2017-11-15 12:02:40 +00:00
|
|
|
}
|