Contacts API: replace raw image data with url (#25081)

* add uri to AddressBookImpl array

* Introduce ImageExportPlugin for CardDav

* add plugin to v1 routes

* replace binary contact photo with link

* update tests

* Adding unit tests
This commit is contained in:
Georg Ehrke 2016-06-21 15:25:44 +02:00 committed by Thomas Müller
parent f2f1eab7f4
commit 1452b74de7
11 changed files with 384 additions and 39 deletions

View File

@ -75,6 +75,7 @@ if ($debugging) {
}
$server->addPlugin(new \Sabre\CardDAV\VCFExportPlugin());
$server->addPlugin(new \OCA\DAV\CardDAV\ImageExportPlugin(\OC::$server->getLogger()));
$server->addPlugin(new ExceptionLoggerPlugin('carddav', \OC::$server->getLogger()));
// And off we go!

View File

@ -136,7 +136,8 @@ class Application extends App {
public function setupContactsProvider(IManager $contactsManager, $userID) {
/** @var ContactsManager $cm */
$cm = $this->getContainer()->query('ContactsManager');
$cm->setupContactsProvider($contactsManager, $userID);
$urlGenerator = $this->getContainer()->getServer()->getURLGenerator();
$cm->setupContactsProvider($contactsManager, $userID, $urlGenerator);
}
public function registerHooks() {

View File

@ -24,6 +24,7 @@ namespace OCA\DAV\CardDAV;
use OCP\Constants;
use OCP\IAddressBook;
use OCP\IURLGenerator;
use Sabre\VObject\Component\VCard;
use Sabre\VObject\Property\Text;
use Sabre\VObject\Reader;
@ -40,21 +41,27 @@ class AddressBookImpl implements IAddressBook {
/** @var AddressBook */
private $addressBook;
/** @var IURLGenerator */
private $urlGenerator;
/**
* AddressBookImpl constructor.
*
* @param AddressBook $addressBook
* @param array $addressBookInfo
* @param CardDavBackend $backend
* @param IUrlGenerator $urlGenerator
*/
public function __construct(
AddressBook $addressBook,
array $addressBookInfo,
CardDavBackend $backend) {
CardDavBackend $backend,
IURLGenerator $urlGenerator) {
$this->addressBook = $addressBook;
$this->addressBookInfo = $addressBookInfo;
$this->backend = $backend;
$this->urlGenerator = $urlGenerator;
}
/**
@ -83,11 +90,11 @@ class AddressBookImpl implements IAddressBook {
* @since 5.0.0
*/
public function search($pattern, $searchProperties, $options) {
$result = $this->backend->search($this->getKey(), $pattern, $searchProperties);
$results = $this->backend->search($this->getKey(), $pattern, $searchProperties);
$vCards = [];
foreach ($result as $cardData) {
$vCards[] = $this->vCard2Array($this->readCard($cardData));
foreach ($results as $result) {
$vCards[] = $this->vCard2Array($result['uri'], $this->readCard($result['carddata']));
}
return $vCards;
@ -100,13 +107,12 @@ class AddressBookImpl implements IAddressBook {
*/
public function createOrUpdate($properties) {
$update = false;
if (!isset($properties['UID'])) { // create a new contact
if (!isset($properties['URI'])) { // create a new contact
$uid = $this->createUid();
$uri = $uid . '.vcf';
$vCard = $this->createEmptyVCard($uid);
} else { // update existing contact
$uid = $properties['UID'];
$uri = $uid . '.vcf';
$uri = $properties['URI'];
$vCardData = $this->backend->getCard($this->getKey(), $uri);
$vCard = $this->readCard($vCardData['carddata']);
$update = true;
@ -122,7 +128,7 @@ class AddressBookImpl implements IAddressBook {
$this->backend->createCard($this->getKey(), $uri, $vCard->serialize());
}
return $this->vCard2Array($vCard);
return $this->vCard2Array($uri, $vCard);
}
@ -207,13 +213,31 @@ class AddressBookImpl implements IAddressBook {
/**
* create array with all vCard properties
*
* @param string $uri
* @param VCard $vCard
* @return array
*/
protected function vCard2Array(VCard $vCard) {
$result = [];
protected function vCard2Array($uri, VCard $vCard) {
$result = [
'URI' => $uri,
];
foreach ($vCard->children as $property) {
$result[$property->name] = $property->getValue();
if ($property->name === 'PHOTO' && $property->getValueType() === 'BINARY') {
$url = $this->urlGenerator->getAbsoluteURL(
$this->urlGenerator->linkTo('', 'remote.php') . '/dav/');
$url .= implode('/', [
'addressbooks',
substr($this->addressBookInfo['principaluri'], 11), //cut off 'principals/'
$this->addressBookInfo['uri'],
$uri
]) . '?photo';
$result['PHOTO'] = 'VALUE=uri:' . $url;
} else {
$result[$property->name] = $property->getValue();
}
}
if ($this->addressBookInfo['principaluri'] === 'principals/system/system' &&
$this->addressBookInfo['uri'] === 'system') {

View File

@ -780,7 +780,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
}
$query2->andWhere($query2->expr()->eq('cp.addressbookid', $query->createNamedParameter($addressBookId)));
$query->select('c.carddata')->from($this->dbCardsTable, 'c')
$query->select('c.carddata', 'c.uri')->from($this->dbCardsTable, 'c')
->where($query->expr()->in('c.id', $query->createFunction($query2->getSQL())));
$result = $query->execute();
@ -788,8 +788,10 @@ class CardDavBackend implements BackendInterface, SyncSupport {
$result->closeCursor();
return array_map(function($array) {return $this->readBlob($array['carddata']);}, $cards);
return array_map(function($array) {
$array['carddata'] = $this->readBlob($array['carddata']);
return $array;
}, $cards);
}
/**

View File

@ -22,6 +22,7 @@
namespace OCA\DAV\CardDAV;
use OCP\Contacts\IManager;
use OCP\IURLGenerator;
class ContactsManager {
@ -37,26 +38,29 @@ class ContactsManager {
/**
* @param IManager $cm
* @param string $userId
* @param IURLGenerator $urlGenerator
*/
public function setupContactsProvider(IManager $cm, $userId) {
public function setupContactsProvider(IManager $cm, $userId, IURLGenerator $urlGenerator) {
$addressBooks = $this->backend->getAddressBooksForUser("principals/users/$userId");
$this->register($cm, $addressBooks);
$this->register($cm, $addressBooks, $urlGenerator);
$addressBooks = $this->backend->getAddressBooksForUser("principals/system/system");
$this->register($cm, $addressBooks);
$this->register($cm, $addressBooks, $urlGenerator);
}
/**
* @param IManager $cm
* @param $addressBooks
* @param IURLGenerator $urlGenerator
*/
private function register(IManager $cm, $addressBooks) {
private function register(IManager $cm, $addressBooks, $urlGenerator) {
foreach ($addressBooks as $addressBookInfo) {
$addressBook = new \OCA\DAV\CardDAV\AddressBook($this->backend, $addressBookInfo);
$cm->registerAddressBook(
new AddressBookImpl(
$addressBook,
$addressBookInfo,
$this->backend
$this->backend,
$urlGenerator
)
);
}

View File

@ -0,0 +1,146 @@
<?php
/**
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @copyright Copyright (c) 2016, ownCloud, Inc.
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\DAV\CardDAV;
use OCP\ILogger;
use Sabre\CardDAV\Card;
use Sabre\DAV\Server;
use Sabre\DAV\ServerPlugin;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
use Sabre\VObject\Parameter;
use Sabre\VObject\Property\Binary;
use Sabre\VObject\Reader;
class ImageExportPlugin extends ServerPlugin {
/** @var Server */
protected $server;
/** @var ILogger */
private $logger;
public function __construct(ILogger $logger) {
$this->logger = $logger;
}
/**
* Initializes the plugin and registers event handlers
*
* @param Server $server
* @return void
*/
function initialize(Server $server) {
$this->server = $server;
$this->server->on('method:GET', [$this, 'httpGet'], 90);
}
/**
* Intercepts GET requests on addressbook urls ending with ?photo.
*
* @param RequestInterface $request
* @param ResponseInterface $response
* @return bool|void
*/
function httpGet(RequestInterface $request, ResponseInterface $response) {
$queryParams = $request->getQueryParameters();
// TODO: in addition to photo we should also add logo some point in time
if (!array_key_exists('photo', $queryParams)) {
return true;
}
$path = $request->getPath();
$node = $this->server->tree->getNodeForPath($path);
if (!($node instanceof Card)) {
return true;
}
$this->server->transactionType = 'carddav-image-export';
// Checking ACL, if available.
if ($aclPlugin = $this->server->getPlugin('acl')) {
/** @var \Sabre\DAVACL\Plugin $aclPlugin */
$aclPlugin->checkPrivileges($path, '{DAV:}read');
}
if ($result = $this->getPhoto($node)) {
$response->setHeader('Content-Type', $result['Content-Type']);
$response->setStatus(200);
$response->setBody($result['body']);
// Returning false to break the event chain
return false;
}
return true;
}
function getPhoto(Card $node) {
// TODO: this is kind of expensive - load carddav data from database and parse it
// we might want to build up a cache one day
try {
$vObject = $this->readCard($node->get());
if (!$vObject->PHOTO) {
return false;
}
$photo = $vObject->PHOTO;
$type = $this->getType($photo);
$valType = $photo->getValueType();
$val = ($valType === 'URI' ? $photo->getRawMimeDirValue() : $photo->getValue());
return [
'Content-Type' => $type,
'body' => $val
];
} catch(\Exception $ex) {
$this->logger->logException($ex);
}
return false;
}
private function readCard($cardData) {
return Reader::read($cardData);
}
/**
* @param Binary $photo
* @return Parameter
*/
private function getType($photo) {
$params = $photo->parameters();
if (isset($params['TYPE']) || isset($params['MEDIATYPE'])) {
/** @var Parameter $typeParam */
$typeParam = isset($params['TYPE']) ? $params['TYPE'] : $params['MEDIATYPE'];
$type = $typeParam->getValue();
if (strpos($type, 'image/') === 0) {
return $type;
} else {
return 'image/' . strtolower($type);
}
}
return '';
}
}

View File

@ -25,6 +25,7 @@
namespace OCA\DAV;
use OCA\DAV\CalDAV\Schedule\IMipPlugin;
use OCA\DAV\CardDAV\ImageExportPlugin;
use OCA\DAV\Connector\Sabre\Auth;
use OCA\DAV\Connector\Sabre\BlockLegacyClientPlugin;
use OCA\DAV\Connector\Sabre\DavAclPlugin;
@ -103,6 +104,7 @@ class Server {
// addressbook plugins
$this->server->addPlugin(new \OCA\DAV\CardDAV\Plugin());
$this->server->addPlugin(new VCFExportPlugin());
$this->server->addPlugin(new ImageExportPlugin(\OC::$server->getLogger()));
// system tags plugins
$this->server->addPlugin(new \OCA\DAV\SystemTag\SystemTagPlugin(

View File

@ -43,6 +43,9 @@ class AddressBookImplTest extends TestCase {
/** @var AddressBook | \PHPUnit_Framework_MockObject_MockObject */
private $addressBook;
/** @var \OCP\IURLGenerator | \PHPUnit_Framework_MockObject_MockObject */
private $urlGenerator;
/** @var CardDavBackend | \PHPUnit_Framework_MockObject_MockObject */
private $backend;
@ -61,11 +64,13 @@ class AddressBookImplTest extends TestCase {
$this->backend = $this->getMockBuilder('\OCA\DAV\CardDAV\CardDavBackend')
->disableOriginalConstructor()->getMock();
$this->vCard = $this->getMock('Sabre\VObject\Component\VCard');
$this->urlGenerator = $this->getMock('OCP\IURLGenerator');
$this->addressBookImpl = new AddressBookImpl(
$this->addressBook,
$this->addressBookInfo,
$this->backend
$this->backend,
$this->urlGenerator
);
}
@ -87,7 +92,8 @@ class AddressBookImplTest extends TestCase {
[
$this->addressBook,
$this->addressBookInfo,
$this->backend
$this->backend,
$this->urlGenerator,
]
)
->setMethods(['vCard2Array', 'readCard'])
@ -100,15 +106,18 @@ class AddressBookImplTest extends TestCase {
->with($this->addressBookInfo['id'], $pattern, $searchProperties)
->willReturn(
[
'cardData1',
'cardData2'
['uri' => 'foo.vcf', 'carddata' => 'cardData1'],
['uri' => 'bar.vcf', 'carddata' => 'cardData2']
]
);
$addressBookImpl->expects($this->exactly(2))->method('readCard')
->willReturn($this->vCard);
$addressBookImpl->expects($this->exactly(2))->method('vCard2Array')
->with($this->vCard)->willReturn('vCard');
->withConsecutive(
['foo.vcf', $this->vCard],
['bar.vcf', $this->vCard]
)->willReturn('vCard');
$result = $addressBookImpl->search($pattern, $searchProperties, []);
$this->assertTrue((is_array($result)));
@ -130,7 +139,8 @@ class AddressBookImplTest extends TestCase {
[
$this->addressBook,
$this->addressBookInfo,
$this->backend
$this->backend,
$this->urlGenerator,
]
)
->setMethods(['vCard2Array', 'createUid', 'createEmptyVCard'])
@ -146,7 +156,7 @@ class AddressBookImplTest extends TestCase {
$this->backend->expects($this->never())->method('updateCard');
$this->backend->expects($this->never())->method('getCard');
$addressBookImpl->expects($this->once())->method('vCard2Array')
->with($this->vCard)->willReturn(true);
->with('uid.vcf', $this->vCard)->willReturn(true);
$this->assertTrue($addressBookImpl->createOrUpdate($properties));
}
@ -161,7 +171,8 @@ class AddressBookImplTest extends TestCase {
public function testUpdate() {
$uid = 'uid';
$properties = ['UID' => $uid, 'FN' => 'John Doe'];
$uri = 'bla.vcf';
$properties = ['URI' => $uri, 'UID' => $uid, 'FN' => 'John Doe'];
/** @var \PHPUnit_Framework_MockObject_MockObject | AddressBookImpl $addressBookImpl */
$addressBookImpl = $this->getMockBuilder('OCA\DAV\CardDAV\AddressBookImpl')
@ -169,7 +180,8 @@ class AddressBookImplTest extends TestCase {
[
$this->addressBook,
$this->addressBookInfo,
$this->backend
$this->backend,
$this->urlGenerator,
]
)
->setMethods(['vCard2Array', 'createUid', 'createEmptyVCard', 'readCard'])
@ -178,7 +190,7 @@ class AddressBookImplTest extends TestCase {
$addressBookImpl->expects($this->never())->method('createUid');
$addressBookImpl->expects($this->never())->method('createEmptyVCard');
$this->backend->expects($this->once())->method('getCard')
->with($this->addressBookInfo['id'], $uid . '.vcf')
->with($this->addressBookInfo['id'], $uri)
->willReturn(['carddata' => 'data']);
$addressBookImpl->expects($this->once())->method('readCard')
->with('data')->willReturn($this->vCard);
@ -187,7 +199,7 @@ class AddressBookImplTest extends TestCase {
$this->backend->expects($this->never())->method('createCard');
$this->backend->expects($this->once())->method('updateCard');
$addressBookImpl->expects($this->once())->method('vCard2Array')
->with($this->vCard)->willReturn(true);
->with($uri, $this->vCard)->willReturn(true);
$this->assertTrue($addressBookImpl->createOrUpdate($properties));
}
@ -251,7 +263,8 @@ class AddressBookImplTest extends TestCase {
[
$this->addressBook,
$this->addressBookInfo,
$this->backend
$this->backend,
$this->urlGenerator,
]
)
->setMethods(['getUid'])

View File

@ -535,8 +535,8 @@ class CardDavBackendTest extends TestCase {
$found = [];
foreach ($result as $r) {
foreach ($expected as $exp) {
if (strpos($r, $exp) > 0) {
$found[$exp] = true;
if ($r['uri'] === $exp[0] && strpos($r['carddata'], $exp[1]) > 0) {
$found[$exp[1]] = true;
break;
}
}
@ -547,11 +547,11 @@ class CardDavBackendTest extends TestCase {
public function dataTestSearch() {
return [
['John', ['FN'], ['John Doe', 'John M. Doe']],
['M. Doe', ['FN'], ['John M. Doe']],
['Do', ['FN'], ['John Doe', 'John M. Doe']],
'check if duplicates are handled correctly' => ['John', ['FN', 'CLOUD'], ['John Doe', 'John M. Doe']],
'case insensitive' => ['john', ['FN'], ['John Doe', 'John M. Doe']]
['John', ['FN'], [['uri0', 'John Doe'], ['uri1', 'John M. Doe']]],
['M. Doe', ['FN'], [['uri1', 'John M. Doe']]],
['Do', ['FN'], [['uri0', 'John Doe'], ['uri1', 'John M. Doe']]],
'check if duplicates are handled correctly' => ['John', ['FN', 'CLOUD'], [['uri0', 'John Doe'], ['uri1', 'John M. Doe']]],
'case insensitive' => ['john', ['FN'], [['uri0', 'John Doe'], ['uri1', 'John M. Doe']]]
];
}

View File

@ -32,6 +32,7 @@ class ContactsManagerTest extends TestCase {
/** @var IManager | \PHPUnit_Framework_MockObject_MockObject $cm */
$cm = $this->getMockBuilder('OCP\Contacts\IManager')->disableOriginalConstructor()->getMock();
$cm->expects($this->exactly(2))->method('registerAddressBook');
$urlGenerator = $this->getMockBuilder('OCP\IUrlGenerator')->disableOriginalConstructor()->getMock();
/** @var CardDavBackend | \PHPUnit_Framework_MockObject_MockObject $backEnd */
$backEnd = $this->getMockBuilder('OCA\DAV\CardDAV\CardDavBackend')->disableOriginalConstructor()->getMock();
$backEnd->method('getAddressBooksForUser')->willReturn([
@ -39,6 +40,6 @@ class ContactsManagerTest extends TestCase {
]);
$app = new ContactsManager($backEnd);
$app->setupContactsProvider($cm, 'user01');
$app->setupContactsProvider($cm, 'user01', $urlGenerator);
}
}

View File

@ -0,0 +1,151 @@
<?php
/**
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @copyright Copyright (c) 2016, ownCloud, Inc.
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\DAV\Tests\unit\CardDAV;
use OCA\DAV\CardDAV\ImageExportPlugin;
use OCP\ILogger;
use Sabre\CardDAV\Card;
use Sabre\DAV\Server;
use Sabre\DAV\Tree;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
use Test\TestCase;
class ImageExportPluginTest extends TestCase {
/** @var ResponseInterface | \PHPUnit_Framework_MockObject_MockObject */
private $response;
/** @var RequestInterface | \PHPUnit_Framework_MockObject_MockObject */
private $request;
/** @var ImageExportPlugin | \PHPUnit_Framework_MockObject_MockObject */
private $plugin;
/** @var Server */
private $server;
/** @var Tree | \PHPUnit_Framework_MockObject_MockObject */
private $tree;
/** @var ILogger | \PHPUnit_Framework_MockObject_MockObject */
private $logger;
function setUp() {
parent::setUp();
$this->request = $this->getMockBuilder('Sabre\HTTP\RequestInterface')->getMock();
$this->response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface')->getMock();
$this->server = $this->getMockBuilder('Sabre\DAV\Server')->getMock();
$this->tree = $this->getMockBuilder('Sabre\DAV\Tree')->disableOriginalConstructor()->getMock();
$this->server->tree = $this->tree;
$this->logger = $this->getMockBuilder('\OCP\ILogger')->getMock();
$this->plugin = $this->getMock('OCA\DAV\CardDAV\ImageExportPlugin', ['getPhoto'], [$this->logger]);
$this->plugin->initialize($this->server);
}
/**
* @dataProvider providesQueryParams
* @param $param
*/
public function testQueryParams($param) {
$this->request->expects($this->once())->method('getQueryParameters')->willReturn($param);
$result = $this->plugin->httpGet($this->request, $this->response);
$this->assertTrue($result);
}
public function providesQueryParams() {
return [
[[]],
[['1']],
[['foo' => 'bar']],
];
}
public function testNotACard() {
$this->request->expects($this->once())->method('getQueryParameters')->willReturn(['photo' => true]);
$this->request->expects($this->once())->method('getPath')->willReturn('/files/welcome.txt');
$this->tree->expects($this->once())->method('getNodeForPath')->with('/files/welcome.txt')->willReturn(null);
$result = $this->plugin->httpGet($this->request, $this->response);
$this->assertTrue($result);
}
/**
* @dataProvider providesCardWithOrWithoutPhoto
* @param bool $expected
* @param array $getPhotoResult
*/
public function testCardWithOrWithoutPhoto($expected, $getPhotoResult) {
$this->request->expects($this->once())->method('getQueryParameters')->willReturn(['photo' => true]);
$this->request->expects($this->once())->method('getPath')->willReturn('/files/welcome.txt');
$card = $this->getMockBuilder('Sabre\CardDAV\Card')->disableOriginalConstructor()->getMock();
$this->tree->expects($this->once())->method('getNodeForPath')->with('/files/welcome.txt')->willReturn($card);
$this->plugin->expects($this->once())->method('getPhoto')->willReturn($getPhotoResult);
if (!$expected) {
$this->response->expects($this->once())->method('setHeader');
$this->response->expects($this->once())->method('setStatus');
$this->response->expects($this->once())->method('setBody');
}
$result = $this->plugin->httpGet($this->request, $this->response);
$this->assertEquals($expected, $result);
}
public function providesCardWithOrWithoutPhoto() {
return [
[true, null],
[false, ['Content-Type' => 'image/jpeg', 'body' => '1234']],
];
}
/**
* @dataProvider providesPhotoData
* @param $expected
* @param $cardData
*/
public function testGetPhoto($expected, $cardData) {
/** @var Card | \PHPUnit_Framework_MockObject_MockObject $card */
$card = $this->getMockBuilder('Sabre\CardDAV\Card')->disableOriginalConstructor()->getMock();
$card->expects($this->once())->method('get')->willReturn($cardData);
$this->plugin = new ImageExportPlugin($this->logger);
$this->plugin->initialize($this->server);
$result = $this->plugin->getPhoto($card);
$this->assertEquals($expected, $result);
}
public function providesPhotoData() {
return [
'empty vcard' => [false, ''],
'vcard without PHOTO' => [false, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nEND:VCARD\r\n"],
'vcard 3 with PHOTO' => [['Content-Type' => 'image/jpeg', 'body' => '12345'], "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nPHOTO;ENCODING=b;TYPE=JPEG:MTIzNDU=\r\nEND:VCARD\r\n"],
//
// TODO: these three below are not working - needs debugging
//
//'vcard 3 with PHOTO URL' => [['Content-Type' => 'image/jpeg', 'body' => '12345'], "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nPHOTO;TYPE=JPEG:http://example.org/photo.jpg\r\nEND:VCARD\r\n"],
//'vcard 4 with PHOTO' => [['Content-Type' => 'image/jpeg', 'body' => '12345'], "BEGIN:VCARD\r\nVERSION:4.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nPHOTO:data:image/jpeg;MTIzNDU=\r\nEND:VCARD\r\n"],
'vcard 4 with PHOTO URL' => [['Content-Type' => 'image/jpeg', 'body' => 'http://example.org/photo.jpg'], "BEGIN:VCARD\r\nVERSION:4.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nPHOTO;MEDIATYPE=image/jpeg:http://example.org/photo.jpg\r\nEND:VCARD\r\n"],
];
}
}