Wiki

Clone wiki

django-portablecontacts / Home

Portable Contacts

The Portable Contacts specification is designed to make it easier for developers to give their users a secure way to access the address books and friends lists they have built up all over the web. Specifically, it seeks to create a common access pattern and contact schema that any site can provide, well-specified authentication and access rules, standard libraries that can work with any site, and absolutely minimal complexity, with the lightest possible toolchain requirements for developers.

Workflow Overview

A Consumer wishing to access a user's data via Portable Contacts must start with an Initial Identifier for the Service Provider containing the user's data, usually provided by the user. In many cases, this may be the domain name of the Service Provider's web site, such as sample.site.org, but may be a more specific URL, such as the OpenID identifier of the user, if available. Consumers then perform Discovery on the Initial Identifier to determine where the Portable Contacts endpoint for this Service Provider resides. If successful, the Consumer may then attempt to request information from that endpoint. If the endpoint contains private data, the Service Provider will return an authorization challenge, and the Consumer must then guide the user through an appropriate authorization flow to obtain the credentials necessary to access this private data. Upon successful authorization, the Consumer may request data from the Portable Contacts endpoint using these authorization credentials. Whether accessing public or private data, Consumers may request a specific subset of the user's data using standard Query Parameters. Upon a successful request, the data is returned in the response, and the Consumer may then parse the response data and use it as desired. The following sections detail each of these steps.

Installation

There are 3 basic steps in order to install this Django application, edit your settings and:

  • Add portablecontacts to your INSTALLED_APPS

  • Add portablecontacts.middlewares.PortableContactsMiddleware to your MIDDLEWARE_CLASSES (there is no particular order for now)

  • Add a SERIALIZATION_MODULES setting like that:

    SERIALIZATION_MODULES = {
        "poco-xml"    : "portablecontacts.serializers.xml_serializer",
        "poco-json"   : "portablecontacts.serializers.json",
    }
    

That's it! Optionally, you can specify your own Contact model class with PORTABLE_CONTACTS_MODEL setting, for instance:

PORTABLE_CONTACTS_MODEL = 'yourapp.CustomContact'

Note that some fields are required by the specification. A complete example of what can be done is available in portablecontacts_example folder.

Initialization

As a Django app, we need to create a user and contacts in order to test (as described in Appendix A of specifications):

>>> from django.contrib.auth.models import User
>>> david = User.objects.create_user(username='david',
...     email='david@example.com', password='toto')
>>> from portablecontacts.models import Contact
>>> minimal_contact = Contact.objects.create(userName='Minimal Contact',
...     user=david)
>>> minimal_contact
<Contact: Minimal Contact>
>>> mork = Contact.objects.create(familyName='Hashimoto',
...     givenName='Mork', # TODO: add more info
...     user=david)
>>> mork
<Contact: Mork Hashimoto>

Now let's add some information to this contact:

>>> from portablecontacts.models import ContactEmail
>>> from portablecontacts.constants import WORK, HOME, OTHER, MOBILE, AIM, \
...     THUMBNAIL
>>> contact_email_work = ContactEmail.objects.create(type=WORK,
...     value='mhashimoto-04@plaxo.com', primary=True, contact=mork)
>>> contact_email_work
<ContactEmail: mhashimoto-04@plaxo.com (work) for Mork Hashimoto>
>>> contact_email_home = ContactEmail.objects.create(type=HOME,
...     value='mhashimoto-04@plaxo.com', contact=mork)
>>> contact_email_home
<ContactEmail: mhashimoto-04@plaxo.com (home) for Mork Hashimoto>

>>> from portablecontacts.models import ContactUrl
>>> contact_url_work = ContactUrl.objects.create(type=WORK,
...     value='http://www.seeyellow.com', contact=mork)
>>> contact_url_work
<ContactUrl: http://www.seeyellow.com (work) for Mork Hashimoto>
>>> contact_url_home = ContactUrl.objects.create(type=HOME,
...     value='http://www.angryalien.com', contact=mork)
>>> contact_url_home
<ContactUrl: http://www.angryalien.com (home) for Mork Hashimoto>

>>> from portablecontacts.models import ContactPhoneNumber
>>> contact_phone_work = ContactPhoneNumber.objects.create(type=WORK,
...     value='KLONDIKE5', contact=mork)
>>> contact_phone_work
<ContactPhoneNumber: KLONDIKE5 (work) for Mork Hashimoto>
>>> contact_phone_mobile = ContactPhoneNumber.objects.create(type=MOBILE,
...     value='650-123-4567', contact=mork)
>>> contact_phone_mobile
<ContactPhoneNumber: 650-123-4567 (mobile) for Mork Hashimoto>

