<?php
/**
 * StatusNet - the distributed open-source microblogging tool
 * Copyright (C) 2010, StatusNet, Inc.
 *
 * This class performs lookups based on methods implemented in separate
 * classes, where a resource uri is given. Examples are WebFinger (RFC7033)
 * and the LRDD (Link-based Resource Descriptor Discovery) in RFC6415.
 *
 * PHP version 5
 *
 * 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
 * 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
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @category  Discovery
 * @package   GNUSocial
 * @author    James Walker <james@status.net>
 * @author    Mikael Nordfeldth <mmn@hethane.se>
 * @copyright 2010 StatusNet, Inc.
 * @copyright 2013 Free Software Foundation, Inc.
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
 * @link      http://www.gnu.org/software/social/
 */

if (!defined('GNUSOCIAL')) { exit(1); }

class Discovery
{
    const LRDD_REL    = 'lrdd';
    const UPDATESFROM = 'http://schemas.google.com/g/2010#updates-from';
    const HCARD       = 'http://microformats.org/profile/hcard';

    const JRD_MIMETYPE_OLD = 'application/json';    // RFC6415 uses this
    const JRD_MIMETYPE = 'application/jrd+json';
    const XRD_MIMETYPE = 'application/xrd+xml';

    public $methods = array();

    /**
     * Constructor for a discovery object
     *
     * Registers different discovery methods.
     *
     * @return Discovery this
     */

    public function __construct()
    {
        if (Event::handle('StartDiscoveryMethodRegistration', array($this))) {
            Event::handle('EndDiscoveryMethodRegistration', array($this));
        }
    }

    public static function supportedMimeTypes()
    {
        return array('json'=>self::JRD_MIMETYPE,
                     'jsonold'=>self::JRD_MIMETYPE_OLD,
                     'xml'=>self::XRD_MIMETYPE);
    }

    /**
     * Register a discovery class
     *
     * @param string $class Class name
     *
     * @return void
     */
    public function registerMethod($class)
    {
        $this->methods[] = $class;
    }

    /**
     * Given a user ID, return the first available resource descriptor
     *
     * @param string $id User ID URI
     *
     * @return XML_XRD object for the resource descriptor of the id
     */
    public function lookup($id)
    {
        // Normalize the incoming $id to make sure we have a uri
        $uri = self::normalize($id);

        foreach ($this->methods as $class) {
            try {
                $xrd = new XML_XRD();

                common_debug("LRDD discovery method for '$uri': {$class}");
                $lrdd = new $class;
                $links = call_user_func(array($lrdd, 'discover'), $uri);
                $link = Discovery::getService($links, Discovery::LRDD_REL);

                // Load the LRDD XRD
                if (!empty($link->template)) {
                    $xrd_uri = Discovery::applyTemplate($link->template, $uri);
                } elseif (!empty($link->href)) {
                    $xrd_uri = $link->href;
                } else {
                    throw new Exception('No resource descriptor URI in link.');
                }

                $client  = new HTTPClient();
                $headers = array();
                if (!is_null($link->type)) {
                    $headers[] = "Accept: {$link->type}";
                }

                $response = $client->get($xrd_uri, $headers); 
                if ($response->getStatus() != 200) {
                    throw new Exception('Unexpected HTTP status code.');
                }

                $xrd->loadString($response->getBody());
                return $xrd;
            } catch (Exception $e) {
                continue;
            }
        }

        // TRANS: Exception. %s is an ID.
        throw new Exception(sprintf(_('Unable to find services for %s.'), $id));
    }

    /**
     * Given an array of links, returns the matching service
     *
     * @param array  $links   Links to check (as instances of XML_XRD_Element_Link)
     * @param string $service Service to find
     *
     * @return array $link assoc array representing the link
     */
    public static function getService(array $links, $service)
    {
        foreach ($links as $link) {
            if ($link->rel === $service) {
                return $link;
            }
            common_debug('LINK: rel '.$link->rel.' !== '.$service);
        }

        throw new Exception('No service link found');
    }

    /**
     * Given a "user id" make sure it's normalized to an acct: uri
     *
     * @param string $user_id User ID to normalize
     *
     * @return string normalized acct: URI
     */
    public static function normalize($uri)
    {
        if (is_null($uri) || $uri==='') {
            throw new Exception(_('No resource given.'));
        }

        $parts = parse_url($uri);
        // If we don't have a scheme, but the path implies user@host,
        // though this is far from a perfect matching procedure...
        if (!isset($parts['scheme']) && isset($parts['path'])
                && preg_match('/[\w@\w]/u', $parts['path'])) {
            return 'acct:' . $uri;
        }

        return $uri;
    }

    public static function isAcct($uri)
    {
        return (mb_strtolower(mb_substr($uri, 0, 5)) == 'acct:');
    }

    /**
     * Apply a template using an ID
     *
     * Replaces {uri} in template string with the ID given.
     *
     * @param string $template Template to match
     * @param string $uri      URI to replace with
     *
     * @return string replaced values
     */
    public static function applyTemplate($template, $uri)
    {
        $template = str_replace('{uri}', urlencode($uri), $template);

        return $template;
    }
}