Compare commits

...

9 commits

20 changed files with 181 additions and 60 deletions

View file

@ -44,10 +44,11 @@ Example:
"region": "New York", "region": "New York",
"country": "USA", "country": "USA",
"profile_url": "https://friendica.mrpetovan.com/profile/hypolite", "profile_url": "https://friendica.mrpetovan.com/profile/hypolite",
"dfrn_request": "https://friendica.mrpetovan.com/dfrn_request/hypolite",
"photo": "https://friendica.mrpetovan.com/photo/27330388315ae4ed2b03e3c116980490-4.jpg?ts=1541567135", "photo": "https://friendica.mrpetovan.com/photo/27330388315ae4ed2b03e3c116980490-4.jpg?ts=1541567135",
"tags": "videogame gaming boardgame politics philosophy development programming php", "tags": "videogame gaming boardgame politics philosophy development programming php",
"last_activity": "2018-45" "last_activity": "2018-45",
"remote_follow": "https://friendica.mrpetovan.com/remote_follow/hypolite",
"subscribe": null
}, },
... ...
] ]

View file

@ -11,10 +11,6 @@ use Psr\Http\Message\ServerRequestInterface;
*/ */
class MatchSearch class MatchSearch
{ {
/**
* @var \Atlas\Pdo\Connection
*/
private $atlas;
/** /**
* @var \Friendica\Directory\Models\Profile * @var \Friendica\Directory\Models\Profile
*/ */
@ -25,12 +21,10 @@ class MatchSearch
private $l10n; private $l10n;
public function __construct( public function __construct(
\Atlas\Pdo\Connection $atlas,
\Friendica\Directory\Models\Profile $profileModel, \Friendica\Directory\Models\Profile $profileModel,
\Gettext\TranslatorInterface $l10n \Gettext\TranslatorInterface $l10n
) )
{ {
$this->atlas = $atlas;
$this->profileModel = $profileModel; $this->profileModel = $profileModel;
$this->l10n = $l10n; $this->l10n = $l10n;
} }
@ -53,7 +47,13 @@ class MatchSearch
$values = ['query' => $query]; $values = ['query' => $query];
$profiles = $this->profileModel->getListForDisplay($pager->getItemsPerPage(), $pager->getStart(), $sql_where, $values); $profiles = $this->profileModel->getListForDisplay(
null,
$pager->getItemsPerPage(),
$pager->getStart(),
$sql_where,
$values,
);
$results = []; $results = [];
foreach ($profiles as $profile) { foreach ($profiles as $profile) {

View file

@ -11,10 +11,6 @@ use Psr\Http\Message\ServerRequestInterface;
*/ */
class Search class Search
{ {
/**
* @var \Atlas\Pdo\Connection
*/
private $atlas;
/** /**
* @var \Friendica\Directory\Models\Profile * @var \Friendica\Directory\Models\Profile
*/ */
@ -25,12 +21,10 @@ class Search
private $l10n; private $l10n;
public function __construct( public function __construct(
\Atlas\Pdo\Connection $atlas,
\Friendica\Directory\Models\Profile $profileModel, \Friendica\Directory\Models\Profile $profileModel,
\Gettext\TranslatorInterface $l10n \Gettext\TranslatorInterface $l10n
) )
{ {
$this->atlas = $atlas;
$this->profileModel = $profileModel; $this->profileModel = $profileModel;
$this->l10n = $l10n; $this->l10n = $l10n;
} }
@ -64,7 +58,13 @@ AND `account_type` = :account_type';
$values['account_type'] = $account_type; $values['account_type'] = $account_type;
} }
$profiles = $this->profileModel->getListForDisplay($pager->getItemsPerPage(), $pager->getStart(), $sql_where, $values); $profiles = $this->profileModel->getListForDisplay(
null,
$pager->getItemsPerPage(),
$pager->getStart(),
$sql_where,
$values,
);
$count = $this->profileModel->getCountForDisplay($sql_where, $values); $count = $this->profileModel->getCountForDisplay($sql_where, $values);

View file

@ -31,9 +31,9 @@ class Install extends \Asika\SimpleConsole\Console
protected function getHelp() protected function getHelp()
{ {
$help = <<<HELP $help = <<<HELP
console install - Install directory console install - Install wizard
Usage Usage
bin/console install <server_url> [-h|--help|-?] [-v] bin/console install [-h|--help|-?] [-v]
Description Description
Install directory Install directory

View file

@ -37,12 +37,14 @@ class UpdateDb extends \Asika\SimpleConsole\Console
$help = <<<HELP $help = <<<HELP
console updatedb - Update database schema console updatedb - Update database schema
Usage Usage
bin/console updatedb <server_url> [-h|--help|-?] [-v] bin/console updatedb [<version>] [-h|--help|-?] [-v]
Description Description
Update database schema Update database schema
Options Options
<version> Optional target version number, default is the latest version.
Do not use this parameter if you're not sure what you're doing, it will result in data loss!
-h|--help|-? Show help information -h|--help|-? Show help information
-v Show more debug information. -v Show more debug information.
HELP; HELP;
@ -56,16 +58,38 @@ HELP;
return 0; return 0;
} }
if (count($this->args) > 1) { if (count($this->args) > 2) {
throw new \Asika\SimpleConsole\CommandArgsException('Too many arguments'); throw new \Asika\SimpleConsole\CommandArgsException('Too many arguments');
} }
$currentVersion = $this->migration->getCurrentVersion()['version'];
$this->out('Database schema currently in version ' . $currentVersion);
if (count($this->args) == 1) {
$this->out('Updating database schema to latest version...'); $this->out('Updating database schema to latest version...');
$this->migration->up(); $this->migration->up();
$this->out('Database schema migrated to version ' . $this->migration->getCurrentVersion()['version']); $this->out('Database schema migrated to version ' . $this->migration->getCurrentVersion()['version']);
return 0;
}
$target = $this->getArgument(1);
if ($target > $currentVersion) {
$this->out('Updating database schema to version ' . $target);
$this->migration->up($target);
$this->out('Database schema migrated up to version ' . $this->migration->getCurrentVersion()['version']);
return 0;
}
if ($target < $currentVersion) {
$this->out('Downgrading database schema to version ' . $target);
$this->migration->down($target);
$this->out('Database schema migrated down to version ' . $this->migration->getCurrentVersion()['version']);
return 0;
}
$this->out('Target version equal to current version, exiting.');
return 0; return 0;
} }
} }

