microformats.php 23.2 KB
Newer Older
1 2
<?php
// Dobrado Content Management System
3
// Copyright (C) 2018 Malcolm Blaney
4 5 6 7 8 9 10 11 12 13 14 15 16 17
//
// 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/>.

18 19 20 21
function discover_endpoint($url, $rels) {
  $result = [];
  if (is_string($rels)) $rels = [$rels];
  $url = trim($url);
22
  if (stripos($url, 'http') !== 0) $url = 'http://' . $url;
23 24 25 26 27
  $ch = curl_init($url);
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
  curl_setopt($ch, CURLOPT_MAXREDIRS, 5);
  curl_setopt($ch, CURLOPT_TIMEOUT, 20);
28
  curl_setopt($ch, CURLOPT_ENCODING, '');
29 30 31 32 33
  curl_setopt($ch, CURLOPT_HEADER, true);
  curl_setopt($ch, CURLOPT_HEADERFUNCTION,
    function($ch, $header) use($rels, &$result) {
      foreach ($rels as $name) {
        if (stripos($header, 'Link:') === 0 && stripos($header, $name)) {
34
          $regex = '/<([^>]+)>; rel="?(([^"]* )?' . $name . '( [^"]*)?)"?/i';
35 36 37 38 39 40 41 42 43 44
          if (preg_match($regex, $header, $match)) {
            if (in_array($name, explode(' ', strtolower($match[2])))) {
              // This makes sure the first discovered endpoint is used.
              if (!isset($result[$name])) $result[$name] = $match[1];
            }
          }
        }
      }
      return strlen($header);
    });
45 46 47
  $response = curl_exec($ch);
  // Need to remove headers from the response as it can mess with the parser.
  $body = substr($response, curl_getinfo($ch, CURLINFO_HEADER_SIZE));
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
  $content_type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
  curl_close($ch);

  // This is a special case for web actions, if the provided url returns JSON
  // then the endpoint was given and the config can be returned here.
  if ($rels[0] === 'webaction' && $content_type === 'application/json') {
    return $body;
  }

  // Check the body for requested endpoints that weren't discovered in headers.
  if (count($result) !== count($rels)) {
    $doc = new DOMDocument();
    @$doc->loadHTML($body);
    $xpath = new DOMXpath($doc);
    $query = '//a[@rel and @href] | //link[@rel and @href]';
    foreach ($xpath->query($query) as $link) {
      $rel_values = explode(' ', strtolower($link->getAttribute('rel')));
      foreach ($rels as $name) {
        if (!isset($result[$name]) && in_array($name, $rel_values)) {
          $result[$name] = trim($link->getAttribute('href'));
          if ($result[$name] === '') $result[$name] = $url;
        }
      }
    }
  }
  // Make sure all requests are returned and all discovered endpoints are valid.
  // Turn relative endpoints into absolute ones using the given url.
  $regex = '/^(?:https?:\/\/)?[\/]*([^\/]+)/i';
  foreach ($rels as $name) {
    if (!isset($result[$name])) {
      $result[$name] = '';
    }
    else if (stripos($result[$name], 'http') !== 0) {
      if (preg_match('/^(https?:\/\/[^\/]+)/i', $url, $match)) {
        if (strpos($result[$name], '/') === 0) {
83
          $result[$name] = $match[1] . $result[$name];
84 85 86 87 88
        }
        else {
          // No leading slash means the endpoint is relative to the url path,
          // so match everything except for the last path segment.
          if (preg_match('/^(https?:\/\/.+\/)[^\/]*$/i', $url, $match)) {
89
            $result[$name] = $match[1] . $result[$name];
90 91 92
          }
          else {
            // This catches a url without any path.
93
            $result[$name] = $url . '/' . $result[$name];
94 95 96 97
          }
        }
      }
    }
98 99 100 101 102 103
    else if (preg_match($regex, $result[$name], $match)) {
      if (strpos($match[1], '127.') === 0 ||
          strtolower($match[1]) === 'localhost') {
        $result[$name] = 'Invalid ' . $name . ' url found at ' . $url;
      }
    }
104 105 106 107 108
  }
  // Another special case for web actions, fetch the config from the endpoint.
  if ($rels[0] === 'webaction') {
    if ($result['webaction'] === '' ||
        stripos($result['webaction'], 'http') !== 0) {
109
      return json_encode('config not found.');
110 111 112 113 114 115 116
    }

    $ch = curl_init($result['webaction']);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
    curl_setopt($ch, CURLOPT_MAXREDIRS, 5);
    curl_setopt($ch, CURLOPT_TIMEOUT, 20);
117
    curl_setopt($ch, CURLOPT_ENCODING, '');
118 119 120 121 122 123
    $body = curl_exec($ch);
    $content_type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
    curl_close($ch);
    if ($content_type === 'application/json') {
      return $body;
    }
124
    return json_encode('config not found.');
125 126
  }

127
  // Also try looking for a canonical h-card to add to the nickname cache.
128
  include_once 'library/Masterminds/HTML5.auto.php';
129 130
  include_once 'library/Mf2/Parser.php';
  $mf = Mf2\parse($body, $url);
131 132 133
  // Use SimplePie to cache images.
  include_once 'autoloader.php';
  $simple_pie = new SimplePie();
134 135 136 137
  $scheme = $this->user->config->Secure() ? 'https://' : 'http://';
  $handler = $scheme . $this->user->config->ServerName() . '/php/image.php';
  // The full image handler url is required for Microsub clients.
  $simple_pie->set_image_handler($handler);
138
  $simple_pie->init();
139 140 141
  // Use the url without the scheme for more lenient matching.
  $domain = $url;
  if (preg_match('/^https?:\/\/(.+)/i', $url, $match)) {
142
    $domain = trim($match[1], ' /');
143
  }
144 145 146 147 148
  // Only interested in an h-card with a url property matching the url
  // provided as a parameter.
  foreach ($mf['items'] as $mf_item) {
    if (!isset($mf_item['type'])) continue;

149
    $hcard = NULL;
150 151 152 153 154 155 156 157 158 159
    if (in_array('h-card', $mf_item['type']) &&
        isset($mf_item['properties']['url'])) {
      $hcard = $mf_item;
    }
    else if (isset($mf_item['properties']['author'])) {
      foreach ($mf_item['properties']['author'] as $author) {
        if (isset($author['type']) &&
            in_array('h-card', $author['type']) &&
            isset($author['properties']['url'])) {
          $hcard = $author;
160 161 162
          break;
        }
      }
163 164
    }
    if (!isset($hcard)) continue;
165

166 167
    $match = false;
    foreach ($hcard['properties']['url'] as $canonical) {
168 169
      if (stripos($canonical, 'http://' . $domain) === 0 ||
          stripos($canonical, 'https://' . $domain) === 0) {
170 171 172
        $url = $canonical;
        $match = true;
        break;
173
      }
174
    }
175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199
    if (!$match) continue;

    $properties = $hcard['properties'];
    $mysqli = connect_db();
    $name = isset($properties['name'][0]) ?
      $mysqli->escape_string($properties['name'][0]) : '';
    $nickname = isset($properties['nickname'][0]) ?
      $mysqli->escape_string($properties['nickname'][0]) : '';
    $us_photo = isset($properties['photo'][0]) ? $properties['photo'][0] : '';
    if ($us_photo !== '') {
      $us_photo = $simple_pie->sanitize($us_photo, SIMPLEPIE_CONSTRUCT_IRI, '',
                                        true);
    }
    $photo = $mysqli->escape_string($us_photo);
    $reachable = stripos($result['webmention'], 'http') === 0 ? '1' : '0';
    $query = 'INSERT INTO nickname VALUES ("' . $name . '", ' .
      '"' . $mysqli->escape_string($url) . '", "' . $photo . '", ' .
      '"' . $nickname . '", ' . $reachable . ') ON DUPLICATE KEY UPDATE ' .
      'name = "' . $name . '", photo = "' . $photo . '", ' .
      'nickname = "' . $nickname . '", reachable = ' . $reachable;
    if (!$mysqli->query($query)) {
      log_db('microformats->discover_endpoint: ' . $mysqli->error);
    }
    $mysqli->close();
    break;
200
  }
201

202
  // If only one endpoint was requested just return the result.
203
  if (count($rels) === 1) return $result[$rels[0]];
204 205 206
  return $result;
}