>>> from portablecontacts.models import ContactIm
>>> contact_im = ContactIm.objects.create(type=AIM,
...     value='plaxodev8', contact=mork)
>>> contact_im
<ContactIm: plaxodev8 (aim) for Mork Hashimoto>

>>> from portablecontacts.models import ContactPhoto
>>> contact_photo = ContactPhoto.objects.create(type=THUMBNAIL,
...     value='http://sample.site.org/photos/12345.jpg', contact=mork)
>>> contact_photo
<ContactPhoto: http://sample.site.org/photos/12345.jpg (thumbnail) for Mork Hashimoto>

>>> from portablecontacts.models import ContactTag
>>> contact_tag = ContactTag.objects.create(
...     value='plaxo guy', contact=mork)
>>> contact_tag
<ContactTag: plaxo guy for Mork Hashimoto>

>>> from portablecontacts.models import ContactRelationship
>>> contact_relationship = ContactRelationship.objects.create(
...     value='friend', contact=mork)
>>> contact_relationship
<ContactRelationship: friend for Mork Hashimoto>

>>> from portablecontacts.models import ContactAddress
>>> contact_address = ContactAddress.objects.create(type=HOME,
...     streetAddress='742 Evergreen Terrace\nSuite 123',
...     locality='Springfield', region='VT', postalCode='12345',
...     country='USA', contact=mork)
>>> contact_address
<ContactAddress: 742 Evergreen Terrace
Suite 123
Springfield VT 12345 USA>

>>> from portablecontacts.models import ContactOrganization
>>> contact_organization = ContactOrganization.objects.create(
...     name='Burns Worldwide', title='Head Bee Guy', contact=mork)
>>> contact_organization
<ContactOrganization: Burns Worldwide (Head Bee Guy) for Mork Hashimoto>

>>> from portablecontacts.models import ContactAccount
>>> contact_account = ContactAccount.objects.create(
...     domain='plaxo.com', userid='2706', contact=mork)
>>> contact_account
<ContactAccount: 2706 (plaxo.com) for Mork Hashimoto>

We need a contact for self too:

>>> self_contact = Contact.objects.create(preferredUsername='david',
...     familyName='Larlet', givenName='David',
...     user=david)
>>> self_contact
<Contact: David Larlet>

Discovery

Portable Contacts API endpoint is discoverable from the domain root using XRDS-Simple (Hammer-Lahav, E., "XRDS-Simple 1.0," .) (previously known as YADIS). The API is identified by the Service Type http://portablecontacts.net/spec/1.0 and the corresponding URI is the Base URL for the API. The Base URL MUST NOT contain any query string, as additional path information and query string variables MAY be appended by Consumers as part of forming the request (as described in detail below):

>>> from django.test.client import Client
>>> c = Client()
>>> response = c.get("/", **{'HTTP_ACCEPT': 'application/xrds+xml'})
>>> print response.content
<XRDS xmlns="xri://$xrds">
  <XRD xmlns:simple="http://xrds-simple.net/core/1.0"
   xmlns="xri://$XRD*($v*2.0)" version="2.0">
    <Type>xri://$xrds*simple</Type>
    <Service>
      <Type>http://portablecontacts.net/spec/1.0</Type>
      <URI>http://example.com/portablecontacts/</URI>
    </Service>
  </XRD>
</XRDS>

Available Authorization Methods