View file

@ -18,6 +18,10 @@ class Directory extends BaseController
* @var \Atlas\Pdo\Connection * @var \Atlas\Pdo\Connection
*/ */
private $atlas; private $atlas;
/**
* @var \Friendica\Directory\Models\Server
*/
private $serverModel;
/** /**
* @var \Friendica\Directory\Models\Profile * @var \Friendica\Directory\Models\Profile
*/ */
@ -37,6 +41,7 @@ class Directory extends BaseController
public function __construct( public function __construct(
\Atlas\Pdo\Connection $atlas, \Atlas\Pdo\Connection $atlas,
\Friendica\Directory\Models\Server $serverModel,
\Friendica\Directory\Models\Profile $profileModel, \Friendica\Directory\Models\Profile $profileModel,
\Friendica\Directory\Views\Widget\AccountTypeTabs $accountTypeTabs, \Friendica\Directory\Views\Widget\AccountTypeTabs $accountTypeTabs,
\Friendica\Directory\Views\PhpRenderer $renderer, \Friendica\Directory\Views\PhpRenderer $renderer,
@ -44,6 +49,7 @@ class Directory extends BaseController
) )
{ {
$this->atlas = $atlas; $this->atlas = $atlas;
$this->serverModel = $serverModel;
$this->profileModel = $profileModel; $this->profileModel = $profileModel;
$this->accountTypeTabs = $accountTypeTabs; $this->accountTypeTabs = $accountTypeTabs;
$this->renderer = $renderer; $this->renderer = $renderer;
@ -58,16 +64,22 @@ class Directory extends BaseController
$pager = new Pager($this->l10n, $request, 20); $pager = new Pager($this->l10n, $request, 20);
$condition = ''; $sql_where = '';
$values = []; $values = [];
if (!empty($args['account_type'])) { if (!empty($args['account_type'])) {
$condition = '`account_type` = :account_type'; $sql_where = '`account_type` = :account_type';
$values = ['account_type' => $args['account_type']]; $values = ['account_type' => $args['account_type']];
} }
$profiles = $this->profileModel->getListForDisplay($pager->getItemsPerPage(), $pager->getStart(), $condition, $values); $profiles = $this->profileModel->getListForDisplay(
$this->serverModel->getSubscribeUrlByProfile($request->getQueryParam('zrl', '')),
$pager->getItemsPerPage(),
$pager->getStart(),
$sql_where,
$values,
);
$count = $this->profileModel->getCountForDisplay($condition, $values); $count = $this->profileModel->getCountForDisplay($sql_where, $values);
$vars = [ $vars = [
'title' => $this->l10n->gettext('People'), 'title' => $this->l10n->gettext('People'),
@ -82,7 +94,6 @@ class Directory extends BaseController
$content = $this->renderer->fetch('directory.phtml', $vars); $content = $this->renderer->fetch('directory.phtml', $vars);
// Render index view
return ['content' => $content]; return ['content' => $content];
} }
} }