207
function parse_comments($comment_list) {
208 209 210
  include_once 'library/Masterminds/HTML5.auto.php';
  include_once 'library/Mf2/Parser.php';

211 212 213 214 215 216 217 218 219 220 221
  $result = [];
  if (is_array($comment_list)) {
    foreach ($comment_list as $comment) {
      if (isset($comment['type']) && (in_array('h-entry', $comment['type']) ||
                                      in_array('h-cite', $comment['type']))) {
        $author = 'someone';
        $author_photo = '';
        $author_url = '';
        $url = '';
        $content = '';
        $date = '';
222 223 224
        $currency = '';
        $payment = '';
        $amount = '';
225 226 227 228 229
        if (isset($comment['properties']['author'][0])) {
          $author = $comment['properties']['author'][0];
          // author is a special case, it can be plain text or an h-card array.
          // If it's plain text it can also be a url that should be followed to
          // get the actual h-card.
230
          if (!is_string($author)) {
231 232
            list($author, $author_photo, $author_url) = parse_hcard($author);
          }
233
          else if (stripos($author, 'http') === 0) {
234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252
            $mf = Mf2\fetch($author);
            foreach ($mf['items'] as $hcard) {
              // Only interested in an h-card by itself in this case.
              if (!in_array('h-card', $hcard['type'])) {
                continue;
              }
              // It must have a url property matching what we fetched.
              if (!isset($hcard['properties']['url'][0]) ||
                  $hcard['properties']['url'][0] !== $author) {
                continue;
              }
              list($author, $author_photo, $author_url) = parse_hcard($hcard);
              break;
            }
          }
        }
        if (isset($comment['properties']['url'][0])) {
          $url = $comment['properties']['url'][0];
        }
253 254 255
        if (isset($comment['properties']['currency'][0])) {
          $currency = $comment['properties']['currency'][0];
        }
256 257 258 259 260 261 262 263 264 265 266 267
        if (isset($comment['properties']['published'][0])) {
          $date = $comment['properties']['published'][0];
        }
        if (isset($comment['properties']['content'][0])) {
          // Check e-content (html) over p-content (plain text).
          if (isset($comment['properties']['content'][0]['html'])) {
            $content = $comment['properties']['content'][0]['html'];
          }
          else if (is_string($comment['properties']['content'][0])) {
            $content = $comment['properties']['content'][0];
          }
        }
268 269 270 271 272 273 274 275
        if (isset($comment['properties']['debit'][0])) {
          $payment = 'debit';
          $amount = $comment['properties']['debit'][0];
        }
        else if (isset($comment['properties']['credit'][0])) {
          $payment = 'credit';
          $amount = $comment['properties']['credit'][0];
        }
276
        $result[] = ['url' => $url, 'author-name' => $author,
277 278
                     'author-photo' => $author_photo,
                     'author-url' => $author_url, 'content' => $content,
279 280
                     'date' => $date, 'currency' => $currency,
                     'payment' => $payment, 'amount' => $amount];
281 282 283 284 285 286 287 288 289
      }
    }
  }
  else if (is_string($comment_list)) {
    $result[] = ['content' => $comment_list];
  }
  return $result;
}