When accessing a Portable Contacts endpoint, if sufficient authorization credentials are not provided, the Service Provider SHOULD return a 401 Unauthorized response, and SHOULD provide the available Authorization mechanisms available by including WWW-Authenticate headers in the response for each type of Authorization method supported (as defined in [RFC2616] (Fielding, R., "Hypertext Transfer Protocol -- HTTP/1.1," .), section 14.47. Consumers will then be able to recognize that the API is a protected resource and initiate the proper Authorization process needed to obtain the appropriate credentials:

>>> c = Client()
>>> response = c.get("/portablecontacts/")
>>> response.status_code
401
>>> response._headers['www-authenticate']
('WWW-Authenticate', 'WWW-Authenticate: OAuth realm="example.com/portablecontacts/"')

Additional Path Information

A request using the Base URL alone MUST yield a result, assuming that adequate authorization credentials are provided. In addition, Consumers MAY append additional path information to the Base URL to request more specific information. Service Providers MUST recognize the following additional path information when appended to the Base URL, and MUST return the corresponding data:

  • /me/all -- Return all contact info (equivalent to providing no additional path info)

  • /me/all/{id} -- Only return contact info for the contact whose id value is equal to the provided {id}, if such a contact exists. In this case, the response format is the same as when requesting all contacts, but any contacts not matching the requested ID MUST be filtered out of the result list by the Service Provider

  • /me/Soul, Inc. -- Return contact info for the owner of this information, i.e. the user on whose behalf this request is being made. In this case, the response format is the same as when requesting all contacts, but any contacts not matching the requested ID MUST be filtered out of the result list by the Service Provider.

    >>> c = Client()
    >>> c.login(username='david', password='toto')
    True
    >>> response = c.get("/portablecontacts/@me/@all/")
    >>> response.status_code
    200
    >>> print response.content
    {
     "itemsPerPage": 100,
     "startIndex": 0,
     "totalResults": 3,
     "entry": [
      {
       "displayName": "Minimal Contact",
       "id": 1
      },
      {
       "relationships": [
        "friend"
       ],
       "organizations": [
        {
         "name": "Burns Worldwide",
         "title": "Head Bee Guy"
        }
       ],
       "phoneNumbers": [
        {
         "type": "work",
         "value": "KLONDIKE5"
        },
        {
         "type": "mobile",
         "value": "650-123-4567"
        }
       ],
       "displayName": "Mork Hashimoto",
       "name": {
        "givenName": "Mork",
        "familyName": "Hashimoto"
       },
       "tags": [
        "plaxo guy"
       ],
       "emails": [
        {
         "type": "work",
         "primary": true,
         "value": "mhashimoto-04@plaxo.com"
        },
        {
         "type": "home",
         "value": "mhashimoto-04@plaxo.com"
        }
       ],
       "photos": [
        {
         "type": "thumbnail",
         "value": "http://sample.site.org/photos/12345.jpg"
        }
       ],
       "ims": [
        {
         "type": "aim",
         "value": "plaxodev8"
        }
       ],
       "accounts": [
        {
         "domain": "plaxo.com",
         "userid": "2706"
        }
       ],
       "urls": [
        {
         "type": "work",
         "value": "http://www.seeyellow.com"
        },
        {
         "type": "home",
         "value": "http://www.angryalien.com"
        }
       ],
       "id": 2,
       "addresses": [
        {
         "locality": "Springfield",
         "country": "USA",
         "region": "VT",
         "formatted": "742 Evergreen Terrace\nSuite 123\nSpringfield VT 12345 USA",
         "streetAddress": "742 Evergreen Terrace\nSuite 123",
         "postalCode": "12345",
         "type": "home"
        }
       ]
      },
      {
       "preferredUsername": "david",
       "displayName": "David Larlet",
       "id": 3,
       "name": {
        "givenName": "David",
        "familyName": "Larlet"
       }
      }
     ]
    }
    >>> response = c.get("/portablecontacts/@me/@all/1/")
    >>> response.status_code
    200
    >>> print response.content
    {
     "entry": [
      {
       "displayName": "Minimal Contact",
       "id": 1
      }
     ]
    }
    >>> response = c.get("/portablecontacts/@me/@self/")
    >>> response.status_code
    200
    >>> print response.content
    {
     "entry": {
      "preferredUsername": "david",
      "displayName": "David Larlet",
      "name": {
       "givenName": "David",
       "familyName": "Larlet"
      },
      "id": 3
     }
    }
    

Presentations

fields, if non-empty, each contact returned SHALL contain only the fields explicitly requested. Service Provider MAY return a subset of the requested fields if they are not supported. This field is used for efficiency when the client only wishes to access a subset of the fields normally returned in results. Value is a comma separated list of top level field names (e.g. id,name,emails,photos) and defaults to an empty list which means it's up to the Provider which fields to return. Consumers may request all available fields to be returned by using the special value all:

>>> response = c.get("/portablecontacts/@me/@all/", {'fields': 'id,familyName'})
>>> response.status_code
200
>>> print response.content
{
 "itemsPerPage": 100,
 "startIndex": 0,
 "totalResults": 3,
 "entry": [
  {
   "id": 1
  },
  {
   "id": 2,
   "name": {
    "familyName": "Hashimoto"
   }
  },
  {
   "id": 3,
   "name": {
    "familyName": "Larlet"
   }
  }
 ]
}

format, specifies the format in which the response data is returned. Service Providers MUST support the values json for JSON (http://json.org) and xml for XML (http://www.w3.org/XML/) and MAY support additional formats if desired. The format defaults to json if no format is specified. The data structure returned is equivalent in both formats; the only difference is in the encoding of the data. Singular Fields are encoded as string key/value pairs in JSON and tags with text content in XML, e.g. "field": "value" and <field>value</field> respectively. Plural Fields and Plural Bundles are encoded as arrays in JSON and repeated tags in XML, e.g. "fields": [ "value1", "value2" ] and <fields>value1</field><fields<value2</field> respectively. Nodes with multiple sub-nodes are represented as objects in JSON and tags with sub-tags in XML, e.g. "field": { "subfield1": "value1", "subfield2": "value2" } and <field><subfield1><value1></subfield1><subfield2> value2</subfield2></field> respectively:

>>> response = c.get("/portablecontacts/@me/@all/", {'format': 'xml'})
>>> response.status_code
200
>>> print response.content
<?xml version="1.0" encoding="utf-8"?>
<response>
 <startIndex>0</startIndex>
 <itemsPerPage>100</itemsPerPage>
 <totalResults>3</totalResults>
 <entry>
  <id>1</id>
  <displayName>Minimal Contact</displayName>
 </entry>
 <entry>
  <id>2</id>
  <displayName>Mork Hashimoto</displayName>
  <name>
   <familyName>Hashimoto</familyName>
   <givenName>Mork</givenName>
  </name>
  <emails>
   <value>mhashimoto-04@plaxo.com</value>
   <type>work</type>
   <primary>true</primary>
  </emails>
  <emails>
   <value>mhashimoto-04@plaxo.com</value>
   <type>home</type>
  </emails>
  <urls>
   <value>http://www.seeyellow.com</value>
   <type>work</type>
  </urls>
  <urls>
   <value>http://www.angryalien.com</value>
   <type>home</type>
  </urls>
  <phoneNumbers>
   <value>KLONDIKE5</value>
   <type>work</type>
  </phoneNumbers>
  <phoneNumbers>
   <value>650-123-4567</value>
   <type>mobile</type>
  </phoneNumbers>
  <ims>
   <value>plaxodev8</value>
   <type>aim</type>
  </ims>
  <photos>
   <value>http://sample.site.org/photos/12345.jpg</value>
   <type>thumbnail</type>
  </photos>
  <tags>
   <value>plaxo guy</value>
  </tags>
  <relationships>
   <value>friend</value>
  </relationships>
  <addresses>
   <formatted>742 Evergreen Terrace
Suite 123
Springfield VT 12345 USA</formatted>
   <type>home</type>
   <streetAddress>742 Evergreen Terrace
Suite 123</streetAddress>
   <locality>Springfield</locality>
   <region>VT</region>
   <postalCode>12345</postalCode>
   <country>USA</country>
  </addresses>
  <organizations>
   <name>Burns Worldwide</name>
   <title>Head Bee Guy</title>
  </organizations>
  <accounts>
   <domain>plaxo.com</domain>
   <userid>2706</userid>
  </accounts>
 </entry>
 <entry>
  <id>3</id>
  <displayName>David Larlet</displayName>
  <name>
   <familyName>Larlet</familyName>
   <givenName>David</givenName>
  </name>
  <preferredUsername>david</preferredUsername>
 </entry>
</response>

Sorting

Sorting allows requests to specify the order in which contacts are returned.

sortBy, specifies the field name whose value SHALL be used to order the returned Contacts. The sort order is determine by the sortOrder parameter. If sortBy is a Singular Field, contacts are sorted according to that field's value; if it's a Plural Field, contacts are sorted by the Value (or Major Value, if it's a Complex Field) of the field marked with "primary": "true", if any, or else the first value in the list, if any, or else they are sorted last if the given contact has no data for the given field:

>>> response = c.get("/portablecontacts/@me/@all/", {'sortBy': 'givenName'})
>>> response.status_code
200
>>> print response.content
{
 "itemsPerPage": 100,
 "startIndex": 0,
 "totalResults": 3,
 "entry": [
  {
   "displayName": "Minimal Contact",
   "id": 1
  },
  {
   "preferredUsername": "david",
   "displayName": "David Larlet",
   "id": 3,
   "name": {
    "givenName": "David",
    "familyName": "Larlet"
   }
  },
  {
   "relationships": [
    "friend"
   ],
   "organizations": [
    {
     "name": "Burns Worldwide",
     "title": "Head Bee Guy"
    }
   ],
   "phoneNumbers": [
    {
     "type": "work",
     "value": "KLONDIKE5"
    },
    {
     "type": "mobile",
     "value": "650-123-4567"
    }
   ],
   "displayName": "Mork Hashimoto",
   "name": {
    "givenName": "Mork",
    "familyName": "Hashimoto"
   },
   "tags": [
    "plaxo guy"
   ],
   "emails": [
    {
     "type": "work",
     "primary": true,
     "value": "mhashimoto-04@plaxo.com"
    },
    {
     "type": "home",
     "value": "mhashimoto-04@plaxo.com"
    }
   ],
   "photos": [
    {
     "type": "thumbnail",
     "value": "http://sample.site.org/photos/12345.jpg"
    }
   ],
   "ims": [
    {
     "type": "aim",
     "value": "plaxodev8"
    }
   ],
   "accounts": [
    {
     "domain": "plaxo.com",
     "userid": "2706"
    }
   ],
   "urls": [
    {
     "type": "work",
     "value": "http://www.seeyellow.com"
    },
    {
     "type": "home",
     "value": "http://www.angryalien.com"
    }
   ],
   "id": 2,
   "addresses": [
    {
     "locality": "Springfield",
     "country": "USA",
     "region": "VT",
     "formatted": "742 Evergreen Terrace\nSuite 123\nSpringfield VT 12345 USA",
     "streetAddress": "742 Evergreen Terrace\nSuite 123",
     "postalCode": "12345",
     "type": "home"
    }
   ]
  }
 ]
}

sortOrder, the order in which the sortBy parameter is applied. Allowed values are ascending and descending. If a value for sortBy is provided and no sortOrder is specifies, the sortOrder SHALL default to ascending. Sort order is expected to be case-insensitive Unicode alphabetic sort order, with no specific locale implied:

>>> response = c.get("/portablecontacts/@me/@all/", {'sortBy': 'givenName', 'sortOrder': 'descending'})
>>> response.status_code
200
>>> print response.content
{
 "itemsPerPage": 100,
 "startIndex": 0,
 "totalResults": 3,
 "entry": [
  {
   "relationships": [
    "friend"
   ],
   "organizations": [
    {
     "name": "Burns Worldwide",
     "title": "Head Bee Guy"
    }
   ],
   "phoneNumbers": [
    {
     "type": "work",
     "value": "KLONDIKE5"
    },
    {
     "type": "mobile",
     "value": "650-123-4567"
    }
   ],
   "displayName": "Mork Hashimoto",
   "name": {
    "givenName": "Mork",
    "familyName": "Hashimoto"
   },
   "tags": [
    "plaxo guy"
   ],
   "emails": [
    {
     "type": "work",
     "primary": true,
     "value": "mhashimoto-04@plaxo.com"
    },
    {
     "type": "home",
     "value": "mhashimoto-04@plaxo.com"
    }
   ],
   "photos": [
    {
     "type": "thumbnail",
     "value": "http://sample.site.org/photos/12345.jpg"
    }
   ],
   "ims": [
    {
     "type": "aim",
     "value": "plaxodev8"
    }
   ],
   "accounts": [
    {
     "domain": "plaxo.com",
     "userid": "2706"
    }
   ],
   "urls": [
    {
     "type": "work",
     "value": "http://www.seeyellow.com"
    },
    {
     "type": "home",
     "value": "http://www.angryalien.com"
    }
   ],
   "id": 2,
   "addresses": [
    {
     "locality": "Springfield",
     "country": "USA",
     "region": "VT",
     "formatted": "742 Evergreen Terrace\nSuite 123\nSpringfield VT 12345 USA",
     "streetAddress": "742 Evergreen Terrace\nSuite 123",
     "postalCode": "12345",
     "type": "home"
    }
   ]
  },
  {
   "preferredUsername": "david",
   "displayName": "David Larlet",
   "id": 3,
   "name": {
    "givenName": "David",
    "familyName": "Larlet"
   }
  },
  {
   "displayName": "Minimal Contact",
   "id": 1
  }
 ]
}

Pagination

The pagination parameters can be used together to "page through" a large number of results in manageable chunks.

For instance, on an initial query, specifying startIndex=0&count=1 will return only the first result. The total number of possible results is indicated by the totalResults field of results, so the client knows how many "pages" of results exist:

>>> response = c.get("/portablecontacts/@me/@all/", {'startIndex':0, 'count':1})
>>> response.status_code
200
>>> print response.content
{
 "itemsPerPage": 1,
 "startIndex": 0,
 "totalResults": 3,
 "entry": [
  {
   "displayName": "Minimal Contact",
   "id": 1
  }
 ]
}

A subsequent query of startIndex=2&count=1 will return the next next result, and so on:

>>> response = c.get("/portablecontacts/@me/@all/", {'startIndex':2, 'count':1})
>>> response.status_code
200
>>> print response.content
{
 "itemsPerPage": 1,
 "startIndex": 2,
 "totalResults": 3,
 "entry": [
  {
   "preferredUsername": "david",
   "displayName": "David Larlet",
   "id": 3,
   "name": {
    "givenName": "David",
    "familyName": "Larlet"
   }
  }
 ]
}

Updated

Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.