View file

@ -13,6 +13,10 @@ class Search extends BaseController
* @var \Atlas\Pdo\Connection * @var \Atlas\Pdo\Connection
*/ */
private $atlas; private $atlas;
/**
* @var \Friendica\Directory\Models\Server
*/
private $serverModel;
/** /**
* @var \Friendica\Directory\Models\Profile * @var \Friendica\Directory\Models\Profile
*/ */
@ -32,6 +36,7 @@ class Search extends BaseController
public function __construct( public function __construct(
\Atlas\Pdo\Connection $atlas, \Atlas\Pdo\Connection $atlas,
\Friendica\Directory\Models\Server $serverModel,
\Friendica\Directory\Models\Profile $profileModel, \Friendica\Directory\Models\Profile $profileModel,
\Friendica\Directory\Views\Widget\AccountTypeTabs $accountTypeTabs, \Friendica\Directory\Views\Widget\AccountTypeTabs $accountTypeTabs,
\Friendica\Directory\Views\PhpRenderer $renderer, \Friendica\Directory\Views\PhpRenderer $renderer,
@ -39,6 +44,7 @@ class Search extends BaseController
) )
{ {
$this->atlas = $atlas; $this->atlas = $atlas;
$this->serverModel = $serverModel;
$this->profileModel = $profileModel; $this->profileModel = $profileModel;
$this->accountTypeTabs = $accountTypeTabs; $this->accountTypeTabs = $accountTypeTabs;
$this->renderer = $renderer; $this->renderer = $renderer;
@ -89,7 +95,13 @@ AND `account_type` = :account_type';
$values['account_type'] = $account_type; $values['account_type'] = $account_type;
} }
$profiles = $this->profileModel->getListForDisplay($pager->getItemsPerPage(), $pager->getStart(), $sql_where, $values); $profiles = $this->profileModel->getListForDisplay(
$this->serverModel->getSubscribeUrlByProfile($request->getQueryParam('zrl', '')),
$pager->getItemsPerPage(),
$pager->getStart(),
$sql_where,
$values,
);
$count = $this->profileModel->getCountForDisplay($sql_where, $values); $count = $this->profileModel->getCountForDisplay($sql_where, $values);
@ -106,7 +118,6 @@ AND `account_type` = :account_type';
$content = $this->renderer->fetch('search.phtml', $vars); $content = $this->renderer->fetch('search.phtml', $vars);
// Render index view
return ['content' => $content, 'noNavSearch' => true]; return ['content' => $content, 'noNavSearch' => true];
} }
} }

View file

@ -74,7 +74,7 @@ class Profile extends \Friendica\Directory\Model
]; ];
} }
public function getListForDisplay(int $limit = 30, int $start = 0, string $condition = '', array $values = []): array public function getListForDisplay(string $subscribeUrl = null, int $limit = 30, int $start = 0, string $condition = '', array $values = []): array
{ {
if ($condition) { if ($condition) {
$condition = 'AND ' . $condition; $condition = 'AND ' . $condition;
@ -86,8 +86,8 @@ class Profile extends \Friendica\Directory\Model
]); ]);
$stmt = 'SELECT p.`id`, p.`name`, p.`username`, p.`addr`, p.`account_type`, p.`language`, $stmt = 'SELECT p.`id`, p.`name`, p.`username`, p.`addr`, p.`account_type`, p.`language`,
p.`pdesc`, p.`locality`, p.`region`, p.`country`, p.`profile_url`, p.`dfrn_request`, p.`pdesc`, p.`locality`, p.`region`, p.`country`, p.`profile_url`,
p.`photo`, p.`tags`, p.`last_activity` p.`photo`, p.`tags`, p.`last_activity`, s.`version`
FROM `profile` p FROM `profile` p
JOIN `server` s ON s.`id` = p.`server_id` AND s.`available` AND NOT s.`hidden` JOIN `server` s ON s.`id` = p.`server_id` AND s.`available` AND NOT s.`hidden`
WHERE p.`available` WHERE p.`available`
@ -98,6 +98,12 @@ class Profile extends \Friendica\Directory\Model
LIMIT :start, :limit'; LIMIT :start, :limit';
$profiles = $this->atlas->fetchAll($stmt, $values); $profiles = $this->atlas->fetchAll($stmt, $values);
array_walk($profiles, function (array &$profile) use ($subscribeUrl) {
$profile['remote_follow'] = version_compare($profile['version'], '2020.03', '>=') ? str_replace('/profile/', '/remote_follow/', $profile['profile_url']) : null;
$profile['subscribe'] = $subscribeUrl ? str_replace('{uri}', urlencode($profile['profile_url']), $subscribeUrl): null;
unset($profile['version']);
});
return $profiles; return $profiles;
} }