290
function parse_happ($source) {
291 292 293
  include_once 'library/Masterminds/HTML5.auto.php';
  include_once 'library/Mf2/Parser.php';

294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313
  $mf = Mf2\fetch($source);
  $h_app = [];
  foreach ($mf['items'] as $mf_item) {
    if (!isset($mf_item['type'])) continue;

    if (in_array('h-app', $mf_item['type']) ||
        in_array('h-x-app', $mf_item['type'])) {
      $h_app = $mf_item;
      break;
    }
  }
  $name = isset($h_app['properties']['name'][0]) ?
    $h_app['properties']['name'][0] : '';
  $logo = isset($h_app['properties']['logo'][0]) ?
    $h_app['properties']['logo'][0] : '';
  $url = isset($h_app['properties']['url'][0]) ?
    $h_app['properties']['url'][0] : '';
  return ['name' => $name, 'logo' => $logo, 'url' => $url];
}

314
function parse_hcard($author, $name_only = false) {
315 316 317 318
  $us_author_name = '';
  $us_photo = '';
  $us_author_photo = '';
  $us_author_url = '';
319 320 321

  if (isset($author['type']) && in_array('h-card', $author['type'])) {
    if (isset($author['properties']['photo'][0])) {
322 323 324 325 326
      $us_photo = $author['properties']['photo'][0];
      if ($us_photo !== '') {
        // Use SimplePie to cache images.
        include_once 'autoloader.php';
        $simple_pie = new SimplePie();
327 328 329 330 331
        $scheme = $this->user->config->Secure() ? 'https://' : 'http://';
        $handler = $scheme . $this->user->config->ServerName() .
          '/php/image.php';
        // The full image handler url is required for Microsub clients.
        $simple_pie->set_image_handler($handler);
332 333 334 335 336 337
        $simple_pie->init();
        $us_photo = $simple_pie->sanitize($us_photo, SIMPLEPIE_CONSTRUCT_IRI,
                                          '', true);
        // img tag is stored to match Detail module for local accounts.
        $us_author_photo = '<img class="thumb u-photo" src="' . $us_photo .'">';
      }
338 339
    }
    if (isset($author['properties']['url'][0])) {
340
      $us_author_url = $author['properties']['url'][0];
341 342 343
      if (stripos($us_author_url, 'http') !== 0) {
        $us_author_url = 'http://' . $us_author_url;
      }
344
    }
345
    if (isset($author['properties']['name'][0])) {
346
      $us_author_name = $author['properties']['name'][0];
347
      $mysqli = connect_db();
348 349 350
      $name = $mysqli->escape_string($us_author_name);
      $photo = $mysqli->escape_string($us_photo);
      $url = $mysqli->escape_string($us_author_url);
351 352
      $nickname = isset($properties['nickname'][0]) ?
        $mysqli->escape_string($properties['nickname'][0]) : '';
353 354
      // Don't replace the photo if not found on this h-card.
      $update_photo_query = $photo === '' ? '' : ', photo = "' . $photo . '"';
355 356
      $query = 'INSERT INTO nickname VALUES ("' . $name . '", "' . $url . '", '.
        '"' . $photo . '", "' . $nickname . '", 0) ON DUPLICATE KEY UPDATE ' .
357
        'name = "' . $name . '"' . $update_photo_query;
358
      if (!$mysqli->query($query)) {
359
        log_db('microformats->parse_hcard: ' . $mysqli->error);
360 361 362
      }
      $mysqli->close();
    }
363
  }
364 365
  else if (isset($author['value'])) {
    $us_author_name = $author['value'];
366
  }
367 368
  return $name_only ? $us_author_name :
    [$us_author_name, $us_author_photo, $us_author_url];
369 370
}

