X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=src%2FProtocol%2FDiaspora.php;h=dd1f678d58945a22f3651f9001ab04f845b7af3a;hb=cc8491223ec17bd12b7f37d5b2cc16b58184291e;hp=fd209911083ed5e3019e2f8c9fcaffbc025b3235;hpb=09961a07e48de836f0629b807c4a276c2ff06e87;p=friendica.git diff --git a/src/Protocol/Diaspora.php b/src/Protocol/Diaspora.php index fd20991108..dd1f678d58 100644 --- a/src/Protocol/Diaspora.php +++ b/src/Protocol/Diaspora.php @@ -35,9 +35,10 @@ use Friendica\Model\Contact; use Friendica\Model\Conversation; use Friendica\Model\GContact; use Friendica\Model\Item; -use Friendica\Model\ItemDeliveryData; +use Friendica\Model\ItemURI; use Friendica\Model\Mail; -use Friendica\Model\Profile; +use Friendica\Model\Post; +use Friendica\Model\Tag; use Friendica\Model\User; use Friendica\Network\Probe; use Friendica\Util\Crypto; @@ -111,7 +112,7 @@ class Diaspora if (DI::config()->get("system", "relay_directly", false)) { // We distribute our stuff based on the parent to ensure that the thread will be complete - $parent = Item::selectFirst(['parent'], ['id' => $item_id]); + $parent = Item::selectFirst(['uri-id'], ['id' => $item_id]); if (!DBA::isResult($parent)) { return; } @@ -121,14 +122,15 @@ class Diaspora while ($server = DBA::fetch($servers)) { $serverlist[$server['url']] = $server['url']; } + DBA::close($servers); // All tags of the current post - $condition = ['otype' => TERM_OBJ_POST, 'type' => TERM_HASHTAG, 'oid' => $parent['parent']]; - $tags = DBA::select('term', ['term'], $condition); + $tags = DBA::select('tag-view', ['name'], ['uri-id' => $parent['uri-id'], 'type' => Tag::HASHTAG]); $taglist = []; while ($tag = DBA::fetch($tags)) { - $taglist[] = $tag['term']; + $taglist[] = $tag['name']; } + DBA::close($tags); // All servers who wants content with this tag $tagserverlist = []; @@ -137,6 +139,7 @@ class Diaspora while ($server = DBA::fetch($tagserver)) { $tagserverlist[] = $server['gserver-id']; } + DBA::close($tagserver); } // All adresses with the given id @@ -145,6 +148,7 @@ class Diaspora while ($server = DBA::fetch($servers)) { $serverlist[$server['url']] = $server['url']; } + DBA::close($servers); } } @@ -248,34 +252,29 @@ class Diaspora * One of the parameters is a contact array. * This is done to avoid duplicates. * - * @param integer $thread The id of the thread - * @param array $contacts The previously fetched contacts + * @param array $item Item that is about to be delivered + * @param array $contacts The previously fetched contacts * * @return array of relay servers * @throws \Exception */ - public static function participantsForThread($thread, array $contacts) + public static function participantsForThread(array $item, array $contacts) { - $r = DBA::p("SELECT `contact`.`batch`, `contact`.`id`, `contact`.`url`, `contact`.`name`, `contact`.`network`, `contact`.`protocol`, - `fcontact`.`batch` AS `fbatch`, `fcontact`.`network` AS `fnetwork` FROM `participation` - INNER JOIN `contact` ON `contact`.`id` = `participation`.`cid` - INNER JOIN `fcontact` ON `fcontact`.`id` = `participation`.`fid` - WHERE `participation`.`iid` = ? AND NOT `contact`.`archive`", $thread); - - while ($contact = DBA::fetch($r)) { - if (!empty($contact['fnetwork'])) { - $contact['network'] = $contact['fnetwork']; - } - unset($contact['fnetwork']); - - if (empty($contact['protocol'])) { - $contact['protocol'] = $contact['network']; - } - - if (empty($contact['batch']) && !empty($contact['fbatch'])) { - $contact['batch'] = $contact['fbatch']; + if (!in_array($item['private'], [Item::PUBLIC, Item::UNLISTED]) || in_array($item["verb"], [Activity::FOLLOW, Activity::TAG])) { + Logger::info('Item is private or a participation request. It will not be relayed', ['guid' => $item['guid'], 'private' => $item['private'], 'verb' => $item['verb']]); + return $contacts; + } + + $items = Item::select(['author-id', 'author-link', 'parent-author-link', 'parent-guid', 'guid'], + ['parent' => $item['parent'], 'gravity' => [GRAVITY_COMMENT, GRAVITY_ACTIVITY]]); + while ($item = DBA::fetch($items)) { + $contact = DBA::selectFirst('contact', ['id', 'url', 'name', 'protocol', 'batch', 'network'], + ['id' => $item['author-id']]); + if (!DBA::isResult($contact) || empty($contact['batch']) || + ($contact['network'] != Protocol::DIASPORA) || + Strings::compareLink($item['parent-author-link'], $item['author-link'])) { + continue; } - unset($contact['fbatch']); $exists = false; foreach ($contacts as $entry) { @@ -285,10 +284,11 @@ class Diaspora } if (!$exists) { + Logger::info('Add participant to receiver list', ['parent' => $item['parent-guid'], 'item' => $item['guid'], 'participant' => $contact['url']]); $contacts[] = $contact; } } - DBA::close($r); + DBA::close($items); return $contacts; } @@ -335,7 +335,7 @@ class Diaspora */ private static function verifyMagicEnvelope($envelope) { - $basedom = XML::parseString($envelope); + $basedom = XML::parseString($envelope, true); if (!is_object($basedom)) { Logger::log("Envelope is no XML file"); @@ -461,7 +461,7 @@ class Diaspora $xml = $raw; } - $basedom = XML::parseString($xml); + $basedom = XML::parseString($xml, true); if (!is_object($basedom)) { Logger::log('Received data does not seem to be an XML. Discarding. '.$xml); @@ -1476,7 +1476,8 @@ class Diaspora public static function fetchByURL($url, $uid = 0) { // Check for Diaspora (and Friendica) typical paths - if (!preg_match("=(https?://.+)/(?:posts|display)/([a-zA-Z0-9-_@.:%]+[a-zA-Z0-9])=i", $url, $matches)) { + if (!preg_match("=(https?://.+)/(?:posts|display|objects)/([a-zA-Z0-9-_@.:%]+[a-zA-Z0-9])=i", $url, $matches)) { + Logger::info('Invalid url', ['url' => $url]); return false; } @@ -1484,15 +1485,20 @@ class Diaspora $item = Item::selectFirst(['id'], ['guid' => $guid, 'uid' => $uid]); if (DBA::isResult($item)) { + Logger::info('Found', ['id' => $item['id']]); return $item['id']; } - self::storeByGuid($guid, $matches[1], $uid); + Logger::info('Fetch GUID from origin', ['guid' => $guid, 'server' => $matches[1]]); + $ret = self::storeByGuid($guid, $matches[1], $uid); + Logger::info('Result', ['ret' => $ret]); $item = Item::selectFirst(['id'], ['guid' => $guid, 'uid' => $uid]); if (DBA::isResult($item)) { + Logger::info('Found', ['id' => $item['id']]); return $item['id']; } else { + Logger::info('Not found', ['guid' => $guid, 'uid' => $uid]); return false; } } @@ -1575,9 +1581,9 @@ class Diaspora * * @return bool is it a hubzilla server? */ - public static function isRedmatrix($url) + private static function isHubzilla($url) { - return(strstr($url, "/channel/")); + return(strstr($url, '/channel/')); } /** @@ -1594,28 +1600,54 @@ class Diaspora private static function plink($addr, $guid, $parent_guid = '') { $contact = Contact::getDetailsByAddr($addr); + if (empty($contact)) { + Logger::info('No contact data for address', ['addr' => $addr]); + return ''; + } - // Fallback - if (!$contact) { - if ($parent_guid != '') { - return "https://" . substr($addr, strpos($addr, "@") + 1) . "/posts/" . $parent_guid . "#" . $guid; - } else { - return "https://" . substr($addr, strpos($addr, "@") + 1) . "/posts/" . $guid; + if (empty($contact['baseurl'])) { + $contact['baseurl'] = 'https://' . substr($addr, strpos($addr, '@') + 1); + Logger::info('Create baseurl from address', ['baseurl' => $contact['baseurl'], 'url' => $contact['url']]); + } + + $platform = ''; + $gserver = DBA::selectFirst('gserver', ['platform'], ['nurl' => Strings::normaliseLink($contact['baseurl'])]); + if (!empty($gserver['platform'])) { + $platform = strtolower($gserver['platform']); + Logger::info('Detected platform', ['platform' => $platform, 'url' => $contact['url']]); + } + + if (!in_array($platform, ['diaspora', 'friendica', 'hubzilla', 'socialhome'])) { + if (self::isHubzilla($contact['url'])) { + Logger::info('Detected unknown platform as Hubzilla', ['platform' => $platform, 'url' => $contact['url']]); + $platform = 'hubzilla'; + } elseif ($contact['network'] == Protocol::DFRN) { + Logger::info('Detected unknown platform as Friendica', ['platform' => $platform, 'url' => $contact['url']]); + $platform = 'friendica'; } } - if ($contact["network"] == Protocol::DFRN) { - return str_replace("/profile/" . $contact["nick"] . "/", "/display/" . $guid, $contact["url"] . "/"); + if ($platform == 'friendica') { + return str_replace('/profile/' . $contact['nick'] . '/', '/display/' . $guid, $contact['url'] . '/'); + } + + if ($platform == 'hubzilla') { + return $contact['baseurl'] . '/item/' . $guid; } - if (self::isRedmatrix($contact["url"])) { - return $contact["url"] . "/?mid=" . $guid; + if ($platform == 'socialhome') { + return $contact['baseurl'] . '/content/' . $guid; + } + + if ($platform != 'diaspora') { + Logger::info('Unknown platform', ['platform' => $platform, 'url' => $contact['url']]); + return ''; } if ($parent_guid != '') { - return "https://" . substr($addr, strpos($addr, "@") + 1) . "/posts/" . $parent_guid . "#" . $guid; + return $contact['baseurl'] . '/posts/' . $parent_guid . '#' . $guid; } else { - return "https://" . substr($addr, strpos($addr, "@") + 1) . "/posts/" . $guid; + return $contact['baseurl'] . '/posts/' . $guid; } } @@ -1698,6 +1730,7 @@ class Diaspora while ($contact = DBA::fetch($contacts)) { Contact::remove($contact["id"]); } + DBA::close($contacts); DBA::delete('gcontact', ['addr' => $author]); @@ -1775,6 +1808,40 @@ class Diaspora return false; } + /** + * Store the mentions in the tag table + * + * @param integer $uriid + * @param string $text + */ + private static function storeMentions(int $uriid, string $text) + { + preg_match_all('/([@!]){(?:([^}]+?); ?)?([^} ]+)}/', $text, $matches, PREG_SET_ORDER); + if (empty($matches)) { + return; + } + + /* + * Matching values for the preg match + * [1] = mention type (@ or !) + * [2] = name (optional) + * [3] = profile URL + */ + + foreach ($matches as $match) { + if (empty($match)) { + continue; + } + + $person = self::personByHandle($match[3]); + if (empty($person)) { + continue; + } + + Tag::storeByHash($uriid, $match[1], $person['name'] ?: $person['nick'], $person['url']); + } + } + /** * Processes an incoming comment * @@ -1845,6 +1912,7 @@ class Diaspora $datarray["guid"] = $guid; $datarray["uri"] = self::getUriFromGuid($author, $guid); + $datarray['uri-id'] = ItemURI::insert(['uri' => $datarray['uri'], 'guid' => $datarray['guid']]); $datarray["verb"] = Activity::POST; $datarray["gravity"] = GRAVITY_COMMENT; @@ -1863,11 +1931,13 @@ class Diaspora $datarray["changed"] = $datarray["created"] = $datarray["edited"] = $created_at; $datarray["plink"] = self::plink($author, $guid, $parent_item['guid']); - $body = Markdown::toBBCode($text); $datarray["body"] = self::replacePeopleGuid($body, $person["url"]); + self::storeMentions($datarray['uri-id'], $text); + Tag::storeRawTagsFromBody($datarray['uri-id'], $datarray["body"]); + self::fetchGuid($datarray); // If we are the origin of the parent we store the original data. @@ -2190,57 +2260,88 @@ class Diaspora * @param array $importer Array of the importer user * @param object $data The message object * - * @return bool always true + * @return bool success * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ private static function receiveParticipation(array $importer, $data) { $author = strtolower(Strings::escapeTags(XML::unescape($data->author))); + $guid = Strings::escapeTags(XML::unescape($data->guid)); $parent_guid = Strings::escapeTags(XML::unescape($data->parent_guid)); - $contact_id = Contact::getIdForURL($author); - if (!$contact_id) { - Logger::log('Contact not found: '.$author); + $contact = self::allowedContactByHandle($importer, $author, true); + if (!$contact) { return false; } - $person = self::personByHandle($author); - if (!is_array($person)) { - Logger::log("Person not found: ".$author); - return false; + if (self::messageExists($importer["uid"], $guid)) { + return true; } - $item = Item::selectFirst(['id'], ['guid' => $parent_guid, 'origin' => true, 'private' => false]); - if (!DBA::isResult($item)) { - Logger::log('Item not found, no origin or private: '.$parent_guid); + $parent_item = self::parentItem($importer["uid"], $parent_guid, $author, $contact); + if (!$parent_item) { return false; } - $author_parts = explode('@', $author); - if (isset($author_parts[1])) { - $server = $author_parts[1]; - } else { - // Should never happen - $server = $author; + if (!$parent_item['origin']) { + Logger::info('Not our origin. Participation is ignored', ['parent_guid' => $parent_guid, 'guid' => $guid, 'author' => $author]); } - Logger::log('Received participation for ID: '.$item['id'].' - Contact: '.$contact_id.' - Server: '.$server, Logger::DEBUG); + if (!in_array($parent_item['private'], [Item::PUBLIC, Item::UNLISTED])) { + Logger::info('Item is not public, participation is ignored', ['parent_guid' => $parent_guid, 'guid' => $guid, 'author' => $author]); + return false; + } - if (!DBA::exists('participation', ['iid' => $item['id'], 'server' => $server])) { - DBA::insert('participation', ['iid' => $item['id'], 'cid' => $contact_id, 'fid' => $person['id'], 'server' => $server]); + $person = self::personByHandle($author); + if (!is_array($person)) { + Logger::log("Person not found: ".$author); + return false; } + $author_contact = self::authorContactByUrl($contact, $person, $importer["uid"]); + + // Store participation + $datarray = []; + + $datarray["protocol"] = Conversation::PARCEL_DIASPORA; + + $datarray["uid"] = $importer["uid"]; + $datarray["contact-id"] = $author_contact["cid"]; + $datarray["network"] = $author_contact["network"]; + + $datarray["owner-link"] = $datarray["author-link"] = $person["url"]; + $datarray["owner-id"] = $datarray["author-id"] = Contact::getIdForURL($person["url"], 0); + + $datarray["guid"] = $guid; + $datarray["uri"] = self::getUriFromGuid($author, $guid); + + $datarray["verb"] = Activity::FOLLOW; + $datarray["gravity"] = GRAVITY_ACTIVITY; + $datarray["parent-uri"] = $parent_item["uri"]; + + $datarray["object-type"] = Activity\ObjectType::NOTE; + + $datarray["body"] = Activity::FOLLOW; + + // Diaspora doesn't provide a date for a participation + $datarray["changed"] = $datarray["created"] = $datarray["edited"] = DateTimeFormat::utcNow(); + + $message_id = Item::insert($datarray); + + Logger::info('Participation stored', ['id' => $message_id, 'guid' => $guid, 'parent_guid' => $parent_guid, 'author' => $author]); + // Send all existing comments and likes to the requesting server - $comments = Item::select(['id', 'parent', 'verb', 'self'], ['parent' => $item['id']]); + $comments = Item::select(['id', 'uri-id', 'parent', 'verb'], ['parent' => $parent_item['id'], 'gravity' => [GRAVITY_COMMENT, GRAVITY_ACTIVITY]]); while ($comment = Item::fetch($comments)) { - if ($comment['id'] == $comment['parent']) { + if (in_array($comment["verb"], [Activity::FOLLOW, Activity::TAG])) { + Logger::info('participation messages are not relayed', ['item' => $comment['id']]); continue; } - Logger::info('Deliver participation', ['item' => $comment['id'], 'contact' => $contact_id]); - if (Worker::add(PRIORITY_HIGH, 'Delivery', Delivery::POST, $comment['id'], $contact_id)) { - ItemDeliveryData::incrementQueueCount($comment['id'], 1); + Logger::info('Deliver participation', ['item' => $comment['id'], 'contact' => $author_contact["cid"]]); + if (Worker::add(PRIORITY_HIGH, 'Delivery', Delivery::POST, $comment['id'], $author_contact["cid"])) { + Post\DeliveryData::incrementQueueCount($comment['uri-id'], 1); } } DBA::close($comments); @@ -2521,9 +2622,9 @@ class Diaspora } // Do we already have this item? - $fields = ['body', 'title', 'attach', 'tag', 'app', 'created', 'object-type', 'uri', 'guid', + $fields = ['body', 'title', 'attach', 'app', 'created', 'object-type', 'uri', 'guid', 'author-name', 'author-link', 'author-avatar']; - $condition = ['guid' => $guid, 'visible' => true, 'deleted' => false, 'private' => false]; + $condition = ['guid' => $guid, 'visible' => true, 'deleted' => false, 'private' => [Item::PUBLIC, Item::UNLISTED]]; $item = Item::selectFirst($fields, $condition); if (DBA::isResult($item)) { @@ -2565,9 +2666,9 @@ class Diaspora } if ($stored) { - $fields = ['body', 'title', 'attach', 'tag', 'app', 'created', 'object-type', 'uri', 'guid', + $fields = ['body', 'title', 'attach', 'app', 'created', 'object-type', 'uri', 'guid', 'author-name', 'author-link', 'author-avatar']; - $condition = ['guid' => $guid, 'visible' => true, 'deleted' => false, 'private' => false]; + $condition = ['guid' => $guid, 'visible' => true, 'deleted' => false, 'private' => [Item::PUBLIC, Item::UNLISTED]]; $item = Item::selectFirst($fields, $condition); if (DBA::isResult($item)) { @@ -2684,6 +2785,7 @@ class Diaspora $datarray["guid"] = $guid; $datarray["uri"] = $datarray["parent-uri"] = self::getUriFromGuid($author, $guid); + $datarray['uri-id'] = ItemURI::insert(['uri' => $datarray['uri'], 'guid' => $datarray['guid']]); $datarray["verb"] = Activity::POST; $datarray["gravity"] = GRAVITY_PARENT; @@ -2691,6 +2793,8 @@ class Diaspora $datarray["protocol"] = Conversation::PARCEL_DIASPORA; $datarray["source"] = $xml; + /// @todo Copy tag data from original post + $prefix = share_header( $original_item["author-name"], $original_item["author-link"], @@ -2706,12 +2810,13 @@ class Diaspora $datarray["body"] = $prefix.$original_item["body"]."[/share]"; - $datarray["tag"] = $original_item["tag"]; + Tag::storeFromBody($datarray['uri-id'], $datarray["body"]); + $datarray["attach"] = $original_item["attach"]; $datarray["app"] = $original_item["app"]; $datarray["plink"] = self::plink($author, $guid); - $datarray["private"] = (($public == "false") ? 1 : 0); + $datarray["private"] = (($public == "false") ? Item::PRIVATE : Item::PUBLIC); $datarray["changed"] = $datarray["created"] = $datarray["edited"] = $created_at; $datarray["object-type"] = $original_item["object-type"]; @@ -2794,7 +2899,7 @@ class Diaspora continue; } - Item::delete(['id' => $item['id']]); + Item::markForDeletion(['id' => $item['id']]); Logger::log("Deleted target ".$target_guid." (".$item["id"].") from user ".$item["uid"]." parent: ".$item["parent"], Logger::DEBUG); } @@ -2901,7 +3006,7 @@ class Diaspora $datarray["object-type"] = Activity\ObjectType::NOTE; // Add OEmbed and other information to the body - if (!self::isRedmatrix($contact["url"])) { + if (!self::isHubzilla($contact["url"])) { $body = add_page_info_to_body($body, false, true); } } @@ -2927,6 +3032,7 @@ class Diaspora $datarray["guid"] = $guid; $datarray["uri"] = $datarray["parent-uri"] = self::getUriFromGuid($author, $guid); + $datarray['uri-id'] = ItemURI::insert(['uri' => $datarray['uri'], 'guid' => $datarray['guid']]); $datarray["verb"] = Activity::POST; $datarray["gravity"] = GRAVITY_PARENT; @@ -2936,12 +3042,15 @@ class Diaspora $datarray["body"] = self::replacePeopleGuid($body, $contact["url"]); + self::storeMentions($datarray['uri-id'], $text); + Tag::storeRawTagsFromBody($datarray['uri-id'], $datarray["body"]); + if ($provider_display_name != "") { $datarray["app"] = $provider_display_name; } $datarray["plink"] = self::plink($author, $guid); - $datarray["private"] = (($public == "false") ? 1 : 0); + $datarray["private"] = (($public == "false") ? Item::PRIVATE : Item::PUBLIC); $datarray["changed"] = $datarray["created"] = $datarray["edited"] = $created_at; if (isset($address["address"])) { @@ -3245,7 +3354,7 @@ class Diaspora private static function sendParticipation(array $contact, array $item) { // Don't send notifications for private postings - if ($item['private']) { + if ($item['private'] == Item::PRIVATE) { return; } @@ -3536,12 +3645,12 @@ class Diaspora $myaddr = self::myHandle($owner); - $public = ($item["private"] ? "false" : "true"); + $public = ($item["private"] == Item::PRIVATE ? "false" : "true"); $created = DateTimeFormat::utc($item['received'], DateTimeFormat::ATOM); $edited = DateTimeFormat::utc($item["edited"] ?? $item["created"], DateTimeFormat::ATOM); // Detect a share element and do a reshare - if (!$item['private'] && ($ret = self::isReshare($item["body"]))) { + if (($item['private'] != Item::PRIVATE) && ($ret = self::isReshare($item["body"]))) { $message = ["author" => $myaddr, "guid" => $item["guid"], "created_at" => $created, @@ -3910,30 +4019,24 @@ class Diaspora Logger::log("Got relayable data ".$type." for item ".$item["guid"]." (".$item["id"].")", Logger::DEBUG); - // Old way - is used by the internal Friendica functions - /// @todo Change all signatur storing functions to the new format - if ($item['signed_text'] && $item['signature'] && $item['signer']) { - $message = self::messageFromSignature($item); - } else {// New way - $msg = json_decode($item['signed_text'], true); - - $message = []; - if (is_array($msg)) { - foreach ($msg as $field => $data) { - if (!$item["deleted"]) { - if ($field == "diaspora_handle") { - $field = "author"; - } - if ($field == "target_type") { - $field = "parent_type"; - } - } + $msg = json_decode($item['signed_text'], true); - $message[$field] = $data; + $message = []; + if (is_array($msg)) { + foreach ($msg as $field => $data) { + if (!$item["deleted"]) { + if ($field == "diaspora_handle") { + $field = "author"; + } + if ($field == "target_type") { + $field = "parent_type"; + } } - } else { - Logger::log("Signature text for item ".$item["guid"]." (".$item["id"].") couldn't be extracted: ".$item['signed_text'], Logger::DEBUG); + + $message[$field] = $data; } + } else { + Logger::log("Signature text for item ".$item["guid"]." (".$item["id"].") couldn't be extracted: ".$item['signed_text'], Logger::DEBUG); } $message["parent_author_signature"] = self::signature($owner, $message); @@ -4095,20 +4198,11 @@ class Diaspora */ private static function createProfileData($uid) { - $r = q( - "SELECT `profile`.`uid` AS `profile_uid`, `profile`.* , `user`.*, `user`.`prvkey` AS `uprvkey`, `contact`.`addr` - FROM `profile` - INNER JOIN `user` ON `profile`.`uid` = `user`.`uid` - INNER JOIN `contact` ON `profile`.`uid` = `contact`.`uid` - WHERE `user`.`uid` = %d AND `contact`.`self` LIMIT 1", - intval($uid) - ); - - if (!$r) { + $profile = DBA::selectFirst('owner-view', ['uid', 'addr', 'name', 'location', 'net-publish', 'dob', 'about', 'pub_keywords'], ['uid' => $uid]); + if (!DBA::isResult($profile)) { return []; } - $profile = $r[0]; $handle = $profile["addr"]; $split_name = self::splitName($profile['name']); @@ -4135,10 +4229,9 @@ class Diaspora $dob = DateTimeFormat::utc($year . '-' . $month . '-'. $day, 'Y-m-d'); } - $about = $profile['about']; - $about = strip_tags(BBCode::convert($about)); + $about = BBCode::toMarkdown($profile['about']); - $location = Profile::formatLocation($profile); + $location = $profile['location']; $tags = ''; if ($profile['pub_keywords']) { $kw = str_replace(',', ' ', $profile['pub_keywords']);