ActivityStreams.php 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. <?php
  2. namespace Zotlabs\Lib;
  3. /**
  4. * @brief ActivityStreams class.
  5. *
  6. * Parses an ActivityStream JSON string.
  7. */
  8. class ActivityStreams {
  9. public $raw = null;
  10. public $data = null;
  11. public $valid = false;
  12. public $id = '';
  13. public $parent_id = '';
  14. public $type = '';
  15. public $actor = null;
  16. public $obj = null;
  17. public $tgt = null;
  18. public $origin = null;
  19. public $owner = null;
  20. public $signer = null;
  21. public $ldsig = null;
  22. public $sigok = false;
  23. public $recips = null;
  24. public $raw_recips = null;
  25. /**
  26. * @brief Constructor for ActivityStreams.
  27. *
  28. * Takes a JSON string as parameter, decodes it and sets up this object.
  29. *
  30. * @param string $string
  31. */
  32. function __construct($string) {
  33. $this->raw = $string;
  34. if(is_array($string)) {
  35. $this->data = $string;
  36. }
  37. else {
  38. $this->data = json_decode($string, true);
  39. }
  40. if($this->data) {
  41. // verify and unpack JSalmon signature if present
  42. if(is_array($this->data) && array_key_exists('signed',$this->data)) {
  43. $ret = JSalmon::verify($this->data);
  44. $tmp = JSalmon::unpack($this->data['data']);
  45. if($ret && $ret['success']) {
  46. if($ret['signer']) {
  47. $saved = json_encode($this->data,JSON_UNESCAPED_SLASHES);
  48. $this->data = $tmp;
  49. $this->data['signer'] = $ret['signer'];
  50. $this->data['signed_data'] = $saved;
  51. if($ret['hubloc']) {
  52. $this->data['hubloc'] = $ret['hubloc'];
  53. }
  54. }
  55. }
  56. }
  57. $this->valid = true;
  58. }
  59. if($this->is_valid()) {
  60. $this->id = $this->get_property_obj('id');
  61. $this->type = $this->get_primary_type();
  62. $this->actor = $this->get_actor('actor','','');
  63. $this->obj = $this->get_compound_property('object');
  64. $this->tgt = $this->get_compound_property('target');
  65. $this->origin = $this->get_compound_property('origin');
  66. $this->recips = $this->collect_recips();
  67. $this->ldsig = $this->get_compound_property('signature');
  68. if($this->ldsig) {
  69. $this->signer = $this->get_compound_property('creator',$this->ldsig);
  70. if($this->signer && $this->signer['publicKey'] && $this->signer['publicKey']['publicKeyPem']) {
  71. $this->sigok = LDSignatures::verify($this->data,$this->signer['publicKey']['publicKeyPem']);
  72. }
  73. }
  74. if(! $this->obj) {
  75. $this->obj = $this->data;
  76. $this->type = 'Create';
  77. if(! $this->actor) {
  78. $this->actor = $this->get_actor('attributedTo',$this->obj);
  79. }
  80. }
  81. if($this->obj && is_array($this->obj) && $this->obj['actor'])
  82. $this->obj['actor'] = $this->get_actor('actor',$this->obj);
  83. if($this->tgt && is_array($this->tgt) && $this->tgt['actor'])
  84. $this->tgt['actor'] = $this->get_actor('actor',$this->tgt);
  85. $this->parent_id = $this->get_property_obj('inReplyTo');
  86. if((! $this->parent_id) && is_array($this->obj)) {
  87. $this->parent_id = $this->obj['inReplyTo'];
  88. }
  89. if((! $this->parent_id) && is_array($this->obj)) {
  90. $this->parent_id = $this->obj['id'];
  91. }
  92. }
  93. }
  94. /**
  95. * @brief Return if instantiated ActivityStream is valid.
  96. *
  97. * @return boolean Return true if the JSON string could be decoded.
  98. */
  99. function is_valid() {
  100. return $this->valid;
  101. }
  102. function set_recips($arr) {
  103. $this->saved_recips = $arr;
  104. }
  105. /**
  106. * @brief Collects all recipients.
  107. *
  108. * @param string $base
  109. * @param string $namespace (optional) default empty
  110. * @return array
  111. */
  112. function collect_recips($base = '', $namespace = '') {
  113. $x = [];
  114. $fields = [ 'to', 'cc', 'bto', 'bcc', 'audience'];
  115. foreach($fields as $f) {
  116. $y = $this->get_compound_property($f, $base, $namespace);
  117. if($y) {
  118. $x = array_merge($x, $y);
  119. if(! is_array($this->raw_recips))
  120. $this->raw_recips = [];
  121. $this->raw_recips[$f] = $x;
  122. }
  123. }
  124. // not yet ready for prime time
  125. // $x = $this->expand($x,$base,$namespace);
  126. return $x;
  127. }
  128. function expand($arr,$base = '',$namespace = '') {
  129. $ret = [];
  130. // right now use a hardwired recursion depth of 5
  131. for($z = 0; $z < 5; $z ++) {
  132. if(is_array($arr) && $arr) {
  133. foreach($arr as $a) {
  134. if(is_array($a)) {
  135. $ret[] = $a;
  136. }
  137. else {
  138. $x = $this->get_compound_property($a,$base,$namespace);
  139. if($x) {
  140. $ret = array_merge($ret,$x);
  141. }
  142. }
  143. }
  144. }
  145. }
  146. /// @fixme de-duplicate
  147. return $ret;
  148. }
  149. /**
  150. * @brief
  151. *
  152. * @param array $base
  153. * @param string $namespace if not set return empty string
  154. * @return string|NULL
  155. */
  156. function get_namespace($base, $namespace) {
  157. if(! $namespace)
  158. return '';
  159. $key = null;
  160. foreach( [ $this->data, $base ] as $b ) {
  161. if(! $b)
  162. continue;
  163. if(array_key_exists('@context', $b)) {
  164. if(is_array($b['@context'])) {
  165. foreach($b['@context'] as $ns) {
  166. if(is_array($ns)) {
  167. foreach($ns as $k => $v) {
  168. if($namespace === $v)
  169. $key = $k;
  170. }
  171. }
  172. else {
  173. if($namespace === $ns) {
  174. $key = '';
  175. }
  176. }
  177. }
  178. }
  179. else {
  180. if($namespace === $b['@context']) {
  181. $key = '';
  182. }
  183. }
  184. }
  185. }
  186. return $key;
  187. }
  188. /**
  189. * @brief
  190. *
  191. * @param string $property
  192. * @param array $base (optional)
  193. * @param string $namespace (optional) default empty
  194. * @return NULL|mixed
  195. */
  196. function get_property_obj($property, $base = '', $namespace = '') {
  197. $prefix = $this->get_namespace($base, $namespace);
  198. if($prefix === null)
  199. return null;
  200. $base = (($base) ? $base : $this->data);
  201. $propname = (($prefix) ? $prefix . ':' : '') . $property;
  202. if(! is_array($base)) {
  203. btlogger('not an array: ' . print_r($base,true));
  204. }
  205. return ((array_key_exists($propname, $base)) ? $base[$propname] : null);
  206. }
  207. /**
  208. * @brief Fetches a property from an URL.
  209. *
  210. * @param string $url
  211. * @return NULL|mixed
  212. */
  213. function fetch_property($url) {
  214. return self::fetch($url);
  215. }
  216. static function fetch($url) {
  217. $redirects = 0;
  218. if(! check_siteallowed($url)) {
  219. logger('blacklisted: ' . $url);
  220. return null;
  221. }
  222. logger('fetch: ' . $url, LOGGER_DEBUG);
  223. $x = z_fetch_url($url, true, $redirects,
  224. [ 'headers' => [ 'Accept: application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"' ]]);
  225. if($x['success']) {
  226. $y = json_decode($x['body'],true);
  227. logger('returned: ' . json_encode($y,JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES));
  228. return json_decode($x['body'], true);
  229. }
  230. else {
  231. logger('fetch failed: ' . $url);
  232. }
  233. return null;
  234. }
  235. static function is_an_actor($s) {
  236. return(in_array($s,[ 'Application','Group','Service','Person','Service' ]));
  237. }
  238. /**
  239. * @brief
  240. *
  241. * @param string $property
  242. * @param array $base
  243. * @param string $namespace (optional) default empty
  244. * @return NULL|mixed
  245. */
  246. function get_actor($property,$base='',$namespace = '') {
  247. $x = $this->get_property_obj($property, $base, $namespace);
  248. if($this->is_url($x)) {
  249. // SECURITY: If we have already stored the actor profile, re-generate it
  250. // from cached data - don't refetch it from the network
  251. $r = q("select * from xchan left join hubloc on xchan_hash = hubloc_hash where hubloc_id_url = '%s' limit 1",
  252. dbesc($x)
  253. );
  254. if($r) {
  255. $y = Activity::encode_person($r[0]);
  256. $y['cached'] = true;
  257. return $y;
  258. }
  259. }
  260. return $this->get_compound_property($property,$base,$namespace,true);
  261. }
  262. /**
  263. * @brief
  264. *
  265. * @param string $property
  266. * @param array $base
  267. * @param string $namespace (optional) default empty
  268. * @param boolean $first (optional) default false, if true and result is a sequential array return only the first element
  269. * @return NULL|mixed
  270. */
  271. function get_compound_property($property, $base = '', $namespace = '', $first = false) {
  272. $x = $this->get_property_obj($property, $base, $namespace);
  273. if($this->is_url($x)) {
  274. $x = $this->fetch_property($x);
  275. }
  276. // verify and unpack JSalmon signature if present
  277. if(is_array($x) && array_key_exists('signed',$x)) {
  278. $ret = JSalmon::verify($x);
  279. $tmp = JSalmon::unpack($x['data']);
  280. if($ret && $ret['success']) {
  281. if($ret['signer']) {
  282. $saved = json_encode($x,JSON_UNESCAPED_SLASHES);
  283. $x = $tmp;
  284. $x['signer'] = $ret['signer'];
  285. $x['signed_data'] = $saved;
  286. if($ret['hubloc']) {
  287. $x['hubloc'] = $ret['hubloc'];
  288. }
  289. }
  290. }
  291. }
  292. if($first && is_array($x) && array_key_exists(0,$x)) {
  293. return $x[0];
  294. }
  295. return $x;
  296. }
  297. /**
  298. * @brief Check if string starts with http.
  299. *
  300. * @param string $url
  301. * @return boolean
  302. */
  303. function is_url($url) {
  304. if(($url) && (! is_array($url)) && (strpos($url, 'http') === 0)) {
  305. return true;
  306. }
  307. return false;
  308. }
  309. /**
  310. * @brief Gets the type property.
  311. *
  312. * @param array $base
  313. * @param string $namespace (optional) default empty
  314. * @return NULL|mixed
  315. */
  316. function get_primary_type($base = '', $namespace = '') {
  317. if(! $base)
  318. $base = $this->data;
  319. $x = $this->get_property_obj('type', $base, $namespace);
  320. if(is_array($x)) {
  321. foreach($x as $y) {
  322. if(strpos($y, ':') === false) {
  323. return $y;
  324. }
  325. }
  326. }
  327. return $x;
  328. }
  329. function debug() {
  330. $x = var_export($this, true);
  331. return $x;
  332. }
  333. static function is_as_request() {
  334. $x = getBestSupportedMimeType([
  335. 'application/ld+json;profile="https://www.w3.org/ns/activitystreams"',
  336. 'application/activity+json',
  337. 'application/ld+json;profile="http://www.w3.org/ns/activitystreams"'
  338. ]);
  339. return(($x) ? true : false);
  340. }
  341. }