371 372 373 374 375 376 377 378 379
function parse_hcite($target, $hcite_list) {
  foreach ($hcite_list as $hcite) {
    if (isset($hcite['type']) && in_array('h-cite', $hcite['type']) &&
        isset($hcite['properties']['url']) &&
        in_array($target, $hcite['properties']['url'])) {
      return true;
    }
  }
  return false;
380 381
}

382
function parse_hentry($source, $target = '', $result = '') {
383
  include_once 'library/Masterminds/HTML5.auto.php';
384
  include_once 'library/Mf2/Parser.php';
385 386 387 388 389

  $mention = 'tag';
  $author = 'someone';
  $author_photo = '';
  $author_url = '';
390
  $uid = '';
391 392 393
  $title = '';
  $content = '';
  $category = '';
394
  $currency = '';
395
  $date = '';
396 397 398 399 400 401
  $tag_author = '';
  $tag_photo = '';
  $tag_url = '';
  $payment = '';
  $amount = '';
  $in_reply_to = '';
402 403 404
  $comment = [];
  $like = [];
  $share = [];
405 406
  $mf = $result === '' ? Mf2\fetch($source) : Mf2\parse($result, $source);

407
  // First look for an h-feed.
408
  $h_feed = [];
409 410
  foreach ($mf['items'] as $mf_item) {
    if (!isset($mf_item['type'])) continue;
411

412 413 414 415 416 417 418 419 420 421 422
    if (in_array('h-feed', $mf_item['type'])) {
      $h_feed = $mf_item;
      break;
    }
    // Also look for an h-feed in the children of each top level item.
    if (!isset($mf_item['children'][0]['type'])) continue;
    if (in_array('h-feed', $mf_item['children'][0]['type'])) {
      $h_feed = $mf_item['children'][0];
      break;
    }
  }
423

424 425
  $entries = isset($h_feed['children']) ? $h_feed['children'] : $mf['items'];
  foreach ($entries as $entry) {
426 427 428 429 430 431 432 433 434 435
    if (!isset($entry['type']) || (!in_array('h-entry', $entry['type']) &&
                                   !in_array('h-cite', $entry['type']))) {
      continue;
    }

    if (isset($entry['properties']['like-of'])) {
      $like_of = $entry['properties']['like-of'];
      // like may not be an array of urls, so check for h-cite first.
      if (parse_hcite($target, $like_of) || in_array($target, $like_of)) {
        $mention = 'star';
436
      }
437 438 439 440 441
    }
    else if (isset($entry['properties']['repost-of'])) {
      $repost = $entry['properties']['repost-of'];
      if (parse_hcite($target, $repost) || in_array($target, $repost)) {
        $mention = 'share';
442
      }
443 444 445 446 447
    }
    else if (isset($entry['properties']['in-reply-to'])) {
      $reply = $entry['properties']['in-reply-to'];
      if (parse_hcite($target, $reply) || in_array($target, $reply)) {
        $mention = 'comment';
448
      }
449 450 451 452
      // Want to return the in-reply-to url but it doesn't have to be the
      // target, so just return the first value found.
      if (is_string($reply[0])) {
        $in_reply_to = $reply[0];
453
      }
454 455 456 457
      else if (isset($reply[0]['type']) &&
               in_array('h-cite', $reply[0]['type']) &&
               isset($reply[0]['properties']['url'][0])) {
        $in_reply_to = $reply[0]['properties']['url'][0];
458
      }
459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474
    }
    if (isset($entry['properties']['debit'][0])) {
      $payment = 'debit';
      $amount = $entry['properties']['debit'][0];
    }
    else if (isset($entry['properties']['credit'][0])) {
      $payment = 'credit';
      $amount = $entry['properties']['credit'][0];
    }
    if (isset($entry['properties']['author'][0])) {
      $author = $entry['properties']['author'][0];
      // author is a special case, it can be plain text or an h-card array.
      // If it's plain text it can also be a url that should be followed to
      // get the actual h-card.
      if (!is_string($author)) {
        list($author, $author_photo, $author_url) = parse_hcard($author);
475
      }
476 477 478 479 480 481 482 483 484 485 486 487
      else if (stripos($author, 'http') === 0) {
        $mf = Mf2\fetch($author);
        foreach ($mf['items'] as $hcard) {
          // Only interested in an h-card by itself in this case.
          if (!in_array('h-card', $hcard['type'])) continue;
          // It must have a url property matching what we fetched.
          if (!isset($hcard['properties']['url']) ||
              !in_array($author, $hcard['properties']['url'])) {
            continue;
          }
          list($author, $author_photo, $author_url) = parse_hcard($hcard);
          break;
488
        }
489
      }
490 491 492 493 494
    }
    if (isset($entry['properties']['url'][0])) {
      // Changing the source url should only be permitted by trusted sites.
      if (stripos($source, 'https://brid.gy') === 0) {
        $source = $entry['properties']['url'][0];
495
      }
496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517
    }
    if (isset($entry['properties']['name'][0])) {
      $title = $entry['properties']['name'][0];
    }
    if (isset($entry['properties']['uid'][0])) {
      $uid = $entry['properties']['uid'][0];
    }
    if (isset($entry['properties']['category'])) {
      foreach ($entry['properties']['category'] as $tag) {
        if ($category !== '') $category .= ',';
        if (is_string($tag)) {
          $category .= $tag;
        }
        else {
          if (isset($tag['value'])) $category .= $tag['value'];
          // Test this tag as an h-card to update the mention type, also want
          // to know about a person tag in case we get a reply from them.
          list($tag_author, $tag_photo, $tag_url) = parse_hcard($tag);
          if ($target !== '' && $tag_url === $target) {
            // Also need to know if this person tag should be shown as a
            // comment.
            $mention = $mention === 'comment' ? 'tag-comment' : 'tag-person';
518 519
          }
        }
520
      }
521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539
    }
    if (isset($entry['properties']['currency'][0])) {
      $currency = $entry['properties']['currency'][0];
    }
    if (isset($entry['properties']['published'][0])) {
      $date = $entry['properties']['published'][0];
    }
    $use_content = true;
    if (isset($entry['properties']['summary'][0])) {
      // Use summary as content if there are any unrecognized properties.
      $known_properties = ['like-of', 'repost-of', 'in-reply-to', 'url',
                           'debit', 'credit', 'author', 'name', 'uid',
                           'category', 'currency', 'published', 'summary',
                           'content', 'photo', 'comment', 'like', 'repost'];
      foreach ($entry['properties'] as $name => $property) {
        if (!in_array($name, $known_properties)) {
          $use_content = false;
          break;
        }
540
      }
541 542 543
      if (!$use_content) {
        if (isset($entry['properties']['summary'][0]['html'])) {
          $content = $entry['properties']['summary'][0]['html'];
544
        }
545 546
        else if (is_string($entry['properties']['summary'][0])) {
          $content = $entry['properties']['summary'][0];
547 548
        }
      }
549 550 551 552 553
    }
    if ($use_content && isset($entry['properties']['content'][0])) {
      // Check e-content (html) over p-content (plain text).
      if (isset($entry['properties']['content'][0]['html'])) {
        $content = $entry['properties']['content'][0]['html'];
554
      }
555 556
      else if (is_string($entry['properties']['content'][0])) {
        $content = $entry['properties']['content'][0];
557
      }
558 559 560 561 562 563 564
    }
    if (isset($entry['properties']['photo'][0])) {
      $photo_list = [];
      foreach ($entry['properties']['photo'] as $photo) {
        if (!empty($photo) && strpos($content, $photo) === false) {
          $photo_list[] = $photo;
        }
565
      }
566 567 568 569 570 571 572
      if (count($photo_list) === 0) {
        // All photos were found to be duplicated in content, so assume this
        // is a photo post and display the lightbox. Add a special wrapper
        // around the existing content so those photos can be hidden.
        $content = '<div class="photo-hidden">' . $content . '</div>';
        $photo_list = $entry['properties']['photo'];
      }
573 574
      $count = count($photo_list);
      if ($count > 1) {
575 576 577
        // When there's more than one photo show the first two and use a
        // lightbox. Need a permanent, unique name for the image set, but
        // don't have anything unique except for photo urls, so use that.
578
        $image_set_id = preg_replace('/[[:^alnum:]]/', '', $photo_list[0]);
579
        $content .= '<p class="photo-list">';
580
        for ($i = 0; $i < $count; $i++) {
581
          $hidden = $i <= 1 ? '' : 'class="hidden" ';
582 583 584 585 586 587 588 589 590 591 592 593 594
          $content .= '<a href="' . $photo_list[$i] . '" ' . $hidden .
            'data-lightbox="image-set-' . $image_set_id . '">' .
            '<img src="' . $photo_list[$i] . '"></a>';
        }
        $content .= '<br><b>' . $count . ' photos</b></p>';
      }
      else if ($count == 1) {
        $content .= '<p><img src="' . $photo_list[0] . '"></p>';
      }
    }
    // Check for children of this entry, either comment, like or repost.
    if (isset($entry['properties']['comment'])) {
      $comment = parse_comments($entry['properties']['comment']);
595
    }
596 597 598 599 600 601 602
    if (isset($entry['properties']['like'])) {
      $like = parse_comments($entry['properties']['like']);
    }
    if (isset($entry['properties']['repost'])) {
      $share = parse_comments($entry['properties']['repost']);
    }
    break;
603
  }
604 605
  return ['url' => $source, 'mention' => $mention, 'author-name' => $author,
          'author-photo' => $author_photo, 'author-url' => $author_url,
606
          'uid' => $uid, 'content' => $content, 'title' => $title,
607 608 609 610
          'category' => $category, 'date' => $date, 'comment' => $comment,
          'like' => $like, 'share' => $share, 'payment' => $payment,
          'amount' => $amount, 'currency' => $currency,
          'in-reply-to' => $in_reply_to];
611
}