View file

@ -41,4 +41,20 @@ class Server extends \Friendica\Directory\Model
'alias' => strtolower($server_alias) 'alias' => strtolower($server_alias)
]); ]);
} }
/**
* Returns the complete subscribe URL of the given profile URL if we have it for the related server
*
* @param string $profile_url
* @return mixed|null
*/
public function getSubscribeUrlByProfile(string $profile_url)
{
if (preg_match('#^(.+)/profile/#', $profile_url, $matches)) {
$server = $this->getByUrlAlias($matches[1]);
return $server['subscribe_url'] ?? null;
}
return null;
}
} }

View file

@ -9,7 +9,6 @@ use Friendica\Directory\Utils\Network;
*/ */
class Profile class Profile
{ {
const PROFILE_MISSING_REQUEST = 1;
const PROFILE_MISSING_CONFIRM = 2; const PROFILE_MISSING_CONFIRM = 2;
const PROFILE_MISSING_NOTIFY = 4; const PROFILE_MISSING_NOTIFY = 4;
const PROFILE_MISSING_POLL = 8; const PROFILE_MISSING_POLL = 8;
@ -177,9 +176,6 @@ class Profile
// This is most likely a problem with the site configuration. Ignore. // This is most likely a problem with the site configuration. Ignore.
if ($error = self::validateParams($params)) { if ($error = self::validateParams($params)) {
$this->logger->warning('Poll aborted, parameters invalid.', ['params' => $params]); $this->logger->warning('Poll aborted, parameters invalid.', ['params' => $params]);
if ($error & Profile::PROFILE_MISSING_REQUEST) {
$this->logger->notice('dfrn-request parameter is empty.');
}
if ($error & Profile::PROFILE_MISSING_CONFIRM) { if ($error & Profile::PROFILE_MISSING_CONFIRM) {
$this->logger->notice('dfrn-confirm parameter is empty.'); $this->logger->notice('dfrn-confirm parameter is empty.');
} }
@ -232,7 +228,6 @@ class Profile
'region' => $params['region'] ?? '', 'region' => $params['region'] ?? '',
'country' => $params['country-name'] ?? '', 'country' => $params['country-name'] ?? '',
'profile_url' => $profile_uri, 'profile_url' => $profile_uri,
'dfrn_request' => $params['dfrn-request'] ?? null,
'photo' => $params['photo'], 'photo' => $params['photo'],
'tags' => implode(' ', $tags), 'tags' => implode(' ', $tags),
'addr' => $addr, 'addr' => $addr,
@ -254,7 +249,6 @@ class Profile
`region` = :region, `region` = :region,
`country` = :country, `country` = :country,
`profile_url` = :profile_url, `profile_url` = :profile_url,
`dfrn_request` = :dfrn_request,
`photo` = :photo, `photo` = :photo,
`tags` = :tags, `tags` = :tags,
`addr` = :addr, `addr` = :addr,
@ -274,7 +268,6 @@ class Profile
`region` = :region, `region` = :region,
`country` = :country, `country` = :country,
`profile_url` = :profile_url, `profile_url` = :profile_url,
`dfrn_request` = :dfrn_request,
`photo` = :photo, `photo` = :photo,
`tags` = :tags, `tags` = :tags,
`addr` = :addr, `addr` = :addr,
@ -361,9 +354,6 @@ class Profile
private static function validateParams(array $params): int private static function validateParams(array $params): int
{ {
$errors = 0; $errors = 0;
if (empty($params['dfrn-request'])) {
$errors &= self::PROFILE_MISSING_REQUEST;
}
if (empty($params['dfrn-confirm'])) { if (empty($params['dfrn-confirm'])) {
$errors &= self::PROFILE_MISSING_CONFIRM; $errors &= self::PROFILE_MISSING_CONFIRM;
} }

View file

@ -147,6 +147,10 @@ class Server
$addons = $probe_result['data']['plugins']; $addons = $probe_result['data']['plugins'];
} }
if ($probe_result['data']['admin']['profile']) {
$subscribe = $this->getSubscribeUrl($probe_result['data']['url'], $probe_result['data']['admin']['profile']);
}
$this->atlas->perform( $this->atlas->perform(
'UPDATE `server` 'UPDATE `server`
SET `available` = 1, SET `available` = 1,
@ -161,6 +165,7 @@ class Server
`admin_name` = :admin_name, `admin_name` = :admin_name,
`admin_profile` = :admin_profile, `admin_profile` = :admin_profile,
`noscrape_url` = :noscrape_url, `noscrape_url` = :noscrape_url,
`subscribe_url` = :subscribe_url,
`ssl_state` = :ssl_state `ssl_state` = :ssl_state
WHERE `id` = :server_id', WHERE `id` = :server_id',
[ [
@ -175,6 +180,7 @@ class Server
'admin_name' => $probe_result['data']['admin']['name'], 'admin_name' => $probe_result['data']['admin']['name'],
'admin_profile' => $probe_result['data']['admin']['profile'], 'admin_profile' => $probe_result['data']['admin']['profile'],
'noscrape_url' => $probe_result['data']['no_scrape_url'] ?? null, 'noscrape_url' => $probe_result['data']['no_scrape_url'] ?? null,
'subscribe_url' => $subscribe ?? null,
'ssl_state' => $probe_result['ssl_state'] 'ssl_state' => $probe_result['ssl_state']
] ]
); );
@ -452,4 +458,38 @@ class Server
} }
} }
} }
public function getSubscribeUrl($base_url, $profile)
{
$xrdRequest = new WebRequest($base_url . '/xrd');
$xrdRequest->addRequestHeader('Accept', 'application/jrd+json');
$xrdJsonData = $xrdRequest->get(['uri' => $profile]);
$this->logger->debug('WebRequest: ' . $xrdRequest->getLastFetchedUrl() . ' Status: ' . $xrdRequest->getLastStatus());
if ($xrdRequest->getLastStatus() != 200) {
$this->logger->info('Unsuccessful XRD request: ' . $xrdRequest->getLastFetchedUrl());
return null;
}
try {
$xrdData = json_decode($xrdJsonData);
} catch (\Throwable $e) {
$this->logger->notice('Invalid JSON string for XRD URL: ' . $xrdRequest->getLastFetchedUrl());
return null;
}
if (!isset($xrdData->links)) {
$this->logger->notice('Invalid JSON structure for XRD URL: ' . $xrdRequest->getLastFetchedUrl());
return null;
}
foreach ($xrdData->links as $link) {
if ($link->rel == 'http://ostatus.org/schema/1.0/subscribe') {
return $link->template ?? null;
}
}
return null;
}
} }

