PageRenderTime 51ms CodeModel.GetById 9ms RepoModel.GetById 0ms app.codeStats 1ms

/lib/activityobject.php

https://gitlab.com/windigo-gs/windigos-gnu-social
PHP | 999 lines | 676 code | 210 blank | 113 comment | 132 complexity | b0bd20e6e9d4a888e8db0f1bc7a34d2c MD5 | raw file
Possible License(s): AGPL-3.0, BSD-3-Clause, GPL-2.0
  1. <?php
  2. /**
  3. * StatusNet, the distributed open-source microblogging tool
  4. *
  5. * An activity
  6. *
  7. * PHP version 5
  8. *
  9. * LICENCE: This program is free software: you can redistribute it and/or modify
  10. * it under the terms of the GNU Affero General Public License as published by
  11. * the Free Software Foundation, either version 3 of the License, or
  12. * (at your option) any later version.
  13. *
  14. * This program is distributed in the hope that it will be useful,
  15. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. * GNU Affero General Public License for more details.
  18. *
  19. * You should have received a copy of the GNU Affero General Public License
  20. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  21. *
  22. * @category Feed
  23. * @package StatusNet
  24. * @author Evan Prodromou <evan@status.net>
  25. * @author Zach Copley <zach@status.net>
  26. * @copyright 2010 StatusNet, Inc.
  27. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
  28. * @link http://status.net/
  29. */
  30. if (!defined('STATUSNET')) {
  31. exit(1);
  32. }
  33. /**
  34. * A noun-ish thing in the activity universe
  35. *
  36. * The activity streams spec talks about activity objects, while also having
  37. * a tag activity:object, which is in fact an activity object. Aaaaaah!
  38. *
  39. * This is just a thing in the activity universe. Can be the subject, object,
  40. * or indirect object (target!) of an activity verb. Rotten name, and I'm
  41. * propagating it. *sigh*
  42. *
  43. * @category OStatus
  44. * @package StatusNet
  45. * @author Evan Prodromou <evan@status.net>
  46. * @copyright 2010 StatusNet, Inc.
  47. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
  48. * @link http://status.net/
  49. */
  50. class ActivityObject
  51. {
  52. const ARTICLE = 'http://activitystrea.ms/schema/1.0/article';
  53. const BLOGENTRY = 'http://activitystrea.ms/schema/1.0/blog-entry';
  54. const NOTE = 'http://activitystrea.ms/schema/1.0/note';
  55. const STATUS = 'http://activitystrea.ms/schema/1.0/status';
  56. const FILE = 'http://activitystrea.ms/schema/1.0/file';
  57. const PHOTO = 'http://activitystrea.ms/schema/1.0/photo';
  58. const ALBUM = 'http://activitystrea.ms/schema/1.0/photo-album';
  59. const PLAYLIST = 'http://activitystrea.ms/schema/1.0/playlist';
  60. const VIDEO = 'http://activitystrea.ms/schema/1.0/video';
  61. const AUDIO = 'http://activitystrea.ms/schema/1.0/audio';
  62. const BOOKMARK = 'http://activitystrea.ms/schema/1.0/bookmark';
  63. const PERSON = 'http://activitystrea.ms/schema/1.0/person';
  64. const GROUP = 'http://activitystrea.ms/schema/1.0/group';
  65. const _LIST = 'http://activitystrea.ms/schema/1.0/list'; // LIST is reserved
  66. const PLACE = 'http://activitystrea.ms/schema/1.0/place';
  67. const COMMENT = 'http://activitystrea.ms/schema/1.0/comment';
  68. // ^^^^^^^^^^ tea!
  69. const ACTIVITY = 'http://activitystrea.ms/schema/1.0/activity';
  70. const SERVICE = 'http://activitystrea.ms/schema/1.0/service';
  71. const IMAGE = 'http://activitystrea.ms/schema/1.0/image';
  72. const COLLECTION = 'http://activitystrea.ms/schema/1.0/collection';
  73. const APPLICATION = 'http://activitystrea.ms/schema/1.0/application';
  74. // Atom elements we snarf
  75. const TITLE = 'title';
  76. const SUMMARY = 'summary';
  77. const ID = 'id';
  78. const SOURCE = 'source';
  79. const NAME = 'name';
  80. const URI = 'uri';
  81. const EMAIL = 'email';
  82. const POSTEROUS = 'http://posterous.com/help/rss/1.0';
  83. const AUTHOR = 'author';
  84. const USERIMAGE = 'userImage';
  85. const PROFILEURL = 'profileUrl';
  86. const NICKNAME = 'nickName';
  87. const DISPLAYNAME = 'displayName';
  88. public $element;
  89. public $type;
  90. public $id;
  91. public $title;
  92. public $summary;
  93. public $content;
  94. public $owner;
  95. public $link;
  96. public $source;
  97. public $avatarLinks = array();
  98. public $geopoint;
  99. public $poco;
  100. public $displayName;
  101. // @todo move this stuff to it's own PHOTO activity object
  102. const MEDIA_DESCRIPTION = 'description';
  103. public $thumbnail;
  104. public $largerImage;
  105. public $description;
  106. public $extra = array();
  107. public $stream;
  108. /**
  109. * Constructor
  110. *
  111. * This probably needs to be refactored
  112. * to generate a local class (ActivityPerson, ActivityFile, ...)
  113. * based on the object type.
  114. *
  115. * @param DOMElement $element DOM thing to turn into an Activity thing
  116. */
  117. function __construct($element = null)
  118. {
  119. if (empty($element)) {
  120. return;
  121. }
  122. $this->element = $element;
  123. $this->geopoint = $this->_childContent(
  124. $element,
  125. ActivityContext::POINT,
  126. ActivityContext::GEORSS
  127. );
  128. if ($element->tagName == 'author') {
  129. $this->_fromAuthor($element);
  130. } else if ($element->tagName == 'item') {
  131. $this->_fromRssItem($element);
  132. } else {
  133. $this->_fromAtomEntry($element);
  134. }
  135. // Some per-type attributes...
  136. if ($this->type == self::PERSON || $this->type == self::GROUP) {
  137. $this->displayName = $this->title;
  138. $photos = ActivityUtils::getLinks($element, 'photo');
  139. if (count($photos)) {
  140. foreach ($photos as $link) {
  141. $this->avatarLinks[] = new AvatarLink($link);
  142. }
  143. } else {
  144. $avatars = ActivityUtils::getLinks($element, 'avatar');
  145. foreach ($avatars as $link) {
  146. $this->avatarLinks[] = new AvatarLink($link);
  147. }
  148. }
  149. $this->poco = new PoCo($element);
  150. }
  151. if ($this->type == self::PHOTO) {
  152. $this->thumbnail = ActivityUtils::getLink($element, 'preview');
  153. $this->largerImage = ActivityUtils::getLink($element, 'enclosure');
  154. $this->description = ActivityUtils::childContent(
  155. $element,
  156. ActivityObject::MEDIA_DESCRIPTION,
  157. Activity::MEDIA
  158. );
  159. }
  160. if ($this->type == self::_LIST) {
  161. $owner = ActivityUtils::child($this->element, Activity::AUTHOR, Activity::SPEC);
  162. $this->owner = new ActivityObject($owner);
  163. }
  164. }
  165. private function _fromAuthor($element)
  166. {
  167. $this->type = $this->_childContent($element,
  168. Activity::OBJECTTYPE,
  169. Activity::SPEC);
  170. if (empty($this->type)) {
  171. $this->type = self::PERSON; // XXX: is this fair?
  172. }
  173. // start with <atom:title>
  174. $title = ActivityUtils::childHtmlContent($element, self::TITLE);
  175. if (!empty($title)) {
  176. $this->title = html_entity_decode(strip_tags($title), ENT_QUOTES, 'UTF-8');
  177. }
  178. // fall back to <atom:name>
  179. if (empty($this->title)) {
  180. $this->title = $this->_childContent($element, self::NAME);
  181. }
  182. // start with <atom:id>
  183. $this->id = $this->_childContent($element, self::ID);
  184. // fall back to <atom:uri>
  185. if (empty($this->id)) {
  186. $this->id = $this->_childContent($element, self::URI);
  187. }
  188. // fall further back to <atom:email>
  189. if (empty($this->id)) {
  190. $email = $this->_childContent($element, self::EMAIL);
  191. if (!empty($email)) {
  192. // XXX: acct: ?
  193. $this->id = 'mailto:'.$email;
  194. }
  195. }
  196. $this->link = ActivityUtils::getPermalink($element);
  197. // fall finally back to <link rel=alternate>
  198. if (empty($this->id) && !empty($this->link)) { // fallback if there's no ID
  199. $this->id = $this->link;
  200. }
  201. }
  202. private function _fromAtomEntry($element)
  203. {
  204. $this->type = $this->_childContent($element, Activity::OBJECTTYPE,
  205. Activity::SPEC);
  206. if (empty($this->type)) {
  207. $this->type = ActivityObject::NOTE;
  208. }
  209. $this->summary = ActivityUtils::childHtmlContent($element, self::SUMMARY);
  210. $this->content = ActivityUtils::getContent($element);
  211. // We don't like HTML in our titles, although it's technically allowed
  212. $title = ActivityUtils::childHtmlContent($element, self::TITLE);
  213. $this->title = html_entity_decode(strip_tags($title), ENT_QUOTES, 'UTF-8');
  214. $this->source = $this->_getSource($element);
  215. $this->link = ActivityUtils::getPermalink($element);
  216. $this->id = $this->_childContent($element, self::ID);
  217. if (empty($this->id) && !empty($this->link)) { // fallback if there's no ID
  218. $this->id = $this->link;
  219. }
  220. }
  221. // @todo FIXME: rationalize with Activity::_fromRssItem()
  222. private function _fromRssItem($item)
  223. {
  224. $this->title = ActivityUtils::childContent($item, ActivityObject::TITLE, Activity::RSS);
  225. $contentEl = ActivityUtils::child($item, ActivityUtils::CONTENT, Activity::CONTENTNS);
  226. if (!empty($contentEl)) {
  227. $this->content = htmlspecialchars_decode($contentEl->textContent, ENT_QUOTES);
  228. } else {
  229. $descriptionEl = ActivityUtils::child($item, Activity::DESCRIPTION, Activity::RSS);
  230. if (!empty($descriptionEl)) {
  231. $this->content = htmlspecialchars_decode($descriptionEl->textContent, ENT_QUOTES);
  232. }
  233. }
  234. $this->link = ActivityUtils::childContent($item, ActivityUtils::LINK, Activity::RSS);
  235. $guidEl = ActivityUtils::child($item, Activity::GUID, Activity::RSS);
  236. if (!empty($guidEl)) {
  237. $this->id = $guidEl->textContent;
  238. if ($guidEl->hasAttribute('isPermaLink')) {
  239. // overwrites <link>
  240. $this->link = $this->id;
  241. }
  242. }
  243. }
  244. public static function fromRssAuthor($el)
  245. {
  246. $text = $el->textContent;
  247. if (preg_match('/^(.*?) \((.*)\)$/', $text, $match)) {
  248. $email = $match[1];
  249. $name = $match[2];
  250. } else if (preg_match('/^(.*?) <(.*)>$/', $text, $match)) {
  251. $name = $match[1];
  252. $email = $match[2];
  253. } else if (preg_match('/.*@.*/', $text)) {
  254. $email = $text;
  255. $name = null;
  256. } else {
  257. $name = $text;
  258. $email = null;
  259. }
  260. // Not really enough info
  261. $obj = new ActivityObject();
  262. $obj->element = $el;
  263. $obj->type = ActivityObject::PERSON;
  264. $obj->title = $name;
  265. if (!empty($email)) {
  266. $obj->id = 'mailto:'.$email;
  267. }
  268. return $obj;
  269. }
  270. public static function fromDcCreator($el)
  271. {
  272. // Not really enough info
  273. $text = $el->textContent;
  274. $obj = new ActivityObject();
  275. $obj->element = $el;
  276. $obj->title = $text;
  277. $obj->type = ActivityObject::PERSON;
  278. return $obj;
  279. }
  280. public static function fromRssChannel($el)
  281. {
  282. $obj = new ActivityObject();
  283. $obj->element = $el;
  284. $obj->type = ActivityObject::PERSON; // @fixme guess better
  285. $obj->title = ActivityUtils::childContent($el, ActivityObject::TITLE, Activity::RSS);
  286. $obj->link = ActivityUtils::childContent($el, ActivityUtils::LINK, Activity::RSS);
  287. $obj->id = ActivityUtils::getLink($el, Activity::SELF);
  288. if (empty($obj->id)) {
  289. $obj->id = $obj->link;
  290. }
  291. $desc = ActivityUtils::childContent($el, Activity::DESCRIPTION, Activity::RSS);
  292. if (!empty($desc)) {
  293. $obj->content = htmlspecialchars_decode($desc, ENT_QUOTES);
  294. }
  295. $imageEl = ActivityUtils::child($el, Activity::IMAGE, Activity::RSS);
  296. if (!empty($imageEl)) {
  297. $url = ActivityUtils::childContent($imageEl, Activity::URL, Activity::RSS);
  298. $al = new AvatarLink();
  299. $al->url = $url;
  300. $obj->avatarLinks[] = $al;
  301. }
  302. return $obj;
  303. }
  304. public static function fromPosterousAuthor($el)
  305. {
  306. $obj = new ActivityObject();
  307. $obj->type = ActivityObject::PERSON; // @fixme any others...?
  308. $userImage = ActivityUtils::childContent($el, self::USERIMAGE, self::POSTEROUS);
  309. if (!empty($userImage)) {
  310. $al = new AvatarLink();
  311. $al->url = $userImage;
  312. $obj->avatarLinks[] = $al;
  313. }
  314. $obj->link = ActivityUtils::childContent($el, self::PROFILEURL, self::POSTEROUS);
  315. $obj->id = $obj->link;
  316. $obj->poco = new PoCo();
  317. $obj->poco->preferredUsername = ActivityUtils::childContent($el, self::NICKNAME, self::POSTEROUS);
  318. $obj->poco->displayName = ActivityUtils::childContent($el, self::DISPLAYNAME, self::POSTEROUS);
  319. $obj->title = $obj->poco->displayName;
  320. return $obj;
  321. }
  322. private function _childContent($element, $tag, $namespace=ActivityUtils::ATOM)
  323. {
  324. return ActivityUtils::childContent($element, $tag, $namespace);
  325. }
  326. // Try to get a unique id for the source feed
  327. private function _getSource($element)
  328. {
  329. $sourceEl = ActivityUtils::child($element, 'source');
  330. if (empty($sourceEl)) {
  331. return null;
  332. } else {
  333. $href = ActivityUtils::getLink($sourceEl, 'self');
  334. if (!empty($href)) {
  335. return $href;
  336. } else {
  337. return ActivityUtils::childContent($sourceEl, 'id');
  338. }
  339. }
  340. }
  341. static function fromNotice(Notice $notice)
  342. {
  343. $object = new ActivityObject();
  344. if (Event::handle('StartActivityObjectFromNotice', array($notice, &$object))) {
  345. $object->type = (empty($notice->object_type)) ? ActivityObject::NOTE : $notice->object_type;
  346. $object->id = $notice->uri;
  347. $object->title = 'New ' . ActivityObject::canonicalType($object->type) . ' by ';
  348. try {
  349. $object->title .= $notice->getProfile()->getAcctUri();
  350. } catch (ProfileNoAcctUriException $e) {
  351. $object->title .= $e->profile->nickname;
  352. }
  353. $object->content = $notice->rendered;
  354. $object->link = $notice->getUrl();
  355. $object->extra[] = array('status_net', array('notice_id' => $notice->id));
  356. Event::handle('EndActivityObjectFromNotice', array($notice, &$object));
  357. }
  358. return $object;
  359. }
  360. static function fromProfile(Profile $profile)
  361. {
  362. $object = new ActivityObject();
  363. if (Event::handle('StartActivityObjectFromProfile', array($profile, &$object))) {
  364. $object->type = ActivityObject::PERSON;
  365. $object->id = $profile->getUri();
  366. $object->title = $profile->getBestName();
  367. $object->link = $profile->profileurl;
  368. try {
  369. $avatar = Avatar::getUploaded($profile);
  370. $object->avatarLinks[] = AvatarLink::fromAvatar($avatar);
  371. } catch (NoAvatarException $e) {
  372. // Could not find an original avatar to link
  373. }
  374. $sizes = array(
  375. AVATAR_PROFILE_SIZE,
  376. AVATAR_STREAM_SIZE,
  377. AVATAR_MINI_SIZE
  378. );
  379. foreach ($sizes as $size) {
  380. $alink = null;
  381. try {
  382. $avatar = Avatar::byProfile($profile, $size);
  383. $alink = AvatarLink::fromAvatar($avatar);
  384. } catch (NoAvatarException $e) {
  385. $alink = new AvatarLink();
  386. $alink->type = 'image/png';
  387. $alink->height = $size;
  388. $alink->width = $size;
  389. $alink->url = Avatar::defaultImage($size);
  390. }
  391. $object->avatarLinks[] = $alink;
  392. }
  393. if (isset($profile->lat) && isset($profile->lon)) {
  394. $object->geopoint = (float)$profile->lat
  395. . ' ' . (float)$profile->lon;
  396. }
  397. $object->poco = PoCo::fromProfile($profile);
  398. if ($profile->isLocal()) {
  399. $object->extra[] = array('followers', array('url' => common_local_url('subscribers', array('nickname' => $profile->nickname))));
  400. }
  401. Event::handle('EndActivityObjectFromProfile', array($profile, &$object));
  402. }
  403. return $object;
  404. }
  405. static function fromGroup(User_group $group)
  406. {
  407. $object = new ActivityObject();
  408. if (Event::handle('StartActivityObjectFromGroup', array($group, &$object))) {
  409. $object->type = ActivityObject::GROUP;
  410. $object->id = $group->getUri();
  411. $object->title = $group->getBestName();
  412. $object->link = $group->getUri();
  413. $object->avatarLinks[] = AvatarLink::fromFilename($group->homepage_logo,
  414. AVATAR_PROFILE_SIZE);
  415. $object->avatarLinks[] = AvatarLink::fromFilename($group->stream_logo,
  416. AVATAR_STREAM_SIZE);
  417. $object->avatarLinks[] = AvatarLink::fromFilename($group->mini_logo,
  418. AVATAR_MINI_SIZE);
  419. $object->poco = PoCo::fromGroup($group);
  420. Event::handle('EndActivityObjectFromGroup', array($group, &$object));
  421. }
  422. return $object;
  423. }
  424. static function fromPeopletag($ptag)
  425. {
  426. $object = new ActivityObject();
  427. if (Event::handle('StartActivityObjectFromPeopletag', array($ptag, &$object))) {
  428. $object->type = ActivityObject::_LIST;
  429. $object->id = $ptag->getUri();
  430. $object->title = $ptag->tag;
  431. $object->summary = $ptag->description;
  432. $object->link = $ptag->homeUrl();
  433. $object->owner = Profile::getKV('id', $ptag->tagger);
  434. $object->poco = PoCo::fromProfile($object->owner);
  435. Event::handle('EndActivityObjectFromPeopletag', array($ptag, &$object));
  436. }
  437. return $object;
  438. }
  439. static function fromFile(File $file)
  440. {
  441. $object = new ActivityObject();
  442. if (Event::handle('StartActivityObjectFromFile', array($file, &$object))) {
  443. $object->type = self::mimeTypeToObjectType($file->mimetype);
  444. $object->id = TagURI::mint(sprintf("file:%d", $file->id));
  445. $object->link = common_local_url('attachment', array('attachment' => $file->id));
  446. if ($file->title) {
  447. $object->title = $file->title;
  448. }
  449. if ($file->date) {
  450. $object->date = $file->date;
  451. }
  452. try {
  453. $thumbnail = $file->getThumbnail();
  454. $object->thumbnail = $thumbnail;
  455. } catch (UnsupportedMediaException $e) {
  456. $object->thumbnail = null;
  457. }
  458. switch (ActivityObject::canonicalType($object->type)) {
  459. case 'image':
  460. $object->largerImage = $file->url;
  461. break;
  462. case 'video':
  463. case 'audio':
  464. $object->stream = $file->url;
  465. break;
  466. }
  467. Event::handle('EndActivityObjectFromFile', array($file, &$object));
  468. }
  469. return $object;
  470. }
  471. static function fromNoticeSource(Notice_source $source)
  472. {
  473. $object = new ActivityObject();
  474. $wellKnown = array('web', 'xmpp', 'mail', 'omb', 'system', 'api', 'ostatus',
  475. 'activity', 'feed', 'mirror', 'twitter', 'facebook');
  476. if (Event::handle('StartActivityObjectFromNoticeSource', array($source, &$object))) {
  477. $object->type = ActivityObject::APPLICATION;
  478. if (in_array($source->code, $wellKnown)) {
  479. // We use one ID for all well-known StatusNet sources
  480. $object->id = "tag:status.net,2009:notice-source:".$source->code;
  481. } else if ($source->url) {
  482. // They registered with an URL
  483. $object->id = $source->url;
  484. } else {
  485. // Locally-registered, no URL
  486. $object->id = TagURI::mint("notice-source:".$source->code);
  487. }
  488. if ($source->url) {
  489. $object->link = $source->url;
  490. }
  491. if ($source->name) {
  492. $object->title = $source->name;
  493. } else {
  494. $object->title = $source->code;
  495. }
  496. if ($source->created) {
  497. $object->date = $source->created;
  498. }
  499. $object->extra[] = array('status_net', array('source_code' => $source->code));
  500. Event::handle('EndActivityObjectFromNoticeSource', array($source, &$object));
  501. }
  502. return $object;
  503. }
  504. static function fromMessage(Message $message)
  505. {
  506. $object = new ActivityObject();
  507. if (Event::handle('StartActivityObjectFromMessage', array($message, &$object))) {
  508. $object->type = ActivityObject::NOTE;
  509. $object->id = ($message->uri) ? $message->uri : (($message->url) ? $message->url : TagURI::mint(sprintf("message:%d", $message->id)));
  510. $object->content = $message->rendered;
  511. $object->date = $message->created;
  512. if ($message->url) {
  513. $object->link = $message->url;
  514. } else {
  515. $object->link = common_local_url('showmessage', array('message' => $message->id));
  516. }
  517. $object->extra[] = array('status_net', array('message_id' => $message->id));
  518. Event::handle('EndActivityObjectFromMessage', array($message, &$object));
  519. }
  520. return $object;
  521. }
  522. function outputTo($xo, $tag='activity:object')
  523. {
  524. if (!empty($tag)) {
  525. $xo->elementStart($tag);
  526. }
  527. if (Event::handle('StartActivityObjectOutputAtom', array($this, $xo))) {
  528. $xo->element('activity:object-type', null, $this->type);
  529. // <author> uses URI
  530. if ($tag == 'author') {
  531. $xo->element(self::URI, null, $this->id);
  532. } else {
  533. $xo->element(self::ID, null, $this->id);
  534. }
  535. if (!empty($this->title)) {
  536. $name = common_xml_safe_str($this->title);
  537. if ($tag == 'author') {
  538. // XXX: Backward compatibility hack -- atom:name should contain
  539. // full name here, instead of nickname, i.e.: $name. Change
  540. // this in the next version.
  541. $xo->element(self::NAME, null, $this->poco->preferredUsername);
  542. } else {
  543. $xo->element(self::TITLE, null, $name);
  544. }
  545. }
  546. if (!empty($this->summary)) {
  547. $xo->element(
  548. self::SUMMARY,
  549. null,
  550. common_xml_safe_str($this->summary)
  551. );
  552. }
  553. if (!empty($this->content)) {
  554. // XXX: assuming HTML content here
  555. $xo->element(
  556. ActivityUtils::CONTENT,
  557. array('type' => 'html'),
  558. common_xml_safe_str($this->content)
  559. );
  560. }
  561. if (!empty($this->link)) {
  562. $xo->element(
  563. 'link',
  564. array(
  565. 'rel' => 'alternate',
  566. 'type' => 'text/html',
  567. 'href' => $this->link
  568. ),
  569. null
  570. );
  571. }
  572. if(!empty($this->owner)) {
  573. $owner = $this->owner->asActivityNoun(self::AUTHOR);
  574. $xo->raw($owner);
  575. }
  576. if ($this->type == ActivityObject::PERSON
  577. || $this->type == ActivityObject::GROUP) {
  578. foreach ($this->avatarLinks as $alink) {
  579. $xo->element('link',
  580. array(
  581. 'rel' => 'avatar',
  582. 'type' => $alink->type,
  583. 'media:width' => $alink->width,
  584. 'media:height' => $alink->height,
  585. 'href' => $alink->url,
  586. ),
  587. null);
  588. }
  589. }
  590. if (!empty($this->geopoint)) {
  591. $xo->element(
  592. 'georss:point',
  593. null,
  594. $this->geopoint
  595. );
  596. }
  597. if (!empty($this->poco)) {
  598. $this->poco->outputTo($xo);
  599. }
  600. // @fixme there's no way here to make a tree; elements can only contain plaintext
  601. // @fixme these may collide with JSON extensions
  602. foreach ($this->extra as $el) {
  603. list($extraTag, $attrs, $content) = array_pad($el, 3, null);
  604. $xo->element($extraTag, $attrs, $content);
  605. }
  606. Event::handle('EndActivityObjectOutputAtom', array($this, $xo));
  607. }
  608. if (!empty($tag)) {
  609. $xo->elementEnd($tag);
  610. }
  611. return;
  612. }
  613. function asString($tag='activity:object')
  614. {
  615. $xs = new XMLStringer(true);
  616. $this->outputTo($xs, $tag);
  617. return $xs->getString();
  618. }
  619. /*
  620. * Returns an array based on this Activity Object suitable for
  621. * encoding as JSON.
  622. *
  623. * @return array $object the activity object array
  624. */
  625. function asArray()
  626. {
  627. $object = array();
  628. if (Event::handle('StartActivityObjectOutputJson', array($this, &$object))) {
  629. // XXX: attachments are added by Activity
  630. // author (Add object for author? Could be useful for repeats.)
  631. // content (Add rendered version of the notice?)
  632. // downstreamDuplicates
  633. // id
  634. if ($this->id) {
  635. $object['id'] = $this->id;
  636. } else if ($this->link) {
  637. $object['id'] = $this->link;
  638. }
  639. if ($this->type == ActivityObject::PERSON
  640. || $this->type == ActivityObject::GROUP) {
  641. // displayName
  642. $object['displayName'] = $this->title;
  643. // XXX: Not sure what the best avatar is to use for the
  644. // author's "image". For now, I'm using the large size.
  645. $imgLink = null;
  646. $avatarMediaLinks = array();
  647. foreach ($this->avatarLinks as $a) {
  648. // Make a MediaLink for every other Avatar
  649. $avatar = new ActivityStreamsMediaLink(
  650. $a->url,
  651. $a->width,
  652. $a->height,
  653. $a->type,
  654. 'avatar'
  655. );
  656. // Find the big avatar to use as the "image"
  657. if ($a->height == AVATAR_PROFILE_SIZE) {
  658. $imgLink = $avatar;
  659. }
  660. $avatarMediaLinks[] = $avatar->asArray();
  661. }
  662. if (!array_key_exists('status_net', $object)) {
  663. $object['status_net'] = array();
  664. }
  665. $object['status_net']['avatarLinks'] = $avatarMediaLinks; // extension
  666. // image
  667. if (!empty($imgLink)) {
  668. $object['image'] = $imgLink->asArray();
  669. }
  670. }
  671. // objectType
  672. //
  673. // We can probably use the whole schema URL here but probably the
  674. // relative simple name is easier to parse
  675. $object['objectType'] = ActivityObject::canonicalType($this->type);
  676. // summary
  677. $object['summary'] = $this->summary;
  678. // content, usually rendered HTML
  679. $object['content'] = $this->content;
  680. // published (probably don't need. Might be useful for repeats.)
  681. // updated (probably don't need this)
  682. // TODO: upstreamDuplicates
  683. if ($this->link) {
  684. $object['url'] = $this->link;
  685. }
  686. /* Extensions */
  687. // @fixme these may collide with XML extensions
  688. // @fixme multiple tags of same name will overwrite each other
  689. // @fixme text content from XML extensions will be lost
  690. foreach ($this->extra as $e) {
  691. list($objectName, $props, $txt) = array_pad($e, 3, null);
  692. if (!empty($objectName)) {
  693. $parts = explode(":", $objectName);
  694. if (count($parts) == 2 && $parts[0] == "statusnet") {
  695. if (!array_key_exists('status_net', $object)) {
  696. $object['status_net'] = array();
  697. }
  698. $object['status_net'][$parts[1]] = $props;
  699. } else {
  700. $object[$objectName] = $props;
  701. }
  702. }
  703. }
  704. if (!empty($this->geopoint)) {
  705. list($lat, $lon) = explode(' ', $this->geopoint);
  706. if (!empty($lat) && !empty($lon)) {
  707. $object['location'] = array(
  708. 'objectType' => 'place',
  709. 'position' => sprintf("%+02.5F%+03.5F/", $lat, $lon),
  710. 'lat' => $lat,
  711. 'lon' => $lon
  712. );
  713. $loc = Location::fromLatLon((float)$lat, (float)$lon);
  714. if ($loc) {
  715. $name = $loc->getName();
  716. if ($name) {
  717. $object['location']['displayName'] = $name;
  718. }
  719. $url = $loc->getURL();
  720. if ($url) {
  721. $object['location']['url'] = $url;
  722. }
  723. }
  724. }
  725. }
  726. if (!empty($this->poco)) {
  727. $object['portablecontacts_net'] = array_filter($this->poco->asArray());
  728. }
  729. if (!empty($this->thumbnail)) {
  730. if (is_string($this->thumbnail)) {
  731. $object['image'] = array('url' => $this->thumbnail);
  732. } else {
  733. $object['image'] = array('url' => $this->thumbnail->url);
  734. if ($this->thumbnail->width) {
  735. $object['image']['width'] = $this->thumbnail->width;
  736. }
  737. if ($this->thumbnail->height) {
  738. $object['image']['height'] = $this->thumbnail->height;
  739. }
  740. }
  741. }
  742. switch (ActivityObject::canonicalType($this->type)) {
  743. case 'image':
  744. if (!empty($this->largerImage)) {
  745. $object['fullImage'] = array('url' => $this->largerImage);
  746. }
  747. break;
  748. case 'audio':
  749. case 'video':
  750. if (!empty($this->stream)) {
  751. $object['stream'] = array('url' => $this->stream);
  752. }
  753. break;
  754. }
  755. Event::handle('EndActivityObjectOutputJson', array($this, &$object));
  756. }
  757. return array_filter($object);
  758. }
  759. static function canonicalType($type) {
  760. $ns = 'http://activitystrea.ms/schema/1.0/';
  761. if (substr($type, 0, mb_strlen($ns)) == $ns) {
  762. return substr($type, mb_strlen($ns));
  763. } else {
  764. return $type;
  765. }
  766. }
  767. static function mimeTypeToObjectType($mimeType) {
  768. $ot = null;
  769. // Default
  770. if (empty($mimeType)) {
  771. return self::FILE;
  772. }
  773. $parts = explode('/', $mimeType);
  774. switch ($parts[0]) {
  775. case 'image':
  776. $ot = self::IMAGE;
  777. break;
  778. case 'audio':
  779. $ot = self::AUDIO;
  780. break;
  781. case 'video':
  782. $ot = self::VIDEO;
  783. break;
  784. default:
  785. $ot = self::FILE;
  786. }
  787. return $ot;
  788. }
  789. }