text: define(
    ['./platform', './util'],
    function (platform, util) {
  
      "use strict";
  
      // Namespace: webfinger
      //
      // Webfinger discovery.
      // Supports XRD, JRD and the <"resource" parameter at http://tools.ietf.org/html/draft-jones-appsawg-webfinger-06#section-5.2>
      //
      // remoteStorage.js tries the following things to discover a user's profile:
      //   * via HTTPS to /.well-known/host-meta.json
      //   * via HTTPS to /.well-known/host-meta
      //   * via HTTP to /.well-known/host-meta.json
      //   * via HTTP to /.well-known/host-meta
      //
      //   All those requests carry the "resource" query parameter.
      //
      // So in order for a discovery to work most quickly, a server should
      // respond to HTTPS requests like:
      //
      //   > /.well-known/host-meta.json?resource=acct%3Abob%40example.com
      // And return a JSON representation of the profile, such as this:
      //
      //   (start code)
      //
      //   {
      //     links:[{
      //       href: 'https://example.com/storage/bob',
      //       rel: "remoteStorage",
      //       type: "https://www.w3.org/community/rww/wiki/read-write-web-00#simple",
      //       properties: {
      //         'auth-method': "https://tools.ietf.org/html/draft-ietf-oauth-v2-26#section-4.2",
      //         'auth-endpoint': 'https://example.com/auth/bob'
      //       }
      //     }]
      //   }
      //
      //   (end code)
      //
  
      var logger = util.getLogger('webfinger');
  
      var timeout = 10000;
  
        ///////////////
       // Webfinger //
      ///////////////
  
      // PARSE
  
      // parse user address
      function extractHostname(userAddress) {
        var parts = userAddress.toLowerCase().split('@');
        var error;
        if(parts.length < 2) {
          error = 'no-at';
        } else if(parts.length > 2) {
          error = 'multiple-at';
        } else {
          if(!(/^[\.0-9a-z\-\_]+$/.test(parts[0]))) {
            error = 'non-dotalphanum';
          } else if(!(/^[\.0-9a-z\-]+$/.test(parts[1]))) {
            error = 'non-dotalphanum';
          }
        }
        if(error) {
          throw error;
        }
        return parts[1];
      }
  
      function parseXRD(str) {
        return util.getPromise(function(promise) {
          platform.parseXml(str, function(err, obj) {
            if(err) {
              promise.reject(err);
            } else {
              if(obj && obj.Link) {
                var links = {};
                if(obj.Link && obj.Link['@']) {//obj.Link is one element
                  if(obj.Link['@'].rel) {
                    links[obj.Link['@'].rel]=obj.Link['@'];
                  }
                } else {//obj.Link is an array
                  for(var i=0; i<obj.Link.length; i++) {
                    if(obj.Link[i]['@'] && obj.Link[i]['@'].rel) {
                      links[obj.Link[i]['@'].rel]=obj.Link[i]['@'];
                    }
                  }
                }
                promise.fulfill(links);
              } else {
                promise.reject('invalid-xml');
              }
            }
          });
        });
      }
  
      function parseJRD(data) {
        var object = JSON.parse(data);
        if(! object.links) {
          throw 'invalid-jrd';
        }
        var links = {};
        object.links.forEach(function(link) {
          // just take the first one of each rel:
          if(link.rel && (! links[link.rel])) {
            links[link.rel] = link;
          }
        });
        return links;
      }
  
      // request a single profile
      function fetchProfile(address) {
        logger.info('fetch profile', address);
        return platform.ajax({
          url: address,
          timeout: timeout
        }).then(function(body, headers) {
          var mimeType = headers && headers['content-type'] && headers['content-type'].split(';')[0];
          logger.debug('fetched', body, mimeType);
          if(mimeType && mimeType.match(/^application\/json/)) {
            return parseJRD(body);
          } else {
            return util.getPromise(function(jrdPromise) {
              parseXRD(body).then(
                function(xrd) {
                  jrdPromise.fulfill(xrd);
                }, function(error) {
                  jrdPromise.fulfill(parseJRD(body));
                });
            });
          }
        });
      }
  
      // fetch profile from all given addresses and yield the first one that
      // succeeds.
      function fetchHostMeta(protocol, addresses) {
        addresses = addresses.map(function(addr) {
          return protocol + addr;
        });
        return util.asyncMap(addresses, fetchProfile).
          then(function(profiles, errors) {
            logger.debug('host meta mapped', profiles);
            for(var i=0;i<profiles.length;i++) {
              if(profiles[i]) {
                return profiles[i];
              }
            }
            // if any of the requests failed due to a timeout, that's our
            // reason as well.
            for(var j=0;j<errors.length;j++) {
              if(errors[j] === 'timeout') {
                throw "timeout";
              }
            }
            // otherwise we just fail with a generic reason.
            throw 'requests-failed';
          });
      }
  
      function extractRemoteStorageLink(links) {
        logger.debug('extract remoteStorage link', links);
        var remoteStorageLink = links.remoteStorage || links.remotestorage;
        var lrddLink;
        if(remoteStorageLink) {
          logger.info('remoteStorageLink', remoteStorageLink);
          if(remoteStorageLink.href &&
             remoteStorageLink.type &&
             remoteStorageLink.properties &&
             remoteStorageLink.properties['auth-endpoint']) {
            return remoteStorageLink;
          } else {
            throw new Error("Invalid remoteStorage link. Required properties are:" +
                            "href, type, properties, properties.auth-endpoint. " +
                            JSON.stringify(remoteStorageLink));
          }
        } else if((lrddLink = links.lrdd) && links.lrdd.template) {
          return fetchProfile(
            lrddLink.template.replace('{uri}', 'acct:' + userAddress)
          ).then(extractRemoteStorageLink);
        } else {
          throw 'not-supported';
        }
      }
  
      // method: getStorageInfo
      // Get the storage information of a given user address.
      //
      // Parameters:
      //   userAddress - a string in the form user@host
      //
      // Callback parameters:
      //   err         - either an error message or null if discovery succeeded
      //   storageInfo - the format is equivalent to that of the JSON representation of the remotestorage link (see above)
      //
      // Returns:
      //   A promise for the user's webfinger profile
      //
      function getStorageInfo(userAddress) {
  
        /*
  
          - validate userAddres
          - fetch host-meta
          - parse host-meta
          - (optionally) fetch lrdd
          - (optionally) parse lrdd
          - extract links
  
         */
  
        return util.getPromise(function(promise) {
          try {
            var hostname = extractHostname(userAddress)
          } catch(error) {
            if(error) {
              return promise.reject(error);
            }
          }
          var query = '?resource=acct:' + encodeURIComponent(userAddress);
          var addresses = [
            '://' + hostname + '/.well-known/webfinger' + query,
            '://' + hostname + '/.well-known/host-meta.json' + query,
            '://' + hostname + '/.well-known/host-meta' + query,
          ];
  
          fetchHostMeta('https', addresses).
            then(extractRemoteStorageLink, function() {
              return fetchHostMeta('http', addresses).
                then(extractRemoteStorageLink);
            }).
            then(function(profile) {
              promise.fulfill(profile);
            }, promise.reject);
        });
      }
  
      return {
        getStorageInfo: getStorageInfo,
  
        setTimeout: function(t) {
          timeout = t;
        },
  
        getTimeout: function() {
          return timeout;
        }
      };
  });
  