View file

@ -10,7 +10,6 @@ class MatchSearch extends BaseRoute
public function __invoke(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args): \Slim\Http\Response public function __invoke(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args): \Slim\Http\Response
{ {
return (new \Friendica\Directory\Controllers\Api\MatchSearch( return (new \Friendica\Directory\Controllers\Api\MatchSearch(
$this->container->atlas,
$this->container->get(\Friendica\Directory\Models\Profile::class), $this->container->get(\Friendica\Directory\Models\Profile::class),
$this->container->l10n $this->container->l10n
))->render($request, $response, $args); ))->render($request, $response, $args);

View file

@ -10,7 +10,6 @@ class Search extends BaseRoute
public function __invoke(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args): \Slim\Http\Response public function __invoke(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args): \Slim\Http\Response
{ {
return (new \Friendica\Directory\Controllers\Api\Search( return (new \Friendica\Directory\Controllers\Api\Search(
$this->container->atlas,
$this->container->get(\Friendica\Directory\Models\Profile::class), $this->container->get(\Friendica\Directory\Models\Profile::class),
$this->container->l10n $this->container->l10n
))->render($request, $response, $args); ))->render($request, $response, $args);

View file

@ -13,6 +13,7 @@ class Directory extends BaseRoute
$this->controller = new \Friendica\Directory\Controllers\Web\Directory( $this->controller = new \Friendica\Directory\Controllers\Web\Directory(
$this->container->atlas, $this->container->atlas,
$this->container->get(\Friendica\Directory\Models\Server::class),
$this->container->get(\Friendica\Directory\Models\Profile::class), $this->container->get(\Friendica\Directory\Models\Profile::class),
$this->container->get(\Friendica\Directory\Views\Widget\AccountTypeTabs::class), $this->container->get(\Friendica\Directory\Views\Widget\AccountTypeTabs::class),
$this->container->renderer, $this->container->renderer,

View file

@ -13,6 +13,7 @@ class Search extends BaseRoute
$this->controller = new \Friendica\Directory\Controllers\Web\Search( $this->controller = new \Friendica\Directory\Controllers\Web\Search(
$this->container->atlas, $this->container->atlas,
$this->container->get(\Friendica\Directory\Models\Server::class),
$this->container->get(\Friendica\Directory\Models\Profile::class), $this->container->get(\Friendica\Directory\Models\Profile::class),
$this->container->get(\Friendica\Directory\Views\Widget\AccountTypeTabs::class), $this->container->get(\Friendica\Directory\Views\Widget\AccountTypeTabs::class),
$this->container->renderer, $this->container->renderer,

View file

@ -0,0 +1,3 @@
BEGIN;
ALTER TABLE `server` DROP `subscribe_url`;
COMMIT;

View file

@ -0,0 +1,3 @@
BEGIN;
ALTER TABLE `profile` ADD `dfrn_request` VARCHAR(250) DEFAULT NULL AFTER `profile_url`;
COMMIT;

View file

@ -0,0 +1,3 @@
BEGIN;
ALTER TABLE `server` ADD `subscribe_url` VARCHAR(250) NULL AFTER `noscrape_url`;
COMMIT;

View file

@ -0,0 +1,3 @@
BEGIN;
ALTER TABLE `profile` DROP `dfrn_request`;
COMMIT;

View file

@ -26,12 +26,22 @@ if (!empty($profile['country'])) {
</a> </a>
<div class="media-body"> <div class="media-body">
<h5 class="name"> <h5 class="name">
<?php if ($profile['dfrn_request']): ?> <?php if ($profile['subscribe']): ?>
<a href="<?php echo $this->escapeHtmlAttr($this->u($profile['dfrn_request'])); ?>" class="card-link btn btn-primary float-right"> <a href="<?php echo $this->escapeHtmlAttr($profile['subscribe']); ?>" class="card-link btn btn-primary float-right" target="_blank" rel="noopener noreferrer">
<i class="fa fa-external-link-alt"></i> <?php echo $this->p__('verb', 'Follow')?>
</a>
<?php elseif ($profile['remote_follow']): ?>
<a href="<?php echo $this->escapeHtmlAttr($this->u($profile['remote_follow'])); ?>" class="card-link btn btn-primary float-right" target="_blank" rel="noopener noreferrer">
<i class="fa fa-external-link-alt"></i> <?php echo $this->p__('verb', 'Follow')?>
</a>
<?php else: ?>
<a href="<?php echo $this->escapeHtmlAttr($this->u($profile['profile_url'])); ?>" class="card-link btn btn-primary float-right" target="_blank" rel="noopener noreferrer">
<i class="fa fa-external-link-alt"></i> <?php echo $this->p__('verb', 'Follow')?> <i class="fa fa-external-link-alt"></i> <?php echo $this->p__('verb', 'Follow')?>
</a> </a>
<?php endif; ?> <?php endif; ?>
<a href="<?php echo $this->escapeHtmlAttr($profile['profile_url']) ?>">
<?php echo $this->escapeHtml($profile['name']) ?> <?php echo $this->escapeHtml($profile['name']) ?>
</a>
</h5> </h5>
<p class="url"> <p class="url">
<a href="<?php echo $this->escapeHtmlAttr($this->u($profile['profile_url'])) ?>"> <a href="<?php echo $this->escapeHtmlAttr($this->u($profile['profile_url'])) ?>">