2020-02-10 10:04:38 -05:00
< ? php
/**
* @ copyright Copyright ( c ) 2020 , Thomas Citharel < nextcloud @ tcit . fr >
*
2020-03-31 04:49:10 -04:00
* @ author Christoph Wurst < christoph @ winzerhof - wurst . at >
2020-12-16 08:54:15 -05:00
* @ author eleith < online + github @ eleith . com >
2020-02-10 10:04:38 -05:00
* @ author Georg Ehrke < oc . list @ georgehrke . com >
2020-04-29 05:57:22 -04:00
* @ author Joas Schilling < coding @ schilljs . com >
2020-02-10 10:04:38 -05:00
* @ author Thomas Citharel < nextcloud @ tcit . fr >
*
* @ license GNU AGPL version 3 or any later version
*
* This program is free software : you can redistribute it and / or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation , either version 3 of the
* License , or ( at your option ) any later version .
*
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
2021-06-04 15:52:51 -04:00
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
2020-02-10 10:04:38 -05:00
* GNU Affero General Public License for more details .
*
* You should have received a copy of the GNU Affero General Public License
* along with this program . If not , see < http :// www . gnu . org / licenses />.
*
*/
2022-03-31 09:34:43 -04:00
namespace OCA\DAV\Tests\unit\CalDAV\WebcalCaching ;
2020-02-10 10:04:38 -05:00
use GuzzleHttp\HandlerStack ;
use OCA\DAV\CalDAV\CalDavBackend ;
use OCA\DAV\CalDAV\WebcalCaching\RefreshWebcalService ;
use OCP\Http\Client\IClient ;
use OCP\Http\Client\IClientService ;
use OCP\Http\Client\IResponse ;
2020-04-14 12:55:41 -04:00
use OCP\Http\Client\LocalServerException ;
2020-02-10 10:04:38 -05:00
use OCP\IConfig ;
use PHPUnit\Framework\MockObject\MockObject ;
2022-03-31 09:34:57 -04:00
use Psr\Log\LoggerInterface ;
2020-09-20 01:47:30 -04:00
use Sabre\DAV\Exception\BadRequest ;
2020-02-10 10:04:38 -05:00
use Sabre\VObject ;
2020-09-20 01:47:30 -04:00
use Sabre\VObject\Recur\NoInstancesException ;
2020-02-10 10:04:38 -05:00
use Test\TestCase ;
class RefreshWebcalServiceTest extends TestCase {
/** @var CalDavBackend | MockObject */
private $caldavBackend ;
/** @var IClientService | MockObject */
private $clientService ;
/** @var IConfig | MockObject */
private $config ;
2022-03-31 09:34:57 -04:00
/** @var LoggerInterface | MockObject */
2020-02-10 10:04:38 -05:00
private $logger ;
protected function setUp () : void {
parent :: setUp ();
$this -> caldavBackend = $this -> createMock ( CalDavBackend :: class );
$this -> clientService = $this -> createMock ( IClientService :: class );
$this -> config = $this -> createMock ( IConfig :: class );
2022-03-31 09:34:57 -04:00
$this -> logger = $this -> createMock ( LoggerInterface :: class );
2020-02-10 10:04:38 -05:00
}
/**
* @ param string $body
* @ param string $contentType
* @ param string $result
*
* @ dataProvider runDataProvider
*/
2023-01-20 02:38:43 -05:00
public function testRun ( string $body , string $contentType , string $result ) : void {
2020-03-16 10:04:21 -04:00
$refreshWebcalService = $this -> getMockBuilder ( RefreshWebcalService :: class )
2022-03-18 13:07:24 -04:00
-> onlyMethods ([ 'getRandomCalendarObjectUri' ])
2020-03-16 10:04:21 -04:00
-> setConstructorArgs ([ $this -> caldavBackend , $this -> clientService , $this -> config , $this -> logger ])
-> getMock ();
$refreshWebcalService
-> method ( 'getRandomCalendarObjectUri' )
-> willReturn ( 'uri-1.ics' );
2020-02-10 10:04:38 -05:00
$this -> caldavBackend -> expects ( $this -> once ())
-> method ( 'getSubscriptionsForUser' )
-> with ( 'principals/users/testuser' )
2020-03-25 17:21:27 -04:00
-> willReturn ([
2020-02-10 10:04:38 -05:00
[
'id' => '99' ,
'uri' => 'sub456' ,
'{http://apple.com/ns/ical/}refreshrate' => 'P1D' ,
'{http://calendarserver.org/ns/}subscribed-strip-todos' => '1' ,
'{http://calendarserver.org/ns/}subscribed-strip-alarms' => '1' ,
'{http://calendarserver.org/ns/}subscribed-strip-attachments' => '1' ,
'source' => 'webcal://foo.bar/bla'
],
[
'id' => '42' ,
'uri' => 'sub123' ,
'{http://apple.com/ns/ical/}refreshrate' => 'PT1H' ,
'{http://calendarserver.org/ns/}subscribed-strip-todos' => '1' ,
'{http://calendarserver.org/ns/}subscribed-strip-alarms' => '1' ,
'{http://calendarserver.org/ns/}subscribed-strip-attachments' => '1' ,
'source' => 'webcal://foo.bar/bla2'
],
2020-03-25 17:21:27 -04:00
]);
2020-02-10 10:04:38 -05:00
$client = $this -> createMock ( IClient :: class );
$response = $this -> createMock ( IResponse :: class );
$this -> clientService -> expects ( $this -> once ())
-> method ( 'newClient' )
-> with ()
2020-03-25 17:21:27 -04:00
-> willReturn ( $client );
2020-02-10 10:04:38 -05:00
$this -> config -> expects ( $this -> once ())
-> method ( 'getAppValue' )
-> with ( 'dav' , 'webcalAllowLocalAccess' , 'no' )
2020-03-25 17:21:27 -04:00
-> willReturn ( 'no' );
2020-02-10 10:04:38 -05:00
$client -> expects ( $this -> once ())
-> method ( 'get' )
2020-04-09 07:53:40 -04:00
-> with ( 'https://foo.bar/bla2' , $this -> callback ( function ( $obj ) {
2020-02-10 10:04:38 -05:00
return $obj [ 'allow_redirects' ][ 'redirects' ] === 10 && $obj [ 'handler' ] instanceof HandlerStack ;
}))
2020-03-25 17:21:27 -04:00
-> willReturn ( $response );
2020-02-10 10:04:38 -05:00
$response -> expects ( $this -> once ())
-> method ( 'getBody' )
-> with ()
2020-03-25 17:21:27 -04:00
-> willReturn ( $body );
2020-02-10 10:04:38 -05:00
$response -> expects ( $this -> once ())
-> method ( 'getHeader' )
-> with ( 'Content-Type' )
2020-03-25 17:21:27 -04:00
-> willReturn ( $contentType );
2020-02-10 10:04:38 -05:00
$this -> caldavBackend -> expects ( $this -> once ())
-> method ( 'purgeAllCachedEventsForSubscription' )
-> with ( 42 );
$this -> caldavBackend -> expects ( $this -> once ())
-> method ( 'createCalendarObject' )
2020-03-16 10:04:21 -04:00
-> with ( 42 , 'uri-1.ics' , $result , 1 );
2020-02-10 10:04:38 -05:00
$refreshWebcalService -> refreshSubscription ( 'principals/users/testuser' , 'sub123' );
}
2022-03-31 09:34:57 -04:00
2020-09-20 01:47:30 -04:00
/**
* @ param string $body
* @ param string $contentType
* @ param string $result
*
* @ dataProvider runDataProvider
*/
2023-01-20 02:38:43 -05:00
public function testRunCreateCalendarNoException ( string $body , string $contentType , string $result ) : void {
2020-09-20 01:47:30 -04:00
$client = $this -> createMock ( IClient :: class );
$response = $this -> createMock ( IResponse :: class );
$refreshWebcalService = $this -> getMockBuilder ( RefreshWebcalService :: class )
2022-03-18 13:07:24 -04:00
-> onlyMethods ([ 'getRandomCalendarObjectUri' , 'getSubscription' , 'queryWebcalFeed' ])
2020-09-20 01:47:30 -04:00
-> setConstructorArgs ([ $this -> caldavBackend , $this -> clientService , $this -> config , $this -> logger ])
-> getMock ();
$refreshWebcalService
-> method ( 'getRandomCalendarObjectUri' )
-> willReturn ( 'uri-1.ics' );
$refreshWebcalService
-> method ( 'getSubscription' )
-> willReturn ([
'id' => '42' ,
'uri' => 'sub123' ,
'{http://apple.com/ns/ical/}refreshrate' => 'PT1H' ,
'{http://calendarserver.org/ns/}subscribed-strip-todos' => '1' ,
'{http://calendarserver.org/ns/}subscribed-strip-alarms' => '1' ,
'{http://calendarserver.org/ns/}subscribed-strip-attachments' => '1' ,
'source' => 'webcal://foo.bar/bla2'
]);
$this -> clientService -> expects ( $this -> once ())
-> method ( 'newClient' )
-> with ()
-> willReturn ( $client );
$this -> config -> expects ( $this -> once ())
-> method ( 'getAppValue' )
-> with ( 'dav' , 'webcalAllowLocalAccess' , 'no' )
-> willReturn ( 'no' );
$client -> expects ( $this -> once ())
-> method ( 'get' )
-> with ( 'https://foo.bar/bla2' , $this -> callback ( function ( $obj ) {
return $obj [ 'allow_redirects' ][ 'redirects' ] === 10 && $obj [ 'handler' ] instanceof HandlerStack ;
}))
-> willReturn ( $response );
$response -> expects ( $this -> once ())
-> method ( 'getBody' )
-> with ()
-> willReturn ( $body );
$response -> expects ( $this -> once ())
-> method ( 'getHeader' )
-> with ( 'Content-Type' )
-> willReturn ( $contentType );
$this -> caldavBackend -> expects ( $this -> once ())
-> method ( 'purgeAllCachedEventsForSubscription' )
-> with ( 42 );
$this -> caldavBackend -> expects ( $this -> once ())
-> method ( 'createCalendarObject' )
-> with ( 42 , 'uri-1.ics' , $result , 1 );
2022-03-31 09:34:57 -04:00
2020-09-20 01:47:30 -04:00
$noInstanceException = new NoInstancesException ( " can't add calendar object " );
$this -> caldavBackend -> expects ( $this -> once ())
-> method ( " createCalendarObject " )
-> willThrowException ( $noInstanceException );
2022-03-31 09:34:57 -04:00
2020-09-20 01:47:30 -04:00
$this -> logger -> expects ( $this -> once ())
2022-03-31 09:34:57 -04:00
-> method ( 'error' )
2022-03-18 13:07:24 -04:00
-> with ( 'Unable to create calendar object from subscription {subscriptionId}' , [ 'exception' => $noInstanceException , 'subscriptionId' => '42' , 'source' => 'webcal://foo.bar/bla2' ]);
2020-09-20 01:47:30 -04:00
$refreshWebcalService -> refreshSubscription ( 'principals/users/testuser' , 'sub123' );
}
/**
* @ param string $body
* @ param string $contentType
* @ param string $result
*
* @ dataProvider runDataProvider
*/
2023-01-20 02:38:43 -05:00
public function testRunCreateCalendarBadRequest ( string $body , string $contentType , string $result ) : void {
2020-09-20 01:47:30 -04:00
$client = $this -> createMock ( IClient :: class );
$response = $this -> createMock ( IResponse :: class );
$refreshWebcalService = $this -> getMockBuilder ( RefreshWebcalService :: class )
2022-03-18 13:07:24 -04:00
-> onlyMethods ([ 'getRandomCalendarObjectUri' , 'getSubscription' , 'queryWebcalFeed' ])
2020-09-20 01:47:30 -04:00
-> setConstructorArgs ([ $this -> caldavBackend , $this -> clientService , $this -> config , $this -> logger ])
-> getMock ();
$refreshWebcalService
-> method ( 'getRandomCalendarObjectUri' )
-> willReturn ( 'uri-1.ics' );
$refreshWebcalService
-> method ( 'getSubscription' )
-> willReturn ([
'id' => '42' ,
'uri' => 'sub123' ,
'{http://apple.com/ns/ical/}refreshrate' => 'PT1H' ,
'{http://calendarserver.org/ns/}subscribed-strip-todos' => '1' ,
'{http://calendarserver.org/ns/}subscribed-strip-alarms' => '1' ,
'{http://calendarserver.org/ns/}subscribed-strip-attachments' => '1' ,
'source' => 'webcal://foo.bar/bla2'
]);
$this -> clientService -> expects ( $this -> once ())
-> method ( 'newClient' )
-> with ()
-> willReturn ( $client );
$this -> config -> expects ( $this -> once ())
-> method ( 'getAppValue' )
-> with ( 'dav' , 'webcalAllowLocalAccess' , 'no' )
-> willReturn ( 'no' );
$client -> expects ( $this -> once ())
-> method ( 'get' )
-> with ( 'https://foo.bar/bla2' , $this -> callback ( function ( $obj ) {
return $obj [ 'allow_redirects' ][ 'redirects' ] === 10 && $obj [ 'handler' ] instanceof HandlerStack ;
}))
-> willReturn ( $response );
$response -> expects ( $this -> once ())
-> method ( 'getBody' )
-> with ()
-> willReturn ( $body );
$response -> expects ( $this -> once ())
-> method ( 'getHeader' )
-> with ( 'Content-Type' )
-> willReturn ( $contentType );
$this -> caldavBackend -> expects ( $this -> once ())
-> method ( 'purgeAllCachedEventsForSubscription' )
-> with ( 42 );
$this -> caldavBackend -> expects ( $this -> once ())
-> method ( 'createCalendarObject' )
-> with ( 42 , 'uri-1.ics' , $result , 1 );
2022-03-31 09:34:57 -04:00
2020-09-20 01:47:30 -04:00
$badRequestException = new BadRequest ( " can't add reach calendar url " );
$this -> caldavBackend -> expects ( $this -> once ())
-> method ( " createCalendarObject " )
-> willThrowException ( $badRequestException );
2022-03-31 09:34:57 -04:00
2020-09-20 01:47:30 -04:00
$this -> logger -> expects ( $this -> once ())
2022-03-31 09:34:57 -04:00
-> method ( 'error' )
2022-03-18 13:07:24 -04:00
-> with ( 'Unable to create calendar object from subscription {subscriptionId}' , [ 'exception' => $badRequestException , 'subscriptionId' => '42' , 'source' => 'webcal://foo.bar/bla2' ]);
2020-09-20 01:47:30 -04:00
$refreshWebcalService -> refreshSubscription ( 'principals/users/testuser' , 'sub123' );
}
2020-02-10 10:04:38 -05:00
/**
* @ return array
*/
public function runDataProvider () : array {
return [
[
" BEGIN:VCALENDAR \r \n VERSION:2.0 \r \n PRODID:-//Sabre//Sabre VObject 4.1.1//EN \r \n CALSCALE:GREGORIAN \r \n BEGIN:VEVENT \r \n UID:12345 \r \n DTSTAMP:20160218T133704Z \r \n DTSTART;VALUE=DATE:19000101 \r \n DTEND;VALUE=DATE:19000102 \r \n RRULE:FREQ=YEARLY \r \n SUMMARY:12345's Birthday (1900) \r \n TRANSP:TRANSPARENT \r \n END:VEVENT \r \n END:VCALENDAR \r \n " ,
'text/calendar;charset=utf8' ,
" BEGIN:VCALENDAR \r \n VERSION:2.0 \r \n PRODID:-//Sabre//Sabre VObject " . VObject\Version :: VERSION . " //EN \r \n CALSCALE:GREGORIAN \r \n BEGIN:VEVENT \r \n UID:12345 \r \n DTSTAMP:20160218T133704Z \r \n DTSTART;VALUE=DATE:19000101 \r \n DTEND;VALUE=DATE:19000102 \r \n RRULE:FREQ=YEARLY \r \n SUMMARY:12345's Birthday (1900) \r \n TRANSP:TRANSPARENT \r \n END:VEVENT \r \n END:VCALENDAR \r \n " ,
],
[
'["vcalendar",[["prodid",{},"text","-//Example Corp.//Example Client//EN"],["version",{},"text","2.0"]],[["vtimezone",[["last-modified",{},"date-time","2004-01-10T03:28:45Z"],["tzid",{},"text","US/Eastern"]],[["daylight",[["dtstart",{},"date-time","2000-04-04T02:00:00"],["rrule",{},"recur",{"freq":"YEARLY","byday":"1SU","bymonth":4}],["tzname",{},"text","EDT"],["tzoffsetfrom",{},"utc-offset","-05:00"],["tzoffsetto",{},"utc-offset","-04:00"]],[]],["standard",[["dtstart",{},"date-time","2000-10-26T02:00:00"],["rrule",{},"recur",{"freq":"YEARLY","byday":"1SU","bymonth":10}],["tzname",{},"text","EST"],["tzoffsetfrom",{},"utc-offset","-04:00"],["tzoffsetto",{},"utc-offset","-05:00"]],[]]]],["vevent",[["dtstamp",{},"date-time","2006-02-06T00:11:21Z"],["dtstart",{"tzid":"US/Eastern"},"date-time","2006-01-02T14:00:00"],["duration",{},"duration","PT1H"],["recurrence-id",{"tzid":"US/Eastern"},"date-time","2006-01-04T12:00:00"],["summary",{},"text","Event #2"],["uid",{},"text","12345"]],[]]]]' ,
'application/calendar+json' ,
" BEGIN:VCALENDAR \r \n VERSION:2.0 \r \n PRODID:-//Sabre//Sabre VObject " . VObject\Version :: VERSION . " //EN \r \n CALSCALE:GREGORIAN \r \n BEGIN:VTIMEZONE \r \n LAST-MODIFIED:20040110T032845Z \r \n TZID:US/Eastern \r \n BEGIN:DAYLIGHT \r \n DTSTART:20000404T020000 \r \n RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 \r \n TZNAME:EDT \r \n TZOFFSETFROM:-0500 \r \n TZOFFSETTO:-0400 \r \n END:DAYLIGHT \r \n BEGIN:STANDARD \r \n DTSTART:20001026T020000 \r \n RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=10 \r \n TZNAME:EST \r \n TZOFFSETFROM:-0400 \r \n TZOFFSETTO:-0500 \r \n END:STANDARD \r \n END:VTIMEZONE \r \n BEGIN:VEVENT \r \n DTSTAMP:20060206T001121Z \r \n DTSTART;TZID=US/Eastern:20060102T140000 \r \n DURATION:PT1H \r \n RECURRENCE-ID;TZID=US/Eastern:20060104T120000 \r \n SUMMARY:Event #2 \r \n UID:12345 \r \n END:VEVENT \r \n END:VCALENDAR \r \n "
],
[
'<?xml version="1.0" encoding="utf-8" ?><icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"><vcalendar><properties><prodid><text>-//Example Inc.//Example Client//EN</text></prodid><version><text>2.0</text></version></properties><components><vevent><properties><dtstamp><date-time>2006-02-06T00:11:21Z</date-time></dtstamp><dtstart><parameters><tzid><text>US/Eastern</text></tzid></parameters><date-time>2006-01-04T14:00:00</date-time></dtstart><duration><duration>PT1H</duration></duration><recurrence-id><parameters><tzid><text>US/Eastern</text></tzid></parameters><date-time>2006-01-04T12:00:00</date-time></recurrence-id><summary><text>Event #2 bis</text></summary><uid><text>12345</text></uid></properties></vevent></components></vcalendar></icalendar>' ,
'application/calendar+xml' ,
" BEGIN:VCALENDAR \r \n VERSION:2.0 \r \n PRODID:-//Sabre//Sabre VObject " . VObject\Version :: VERSION . " //EN \r \n CALSCALE:GREGORIAN \r \n BEGIN:VEVENT \r \n DTSTAMP:20060206T001121Z \r \n DTSTART;TZID=US/Eastern:20060104T140000 \r \n DURATION:PT1H \r \n RECURRENCE-ID;TZID=US/Eastern:20060104T120000 \r \n SUMMARY:Event #2 bis \r \n UID:12345 \r \n END:VEVENT \r \n END:VCALENDAR \r \n "
]
];
}
/**
* @ dataProvider runLocalURLDataProvider
*/
2023-01-20 02:38:43 -05:00
public function testRunLocalURL ( string $source ) : void {
2020-04-14 12:55:41 -04:00
$refreshWebcalService = new RefreshWebcalService (
$this -> caldavBackend ,
$this -> clientService ,
$this -> config ,
$this -> logger
);
2020-02-10 10:04:38 -05:00
$this -> caldavBackend -> expects ( $this -> once ())
-> method ( 'getSubscriptionsForUser' )
-> with ( 'principals/users/testuser' )
2020-03-25 17:21:27 -04:00
-> willReturn ([
2020-02-10 10:04:38 -05:00
[
'id' => 42 ,
'uri' => 'sub123' ,
'refreshreate' => 'P1H' ,
'striptodos' => 1 ,
'stripalarms' => 1 ,
'stripattachments' => 1 ,
'source' => $source
],
2020-03-25 17:21:27 -04:00
]);
2020-02-10 10:04:38 -05:00
$client = $this -> createMock ( IClient :: class );
$this -> clientService -> expects ( $this -> once ())
-> method ( 'newClient' )
-> with ()
2020-03-25 17:21:27 -04:00
-> willReturn ( $client );
2020-02-10 10:04:38 -05:00
$this -> config -> expects ( $this -> once ())
-> method ( 'getAppValue' )
-> with ( 'dav' , 'webcalAllowLocalAccess' , 'no' )
2020-03-25 17:21:27 -04:00
-> willReturn ( 'no' );
2020-02-10 10:04:38 -05:00
2022-03-18 13:07:24 -04:00
$localServerException = new LocalServerException ();
2020-04-14 12:55:41 -04:00
$client -> expects ( $this -> once ())
-> method ( 'get' )
2022-03-18 13:07:24 -04:00
-> willThrowException ( $localServerException );
2020-04-14 12:55:41 -04:00
$this -> logger -> expects ( $this -> once ())
2022-03-31 09:34:57 -04:00
-> method ( 'warning' )
2022-03-18 13:07:24 -04:00
-> with ( " Subscription 42 was not refreshed because it violates local access rules " , [ 'exception' => $localServerException ]);
2020-02-10 10:04:38 -05:00
$refreshWebcalService -> refreshSubscription ( 'principals/users/testuser' , 'sub123' );
}
public function runLocalURLDataProvider () : array {
return [
[ 'localhost/foo.bar' ],
[ 'localHost/foo.bar' ],
[ 'random-host/foo.bar' ],
[ '[::1]/bla.blub' ],
[ '[::]/bla.blub' ],
[ '192.168.0.1' ],
[ '172.16.42.1' ],
[ '[fdf8:f53b:82e4::53]/secret.ics' ],
[ '[fe80::200:5aee:feaa:20a2]/secret.ics' ],
[ '[0:0:0:0:0:0:10.0.0.1]/secret.ics' ],
[ '[0:0:0:0:0:ffff:127.0.0.0]/secret.ics' ],
[ '10.0.0.1' ],
[ 'another-host.local' ],
[ 'service.localhost' ],
];
}
2020-04-14 12:55:41 -04:00
2023-01-20 02:38:43 -05:00
public function testInvalidUrl () : void {
2020-04-14 12:55:41 -04:00
$refreshWebcalService = new RefreshWebcalService ( $this -> caldavBackend ,
$this -> clientService , $this -> config , $this -> logger );
$this -> caldavBackend -> expects ( $this -> once ())
-> method ( 'getSubscriptionsForUser' )
-> with ( 'principals/users/testuser' )
-> willReturn ([
[
'id' => 42 ,
'uri' => 'sub123' ,
'refreshreate' => 'P1H' ,
'striptodos' => 1 ,
'stripalarms' => 1 ,
'stripattachments' => 1 ,
'source' => '!@#$'
],
]);
$client = $this -> createMock ( IClient :: class );
$this -> clientService -> expects ( $this -> once ())
-> method ( 'newClient' )
-> with ()
-> willReturn ( $client );
$this -> config -> expects ( $this -> once ())
-> method ( 'getAppValue' )
-> with ( 'dav' , 'webcalAllowLocalAccess' , 'no' )
-> willReturn ( 'no' );
$client -> expects ( $this -> never ())
-> method ( 'get' );
$refreshWebcalService -> refreshSubscription ( 'principals/users/testuser' , 'sub123' );
}
2020-02-10 10:04:38 -05:00
}