summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--plugins/OStatus/actions/ostatussub.php5
-rw-r--r--plugins/OStatus/classes/Ostatus_profile.php233
-rw-r--r--plugins/OStatus/lib/discovery.php78
-rw-r--r--plugins/OStatus/lib/discoveryhints.php182
-rw-r--r--plugins/OStatus/lib/feeddiscovery.php4
-rw-r--r--plugins/OStatus/lib/linkheader.php63
6 files changed, 401 insertions, 164 deletions
diff --git a/plugins/OStatus/actions/ostatussub.php b/plugins/OStatus/actions/ostatussub.php
index 65dee2392..07081c2c6 100644
--- a/plugins/OStatus/actions/ostatussub.php
+++ b/plugins/OStatus/actions/ostatussub.php
@@ -149,7 +149,7 @@ class OStatusSubAction extends Action
$fullname = $entity->fullname;
$homepage = $entity->homepage;
$location = $entity->location;
-
+
if (!$avatar) {
$avatar = Avatar::defaultImage(AVATAR_PROFILE_SIZE);
}
@@ -242,7 +242,7 @@ class OStatusSubAction extends Action
if (Validate::email($this->profile_uri)) {
$this->oprofile = Ostatus_profile::ensureWebfinger($this->profile_uri);
} else if (Validate::uri($this->profile_uri)) {
- $this->oprofile = Ostatus_profile::ensureProfile($this->profile_uri);
+ $this->oprofile = Ostatus_profile::ensureProfileURL($this->profile_uri);
} else {
$this->error = _m("Sorry, we could not reach that address. Please make sure that the OStatus address is like nickname@example.com or http://example.net/nickname");
common_debug('Invalid address format.', __FILE__);
@@ -339,7 +339,6 @@ class OStatusSubAction extends Action
}
}
-
/**
* Handle posts to this form
*
diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php
index 6ae8e4fd5..73f5d2322 100644
--- a/plugins/OStatus/classes/Ostatus_profile.php
+++ b/plugins/OStatus/classes/Ostatus_profile.php
@@ -708,18 +708,122 @@ class Ostatus_profile extends Memcached_DataObject
* @return Ostatus_profile
* @throws FeedSubException
*/
- public static function ensureProfile($profile_uri, $hints=array())
+
+ public static function ensureProfileURL($profile_url, $hints=array())
{
- // Get the canonical feed URI and check it
+ $oprofile = self::getFromProfileURL($profile_url);
+
+ if (!empty($oprofile)) {
+ return $oprofile;
+ }
+
+ $hints['profileurl'] = $profile_url;
+
+ // Fetch the URL
+ // XXX: HTTP caching
+
+ $client = new HTTPClient();
+ $client->setHeader('Accept', 'text/html,application/xhtml+xml');
+ $response = $client->get($profile_url);
+
+ if (!$response->isOk()) {
+ return null;
+ }
+
+ // Check if we have a non-canonical URL
+
+ $finalUrl = $response->getUrl();
+
+ if ($finalUrl != $profile_url) {
+
+ $hints['profileurl'] = $finalUrl;
+
+ $oprofile = self::getFromProfileURL($finalUrl);
+
+ if (!empty($oprofile)) {
+ return $oprofile;
+ }
+ }
+
+ // Try to get some hCard data
+
+ $body = $response->getBody();
+
+ $hcardHints = DiscoveryHints::hcardHints($body, $finalUrl);
+
+ if (!empty($hcardHints)) {
+ $hints = array_merge($hints, $hcardHints);
+ }
+
+ // Check if they've got an LRDD header
+
+ $lrdd = LinkHeader::getLink($response, 'lrdd', 'application/xrd+xml');
+
+ if (!empty($lrdd)) {
+
+ $xrd = Discovery::fetchXrd($lrdd);
+ $xrdHints = DiscoveryHints::fromXRD($xrd);
+
+ $hints = array_merge($hints, $xrdHints);
+ }
+
+ // If discovery found a feedurl (probably from LRDD), use it.
+
+ if (array_key_exists('feedurl', $hints)) {
+ return self::ensureFeedURL($hints['feedurl'], $hints);
+ }
+
+ // Get the feed URL from HTML
+
$discover = new FeedDiscovery();
- if (isset($hints['feedurl'])) {
- $feeduri = $hints['feedurl'];
- $feeduri = $discover->discoverFromFeedURL($feeduri);
- } else {
- $feeduri = $discover->discoverFromURL($profile_uri);
- $hints['feedurl'] = $feeduri;
+
+ $feedurl = $discover->discoverFromHTML($finalUrl, $body);
+
+ if (!empty($feedurl)) {
+ $hints['feedurl'] = $feedurl;
+
+ return self::ensureFeedURL($feedurl, $hints);
+ }
+ }
+
+ static function getFromProfileURL($profile_url)
+ {
+ $profile = Profile::staticGet('profileurl', $profile_url);
+
+ if (empty($profile)) {
+ return null;
+ }
+
+ // Is it a known Ostatus profile?
+
+ $oprofile = Ostatus_profile::staticGet('profile_id', $profile->id);
+
+ if (!empty($oprofile)) {
+ return $oprofile;
}
+ // Is it a local user?
+
+ $user = User::staticGet('id', $profile->id);
+
+ if (!empty($user)) {
+ throw new Exception("'$profile_url' is the profile for local user '{$user->nickname}'.");
+ }
+
+ // Continue discovery; it's a remote profile
+ // for OMB or some other protocol, may also
+ // support OStatus
+
+ return null;
+ }
+
+ public static function ensureFeedURL($feed_url, $hints=array())
+ {
+ $discover = new FeedDiscovery();
+
+ $feeduri = $discover->discoverFromFeedURL($feed_url);
+ $hints['feedurl'] = $feeduri;
+
$huburi = $discover->getAtomLink('hub');
$hints['hub'] = $huburi;
$salmonuri = $discover->getAtomLink(Salmon::NS_REPLIES);
@@ -1303,7 +1407,7 @@ class Ostatus_profile extends Memcached_DataObject
}
}
- // First, look it up
+ // Try looking it up
$oprofile = Ostatus_profile::staticGet('uri', 'acct:'.$addr);
@@ -1317,7 +1421,7 @@ class Ostatus_profile extends Memcached_DataObject
$disco = new Discovery();
try {
- $result = $disco->lookup($addr);
+ $xrd = $disco->lookup($addr);
} catch (Exception $e) {
// Save negative cache entry so we don't waste time looking it up again.
// @fixme distinguish temporary failures?
@@ -1327,38 +1431,26 @@ class Ostatus_profile extends Memcached_DataObject
$hints = array('webfinger' => $addr);
- foreach ($result->links as $link) {
- switch ($link['rel']) {
- case Discovery::PROFILEPAGE:
- $hints['profileurl'] = $profileUrl = $link['href'];
- break;
- case Salmon::NS_REPLIES:
- $hints['salmon'] = $salmonEndpoint = $link['href'];
- break;
- case Discovery::UPDATESFROM:
- $hints['feedurl'] = $feedUrl = $link['href'];
- break;
- case Discovery::HCARD:
- $hcardUrl = $link['href'];
- break;
- default:
- common_log(LOG_NOTICE, "Don't know what to do with rel = '{$link['rel']}'");
- break;
- }
- }
+ $dhints = DiscoveryHints::fromXRD($xrd);
+
+ $hints = array_merge($hints, $dhints);
+
+ // If there's an Hcard, let's grab its info
- if (isset($hcardUrl)) {
- $hcardHints = self::slurpHcard($hcardUrl);
- // Note: Webfinger > hcard
- $hints = array_merge($hcardHints, $hints);
+ if (array_key_exists('hcard', $hints)) {
+ if (!array_key_exists('profileurl', $hints) ||
+ $hints['hcard'] != $hints['profileurl']) {
+ $hcardHints = DiscoveryHints::fromHcardUrl($hints['hcard']);
+ $hints = array_merge($hcardHints, $hints);
+ }
}
// If we got a feed URL, try that
- if (isset($feedUrl)) {
+ if (array_key_exists('feedurl', $hints)) {
try {
common_log(LOG_INFO, "Discovery on acct:$addr with feed URL $feedUrl");
- $oprofile = self::ensureProfile($feedUrl, $hints);
+ $oprofile = self::ensureFeedURL($hints['feedurl'], $hints);
self::cacheSet(sprintf('ostatus_profile:webfinger:%s', $addr), $oprofile->uri);
return $oprofile;
} catch (Exception $e) {
@@ -1369,10 +1461,10 @@ class Ostatus_profile extends Memcached_DataObject
// If we got a profile page, try that!
- if (isset($profileUrl)) {
+ if (array_key_exists('profileurl', $hints)) {
try {
common_log(LOG_INFO, "Discovery on acct:$addr with profile URL $profileUrl");
- $oprofile = self::ensureProfile($profileUrl, $hints);
+ $oprofile = self::ensureProfile($hints['profileurl'], $hints);
self::cacheSet(sprintf('ostatus_profile:webfinger:%s', $addr), $oprofile->uri);
return $oprofile;
} catch (Exception $e) {
@@ -1384,7 +1476,9 @@ class Ostatus_profile extends Memcached_DataObject
// XXX: try hcard
// XXX: try FOAF
- if (isset($salmonEndpoint)) {
+ if (array_key_exists('salmon', $hints)) {
+
+ $salmonEndpoint = $hints['salmon'];
// An account URL, a salmon endpoint, and a dream? Not much to go
// on, but let's give it a try
@@ -1464,67 +1558,4 @@ class Ostatus_profile extends Memcached_DataObject
return $file;
}
-
- protected static function slurpHcard($url)
- {
- set_include_path(get_include_path() . PATH_SEPARATOR . INSTALLDIR . '/plugins/OStatus/extlib/hkit/');
- require_once('hkit.class.php');
-
- $h = new hKit;
-
- // Google Buzz hcards need to be tidied. Probably others too.
-
- $h->tidy_mode = 'proxy'; // 'proxy', 'exec', 'php' or 'none'
-
- // Get by URL
- $hcards = $h->getByURL('hcard', $url);
-
- if (empty($hcards)) {
- return array();
- }
-
- // @fixme more intelligent guess on multi-hcard pages
- $hcard = $hcards[0];
-
- $hints = array();
-
- $hints['profileurl'] = $url;
-
- if (array_key_exists('nickname', $hcard)) {
- $hints['nickname'] = $hcard['nickname'];
- }
-
- if (array_key_exists('fn', $hcard)) {
- $hints['fullname'] = $hcard['fn'];
- } else if (array_key_exists('n', $hcard)) {
- $hints['fullname'] = implode(' ', $hcard['n']);
- }
-
- if (array_key_exists('photo', $hcard)) {
- $hints['avatar'] = $hcard['photo'];
- }
-
- if (array_key_exists('note', $hcard)) {
- $hints['bio'] = $hcard['note'];
- }
-
- if (array_key_exists('adr', $hcard)) {
- if (is_string($hcard['adr'])) {
- $hints['location'] = $hcard['adr'];
- } else if (is_array($hcard['adr'])) {
- $hints['location'] = implode(' ', $hcard['adr']);
- }
- }
-
- if (array_key_exists('url', $hcard)) {
- if (is_string($hcard['url'])) {
- $hints['homepage'] = $hcard['url'];
- } else if (is_array($hcard['url'])) {
- // HACK get the last one; that's how our hcards look
- $hints['homepage'] = $hcard['url'][count($hcard['url'])-1];
- }
- }
-
- return $hints;
- }
}
diff --git a/plugins/OStatus/lib/discovery.php b/plugins/OStatus/lib/discovery.php
index f8449b309..6d245677a 100644
--- a/plugins/OStatus/lib/discovery.php
+++ b/plugins/OStatus/lib/discovery.php
@@ -40,7 +40,7 @@ class Discovery
const PROFILEPAGE = 'http://webfinger.net/rel/profile-page';
const UPDATESFROM = 'http://schemas.google.com/g/2010#updates-from';
const HCARD = 'http://microformats.org/profile/hcard';
-
+
public $methods = array();
public function __construct()
@@ -50,12 +50,11 @@ class Discovery
$this->registerMethod('Discovery_LRDD_Link_HTML');
}
-
public function registerMethod($class)
{
$this->methods[] = $class;
}
-
+
/**
* Given a "user id" make sure it's normalized to either a webfinger
* acct: uri or a profile HTTP URL.
@@ -78,7 +77,7 @@ class Discovery
public static function isWebfinger($user_id)
{
$uri = Discovery::normalize($user_id);
-
+
return (substr($uri, 0, 5) == 'acct:');
}
@@ -99,7 +98,7 @@ class Discovery
} else {
$xrd_uri = $link['href'];
}
-
+
$xrd = $this->fetchXrd($xrd_uri);
if ($xrd) {
return $xrd;
@@ -114,14 +113,13 @@ class Discovery
if (!is_array($links)) {
return false;
}
-
+
foreach ($links as $link) {
if ($link['rel'] == $service) {
return $link;
}
}
}
-
public static function applyTemplate($template, $id)
{
@@ -130,7 +128,6 @@ class Discovery
return $template;
}
-
public static function fetchXrd($url)
{
try {
@@ -171,7 +168,7 @@ class Discovery_LRDD_Host_Meta implements Discovery_LRDD
if ($xrd->host != $domain) {
return false;
}
-
+
return $xrd->links;
}
}
@@ -187,7 +184,7 @@ class Discovery_LRDD_Link_Header implements Discovery_LRDD
} catch (HTTP_Request2_Exception $e) {
return false;
}
-
+
if ($response->getStatus() != 200) {
return false;
}
@@ -196,51 +193,17 @@ class Discovery_LRDD_Link_Header implements Discovery_LRDD
if (!$link_header) {
// return false;
}
-
+
return Discovery_LRDD_Link_Header::parseHeader($link_header);
}
protected static function parseHeader($header)
{
- preg_match('/^<[^>]+>/', $header, $uri_reference);
- //if (empty($uri_reference)) return;
-
- $links = array();
-
- $link_uri = trim($uri_reference[0], '<>');
- $link_rel = array();
- $link_type = null;
-
- // remove uri-reference from header
- $header = substr($header, strlen($uri_reference[0]));
-
- // parse link-params
- $params = explode(';', $header);
-
- foreach ($params as $param) {
- if (empty($param)) continue;
- list($param_name, $param_value) = explode('=', $param, 2);
- $param_name = trim($param_name);
- $param_value = preg_replace('(^"|"$)', '', trim($param_value));
-
- // for now we only care about 'rel' and 'type' link params
- // TODO do something with the other links-params
- switch ($param_name) {
- case 'rel':
- $link_rel = trim($param_value);
- break;
-
- case 'type':
- $link_type = trim($param_value);
- }
- }
-
- $links[] = array(
- 'href' => $link_uri,
- 'rel' => $link_rel,
- 'type' => $link_type);
+ $lh = new LinkHeader($header);
- return $links;
+ return array('href' => $lh->href,
+ 'rel' => $lh->rel,
+ 'type' => $lh->type);
}
}
@@ -262,49 +225,48 @@ class Discovery_LRDD_Link_HTML implements Discovery_LRDD
return Discovery_LRDD_Link_HTML::parse($response->getBody());
}
-
public function parse($html)
{
$links = array();
-
+
preg_match('/<head(\s[^>]*)?>(.*?)<\/head>/is', $html, $head_matches);
$head_html = $head_matches[2];
-
+
preg_match_all('/<link\s[^>]*>/i', $head_html, $link_matches);
-
+
foreach ($link_matches[0] as $link_html) {
$link_url = null;
$link_rel = null;
$link_type = null;
-
+
preg_match('/\srel=(("|\')([^\\2]*?)\\2|[^"\'\s]+)/i', $link_html, $rel_matches);
if ( isset($rel_matches[3]) ) {
$link_rel = $rel_matches[3];
} else if ( isset($rel_matches[1]) ) {
$link_rel = $rel_matches[1];
}
-
+
preg_match('/\shref=(("|\')([^\\2]*?)\\2|[^"\'\s]+)/i', $link_html, $href_matches);
if ( isset($href_matches[3]) ) {
$link_uri = $href_matches[3];
} else if ( isset($href_matches[1]) ) {
$link_uri = $href_matches[1];
}
-
+
preg_match('/\stype=(("|\')([^\\2]*?)\\2|[^"\'\s]+)/i', $link_html, $type_matches);
if ( isset($type_matches[3]) ) {
$link_type = $type_matches[3];
} else if ( isset($type_matches[1]) ) {
$link_type = $type_matches[1];
}
-
+
$links[] = array(
'href' => $link_url,
'rel' => $link_rel,
'type' => $link_type,
);
}
-
+
return $links;
}
}
diff --git a/plugins/OStatus/lib/discoveryhints.php b/plugins/OStatus/lib/discoveryhints.php
new file mode 100644
index 000000000..db13793dd
--- /dev/null
+++ b/plugins/OStatus/lib/discoveryhints.php
@@ -0,0 +1,182 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * Some utilities for generating hint data
+ *
+ * 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/>.
+ */
+
+class DiscoveryHints {
+
+ static function fromXRD($xrd)
+ {
+ $hints = array();
+
+ foreach ($xrd->links as $link) {
+ switch ($link['rel']) {
+ case Discovery::PROFILEPAGE:
+ $hints['profileurl'] = $link['href'];
+ break;
+ case Salmon::NS_REPLIES:
+ $hints['salmon'] = $link['href'];
+ break;
+ case Discovery::UPDATESFROM:
+ $hints['feedurl'] = $link['href'];
+ break;
+ case Discovery::HCARD:
+ $hints['hcardurl'] = $link['href'];
+ break;
+ default:
+ break;
+ }
+ }
+
+ return $hints;
+ }
+
+ static function fromHcardUrl($url)
+ {
+ $client = new HTTPClient();
+ $client->setHeader('Accept', 'text/html,application/xhtml+xml');
+ $response = $client->get($url);
+
+ if (!$response->isOk()) {
+ return null;
+ }
+
+ return self::hcardHints($response->getBody(),
+ $response->getUrl());
+ }
+
+ static function hcardHints($body, $url)
+ {
+ common_debug("starting tidy");
+
+ $body = self::_tidy($body);
+
+ common_debug("done with tidy");
+
+ set_include_path(get_include_path() . PATH_SEPARATOR . INSTALLDIR . '/plugins/OStatus/extlib/hkit/');
+ require_once('hkit.class.php');
+
+ $h = new hKit;
+
+ $hcards = $h->getByString('hcard', $body);
+
+ if (empty($hcards)) {
+ return array();
+ }
+
+ if (count($hcards) == 1) {
+ $hcard = $hcards[0];
+ } else {
+ foreach ($hcards as $try) {
+ if (array_key_exists('url', $try)) {
+ if (is_string($try['url']) && $try['url'] == $url) {
+ $hcard = $try;
+ break;
+ } else if (is_array($try['url'])) {
+ foreach ($try['url'] as $tryurl) {
+ if ($tryurl == $url) {
+ $hcard = $try;
+ break 2;
+ }
+ }
+ }
+ }
+ }
+ // last chance; grab the first one
+ if (empty($hcard)) {
+ $hcard = $hcards[0];
+ }
+ }
+
+ $hints = array();
+
+ if (array_key_exists('nickname', $hcard)) {
+ $hints['nickname'] = $hcard['nickname'];
+ }
+
+ if (array_key_exists('fn', $hcard)) {
+ $hints['fullname'] = $hcard['fn'];
+ } else if (array_key_exists('n', $hcard)) {
+ $hints['fullname'] = implode(' ', $hcard['n']);
+ }
+
+ if (array_key_exists('photo', $hcard)) {
+ $hints['avatar'] = $hcard['photo'];
+ }
+
+ if (array_key_exists('note', $hcard)) {
+ $hints['bio'] = $hcard['note'];
+ }
+
+ if (array_key_exists('adr', $hcard)) {
+ if (is_string($hcard['adr'])) {
+ $hints['location'] = $hcard['adr'];
+ } else if (is_array($hcard['adr'])) {
+ $hints['location'] = implode(' ', $hcard['adr']);
+ }
+ }
+
+ if (array_key_exists('url', $hcard)) {
+ if (is_string($hcard['url'])) {
+ $hints['homepage'] = $hcard['url'];
+ } else if (is_array($hcard['url'])) {
+ // HACK get the last one; that's how our hcards look
+ $hints['homepage'] = $hcard['url'][count($hcard['url'])-1];
+ }
+ }
+
+ return $hints;
+ }
+
+ private static function _tidy($body)
+ {
+ if (function_exists('tidy_parse_string')) {
+ common_debug("Tidying with extension");
+ $text = tidy_parse_string($body);
+ $text = tidy_clean_repair($text);
+ return $body;
+ } else if ($fullpath = self::_findProgram('tidy')) {
+ common_debug("Tidying with program $fullpath");
+ $tempfile = tempnam('/tmp', 'snht'); // statusnet hcard tidy
+ file_put_contents($tempfile, $source);
+ exec("$fullpath -utf8 -indent -asxhtml -numeric -bare -quiet $tempfile", $tidy);
+ unlink($tempfile);
+ return implode("\n", $tidy);
+ } else {
+ common_debug("Not tidying.");
+ return $body;
+ }
+ }
+
+ private static function _findProgram($name)
+ {
+ $path = $_ENV['PATH'];
+
+ $parts = explode(':', $path);
+
+ foreach ($parts as $part) {
+ $fullpath = $part . '/' . $name;
+ if (is_executable($fullpath)) {
+ return $fullpath;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/plugins/OStatus/lib/feeddiscovery.php b/plugins/OStatus/lib/feeddiscovery.php
index ff76b229e..f9ea3e713 100644
--- a/plugins/OStatus/lib/feeddiscovery.php
+++ b/plugins/OStatus/lib/feeddiscovery.php
@@ -117,7 +117,7 @@ class FeedDiscovery
return $this->discoverFromURL($target, false);
}
}
-
+
return $this->initFromResponse($response);
}
@@ -202,7 +202,7 @@ class FeedDiscovery
'application/atom+xml' => false,
'application/rss+xml' => false,
);
-
+
$nodes = $dom->getElementsByTagName('link');
for ($i = 0; $i < $nodes->length; $i++) {
$node = $nodes->item($i);
diff --git a/plugins/OStatus/lib/linkheader.php b/plugins/OStatus/lib/linkheader.php
new file mode 100644
index 000000000..2f6c66dc9
--- /dev/null
+++ b/plugins/OStatus/lib/linkheader.php
@@ -0,0 +1,63 @@
+<?php
+
+class LinkHeader
+{
+ var $href;
+ var $rel;
+ var $type;
+
+ function __construct($str)
+ {
+ preg_match('/^<[^>]+>/', $str, $uri_reference);
+ //if (empty($uri_reference)) return;
+
+ $this->uri = trim($uri_reference[0], '<>');
+ $this->rel = array();
+ $this->type = null;
+
+ // remove uri-reference from header
+ $str = substr($str, strlen($uri_reference[0]));
+
+ // parse link-params
+ $params = explode(';', $str);
+
+ foreach ($params as $param) {
+ if (empty($param)) continue;
+ list($param_name, $param_value) = explode('=', $param, 2);
+ $param_name = trim($param_name);
+ $param_value = preg_replace('(^"|"$)', '', trim($param_value));
+
+ // for now we only care about 'rel' and 'type' link params
+ // TODO do something with the other links-params
+ switch ($param_name) {
+ case 'rel':
+ $this->rel = trim($param_value);
+ break;
+
+ case 'type':
+ $this->type = trim($param_value);
+ }
+ }
+ }
+
+ static function getLink($response, $rel=null, $type=null)
+ {
+ $headers = $response->getHeader('Link');
+
+ // Can get an array or string, so try to simplify the path
+ if (!is_array($headers)) {
+ $headers = array($headers);
+ }
+
+ foreach ($headers as $header) {
+ $lh = new LinkHeader($header);
+
+ if ((is_null($rel) || $lh->rel == $rel) &&
+ (is_null($type) || $lh->type == $type)) {
+ return $lh->href;
+ }
+ }
+
+ return null;
+ }
+} \ No newline at end of file