streams/Zotlabs/Lib/ActivityStreams.php

466 lines
11 KiB
PHP
Raw Normal View History

2017-07-17 05:28:28 +00:00
<?php
namespace Zotlabs\Lib;
use Zotlabs\Web\HTTPSig;
/**
* @brief ActivityStreams class.
*
* Parses an ActivityStream JSON string.
*/
class ActivityStreams {
2017-07-17 05:28:28 +00:00
public $raw = null;
public $data = null;
public $hub = null;
public $valid = false;
2018-10-10 04:08:57 +00:00
public $deleted = false;
public $id = '';
public $parent_id = '';
public $type = '';
public $actor = null;
public $obj = null;
public $tgt = null;
public $replyto = null;
public $origin = null;
public $owner = null;
public $signer = null;
public $ldsig = null;
public $sigok = false;
public $recips = null;
2017-09-18 01:40:32 +00:00
public $raw_recips = null;
public $implied_create = false;
/**
* @brief Constructor for ActivityStreams.
*
2019-09-30 06:26:59 +00:00
* Takes a JSON string or previously decode activity array as parameter,
* decodes it and sets up this object/activity, fetching any required attributes
* which were only referenced by @id/URI.
*
* @param string $string
*/
2019-10-03 01:07:13 +00:00
function __construct($string,$hub = null,$client = null) {
2017-07-17 05:28:28 +00:00
$this->raw = $string;
$this->hub = $hub;
2019-10-03 01:07:13 +00:00
2019-09-30 02:20:09 +00:00
if (is_array($string)) {
2018-05-30 06:30:18 +00:00
$this->data = $string;
2019-09-30 06:26:59 +00:00
$this->raw = json_encode($string,JSON_UNESCAPED_SLASHES);
2018-05-30 06:30:18 +00:00
}
else {
$this->data = json_decode($string, true);
}
2019-09-30 02:20:09 +00:00
if ($this->data) {
// verify and unpack JSalmon signature if present
2019-09-30 02:20:09 +00:00
// This will only be the case for Zot6 packets
2019-09-30 02:20:09 +00:00
if (is_array($this->data) && array_key_exists('signed',$this->data)) {
$ret = JSalmon::verify($this->data);
$tmp = JSalmon::unpack($this->data['data']);
2019-09-30 02:20:09 +00:00
if ($ret && $ret['success']) {
if ($ret['signer']) {
2019-09-25 03:03:04 +00:00
logger('Unpacked: ' . json_encode($tmp,JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT),LOGGER_DATA,LOG_DEBUG);
$saved = json_encode($this->data,JSON_UNESCAPED_SLASHES);
$this->data = $tmp;
$this->data['signer'] = $ret['signer'];
$this->data['signed_data'] = $saved;
2019-09-30 02:20:09 +00:00
if ($ret['hubloc']) {
$this->data['hubloc'] = $ret['hubloc'];
}
}
}
}
2019-09-30 02:20:09 +00:00
// This indicates only that we have sucessfully decoded JSON.
2017-07-17 05:28:28 +00:00
$this->valid = true;
2019-09-30 02:20:09 +00:00
// Special handling for Mastodon "delete actor" activities which will often fail to verify
// because the key cannot be fetched. We will catch this condition elsewhere.
if (array_key_exists('type',$this->data) && array_key_exists('actor',$this->data) && array_key_exists('object',$this->data)) {
if ($this->data['type'] === 'Delete' && $this->data['actor'] === $this->data['object']) {
2018-10-10 04:08:57 +00:00
$this->deleted = $this->data['actor'];
$this->valid = false;
}
}
2017-07-17 05:28:28 +00:00
}
2019-09-30 02:20:09 +00:00
// Attempt to assemble an Activity from what we were given.
if ($this->is_valid()) {
$this->id = $this->get_property_obj('id');
$this->type = $this->get_primary_type();
$this->actor = $this->get_actor('actor','','');
$this->obj = $this->get_compound_property('object');
2021-04-30 00:50:13 +00:00
$this->tgt = $this->get_compound_property('target');
$this->origin = $this->get_compound_property('origin');
$this->recips = $this->collect_recips();
$this->replyto = $this->get_property_obj('replyTo');
2017-09-12 01:56:17 +00:00
$this->ldsig = $this->get_compound_property('signature');
2019-09-30 02:20:09 +00:00
if ($this->ldsig) {
2017-09-12 01:56:17 +00:00
$this->signer = $this->get_compound_property('creator',$this->ldsig);
2019-09-30 02:20:09 +00:00
if ($this->signer && is_array($this->signer) && array_key_exists('publicKey',$this->signer)
&& is_array($this->signer['publicKey']) && $this->signer['publicKey']['publicKeyPem']) {
2018-09-04 04:30:39 +00:00
$this->sigok = LDSignatures::verify($this->data,$this->signer['publicKey']['publicKeyPem']);
2017-09-12 01:56:17 +00:00
}
}
2019-10-03 01:07:13 +00:00
// Implied create activity required by C2S specification if no object is present
2019-09-30 02:20:09 +00:00
if (! $this->obj) {
if (! $client) {
$this->implied_create = true;
}
$this->obj = $this->data;
$this->type = 'Create';
2019-09-30 02:20:09 +00:00
if (! $this->actor) {
2018-09-04 04:30:39 +00:00
$this->actor = $this->get_actor('attributedTo',$this->obj);
}
}
2019-09-30 02:20:09 +00:00
// fetch recursive or embedded activities
if ($this->obj && is_array($this->obj) && array_key_exists('object',$this->obj)) {
$this->obj['object'] = $this->get_compound_property($this->obj['object']);
}
2019-09-30 02:20:09 +00:00
// Enumerate and store actors in referenced objects
2019-09-30 02:20:09 +00:00
if ($this->obj && is_array($this->obj) && $this->obj['actor']) {
2018-09-04 04:30:39 +00:00
$this->obj['actor'] = $this->get_actor('actor',$this->obj);
2019-09-30 02:20:09 +00:00
}
if ($this->tgt && is_array($this->tgt) && $this->tgt['actor']) {
2018-09-04 04:30:39 +00:00
$this->tgt['actor'] = $this->get_actor('actor',$this->tgt);
2019-09-30 02:20:09 +00:00
}
2018-09-04 04:30:39 +00:00
2019-09-30 02:20:09 +00:00
// Determine if this is a followup or response activity
$this->parent_id = $this->get_property_obj('inReplyTo');
2018-09-07 11:46:57 +00:00
2019-09-30 02:20:09 +00:00
if ((! $this->parent_id) && is_array($this->obj)) {
2018-09-10 06:16:58 +00:00
$this->parent_id = $this->obj['inReplyTo'];
}
2019-09-30 02:20:09 +00:00
if ((! $this->parent_id) && is_array($this->obj)) {
2018-09-03 03:16:33 +00:00
$this->parent_id = $this->obj['id'];
}
2017-07-17 05:28:28 +00:00
}
}
/**
* @brief Return if instantiated ActivityStream is valid.
*
* @return boolean Return true if the JSON string could be decoded.
*/
2017-07-17 05:28:28 +00:00
function is_valid() {
return $this->valid;
}
2017-09-18 01:40:32 +00:00
function set_recips($arr) {
$this->saved_recips = $arr;
}
/**
* @brief Collects all recipients.
*
* @param string $base
* @param string $namespace (optional) default empty
* @return array
*/
function collect_recips($base = '', $namespace = '') {
2017-08-28 04:46:10 +00:00
$x = [];
$fields = [ 'to', 'cc', 'bto', 'bcc', 'audience'];
2019-09-30 02:20:09 +00:00
foreach ($fields as $f) {
// don't expand these yet
$y = $this->get_property_obj($f, $base, $namespace);
2019-09-30 02:20:09 +00:00
if ($y) {
if (! is_array($this->raw_recips)) {
$this->raw_recips = [];
}
2019-09-30 02:20:09 +00:00
if (! is_array($y)) {
2019-02-06 03:01:17 +00:00
$y = [ $y ];
}
$this->raw_recips[$f] = $y;
$x = array_merge($x, $y);
2017-09-18 01:40:32 +00:00
}
}
2019-09-30 02:20:09 +00:00
2017-08-28 04:46:10 +00:00
// not yet ready for prime time
// $x = $this->expand($x,$base,$namespace);
return $x;
}
2017-09-20 23:26:33 +00:00
function expand($arr,$base = '',$namespace = '') {
2017-08-28 04:46:10 +00:00
$ret = [];
// right now use a hardwired recursion depth of 5
2019-09-30 02:20:09 +00:00
for ($z = 0; $z < 5; $z ++) {
if (is_array($arr) && $arr) {
foreach ($arr as $a) {
if (is_array($a)) {
2017-08-28 04:46:10 +00:00
$ret[] = $a;
}
else {
$x = $this->get_compound_property($a,$base,$namespace);
2019-09-30 02:20:09 +00:00
if ($x) {
2017-08-28 04:46:10 +00:00
$ret = array_merge($ret,$x);
}
}
}
}
}
/// @fixme de-duplicate
2017-08-28 04:46:10 +00:00
return $ret;
}
/**
* @brief
*
* @param array $base
* @param string $namespace if not set return empty string
* @return string|NULL
*/
2019-09-30 02:20:09 +00:00
function get_namespace($base, $namespace) {
2019-09-30 02:20:09 +00:00
if (! $namespace) {
return EMPTY_STR;
}
$key = null;
2019-09-30 02:20:09 +00:00
foreach ( [ $this->data, $base ] as $b ) {
if (! $b) {
continue;
2019-09-30 02:20:09 +00:00
}
2019-09-30 02:20:09 +00:00
if (array_key_exists('@context', $b)) {
if (is_array($b['@context'])) {
foreach ($b['@context'] as $ns) {
if (is_array($ns)) {
foreach ($ns as $k => $v) {
if ($namespace === $v) {
$key = $k;
2019-09-30 02:20:09 +00:00
}
}
}
else {
2019-09-30 02:20:09 +00:00
if ($namespace === $ns) {
$key = '';
}
}
}
}
else {
2019-09-30 02:20:09 +00:00
if ($namespace === $b['@context']) {
$key = '';
}
}
}
}
return $key;
}
/**
* @brief
*
* @param string $property
* @param array $base (optional)
* @param string $namespace (optional) default empty
* @return NULL|mixed
*/
2019-09-30 02:20:09 +00:00
function get_property_obj($property, $base = '', $namespace = '') {
$prefix = $this->get_namespace($base, $namespace);
2019-09-30 02:20:09 +00:00
if ($prefix === null) {
return null;
2019-09-30 02:20:09 +00:00
}
$base = (($base) ? $base : $this->data);
$propname = (($prefix) ? $prefix . ':' : '') . $property;
2019-09-30 02:20:09 +00:00
if (! is_array($base)) {
2018-09-21 23:29:13 +00:00
btlogger('not an array: ' . print_r($base,true));
2018-09-24 01:23:28 +00:00
return null;
2018-09-21 23:29:13 +00:00
}
return ((array_key_exists($propname, $base)) ? $base[$propname] : null);
2017-07-17 05:28:28 +00:00
}
2018-09-17 04:41:24 +00:00
/**
* @brief Fetches a property from an URL.
*
* @param string $url
* @param array $channel (signing channel, default system channel)
* @return NULL|mixed
*/
2018-05-30 04:08:52 +00:00
function fetch_property($url,$channel = null,$hub = null) {
return Activity::fetch($url,$channel,$hub);
2017-07-17 05:28:28 +00:00
}
2018-09-17 04:42:41 +00:00
static function is_an_actor($s) {
2019-04-01 01:01:20 +00:00
if (! $s) {
return false;
}
return (in_array($s,[ 'Application','Group','Organization','Person','Service' ]));
2018-09-17 04:42:41 +00:00
}
static function is_response_activity($s) {
if (! $s) {
return false;
}
return (in_array($s, [ 'Like', 'Dislike', 'Flag', 'Block', 'Announce', 'Accept', 'Reject', 'TentativeAccept', 'TentativeReject', 'emojiReaction', 'EmojiReaction', 'EmojiReact' ]));
}
2018-07-12 01:02:25 +00:00
/**
* @brief
*
* @param string $property
* @param array $base
* @param string $namespace (optional) default empty
* @return NULL|mixed
*/
function get_actor($property,$base='',$namespace = '') {
$x = $this->get_property_obj($property, $base, $namespace);
2021-04-13 01:19:27 +00:00
if (self::is_url($x)) {
2021-09-02 07:35:05 +00:00
$y = Activity::get_cached_actor($x);
if ($y) {
2018-09-19 23:25:05 +00:00
return $y;
2018-07-12 01:02:25 +00:00
}
}
2021-09-02 07:35:05 +00:00
$actor = $this->get_compound_property($property,$base,$namespace,true);
2019-09-30 02:20:09 +00:00
if (is_array($actor) && self::is_an_actor($actor['type'])) {
if (array_key_exists('id',$actor) && (! array_key_exists('inbox',$actor))) {
$actor = $this->fetch_property($actor['id']);
}
return $actor;
}
return null;
2018-07-12 01:02:25 +00:00
}
/**
* @brief
*
* @param string $property
* @param array $base
* @param string $namespace (optional) default empty
2018-07-02 04:07:35 +00:00
* @param boolean $first (optional) default false, if true and result is a sequential array return only the first element
* @return NULL|mixed
*/
2019-09-30 02:20:09 +00:00
2018-07-02 04:07:35 +00:00
function get_compound_property($property, $base = '', $namespace = '', $first = false) {
$x = $this->get_property_obj($property, $base, $namespace);
2021-04-13 01:19:27 +00:00
if (self::is_url($x)) {
2019-03-09 21:13:04 +00:00
$y = $this->fetch_property($x);
if (is_array($y)) {
$x = $y;
}
2017-07-17 05:28:28 +00:00
}
// verify and unpack JSalmon signature if present
2019-09-30 02:20:09 +00:00
// This may be present in Zot6 packets
if (is_array($x) && array_key_exists('signed',$x)) {
$ret = JSalmon::verify($x);
$tmp = JSalmon::unpack($x['data']);
2019-09-30 02:20:09 +00:00
if ($ret && $ret['success']) {
if ($ret['signer']) {
logger('Unpacked: ' . json_encode($tmp,JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT),LOGGER_DATA,LOG_DEBUG);
$saved = json_encode($x,JSON_UNESCAPED_SLASHES);
$x = $tmp;
$x['signer'] = $ret['signer'];
$x['signed_data'] = $saved;
2019-09-30 02:20:09 +00:00
if ($ret['hubloc']) {
$x['hubloc'] = $ret['hubloc'];
}
}
}
}
2019-09-30 02:20:09 +00:00
if ($first && is_array($x) && array_key_exists(0,$x)) {
2018-07-02 04:07:35 +00:00
return $x[0];
}
2017-07-17 05:28:28 +00:00
return $x;
}
/**
* @brief Check if string starts with http.
*
* @param string $url
* @return boolean
*/
2019-09-30 02:20:09 +00:00
2021-04-13 01:19:27 +00:00
static public function is_url($url) {
2019-09-30 02:20:09 +00:00
if (($url) && (! is_array($url)) && ((strpos($url, 'http') === 0) || (strpos($url,'x-zot') === 0) || (strpos($url,'bear') === 0))) {
2017-07-17 05:28:28 +00:00
return true;
}
2017-07-17 05:28:28 +00:00
return false;
}
/**
* @brief Gets the type property.
*
* @param array $base
* @param string $namespace (optional) default empty
* @return NULL|mixed
*/
2019-09-30 02:20:09 +00:00
function get_primary_type($base = '', $namespace = '') {
2019-09-30 02:20:09 +00:00
if (! $base) {
2017-07-17 05:28:28 +00:00
$base = $this->data;
2019-09-30 02:20:09 +00:00
}
$x = $this->get_property_obj('type', $base, $namespace);
2019-09-30 02:20:09 +00:00
if (is_array($x)) {
foreach ($x as $y) {
if (strpos($y, ':') === false) {
2017-07-17 05:28:28 +00:00
return $y;
}
}
}
2017-07-17 05:28:28 +00:00
return $x;
}
function debug() {
$x = var_export($this, true);
2017-07-17 05:28:28 +00:00
return $x;
}
2018-06-26 03:55:53 +00:00
static function is_as_request() {
$x = getBestSupportedMimeType([
'application/ld+json;profile="https://www.w3.org/ns/activitystreams"',
'application/activity+json',
2021-02-19 01:55:51 +00:00
'application/ld+json;profile="http://www.w3.org/ns/activitystreams"',
'application/ld+json', // some versions of Friendica aren't spec compliant and leave off the profile
2021-02-19 01:55:51 +00:00
'application/x-zot-activity+json'
2018-06-26 03:55:53 +00:00
]);
return(($x) ? true : false);
}
2017-07-17 05:28:28 +00:00
}