forked from grumpydevelop/federator
		
	support outbox post
- we can now receive content into outbox (f.e. from contentnation) and post it into the outbox-user-logfile for now - we now verify post-requests according to ActivityPub standard - minor bugfix where user specified by passed user-parameter is possibly not included in postForUser-call
This commit is contained in:
		
							parent
							
								
									305ded4986
								
							
						
					
					
						commit
						20c2db4b35
					
				
					 3 changed files with 449 additions and 9 deletions
				
			
		| 
						 | 
					@ -192,6 +192,81 @@ class Api extends Main
 | 
				
			||||||
        throw new $exception($message);
 | 
					        throw new $exception($message);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * check if the headers include a valid signature
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param string[] $headers
 | 
				
			||||||
 | 
					     *          permission(s) to check for
 | 
				
			||||||
 | 
					     * @throws Exceptions\PermissionDenied
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function checkSignature($headers)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $signatureHeader = $headers['Signature'] ?? null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!$signatureHeader) {
 | 
				
			||||||
 | 
					            http_response_code(400);
 | 
				
			||||||
 | 
					            throw new Exceptions\PermissionDenied("Missing Signature header");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Parse Signature header
 | 
				
			||||||
 | 
					        preg_match_all('/(\w+)=["\']?([^"\',]+)["\']?/', $signatureHeader, $matches);
 | 
				
			||||||
 | 
					        $signatureParts = array_combine($matches[1], $matches[2]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $signature = base64_decode($signatureParts['signature']);
 | 
				
			||||||
 | 
					        $keyId = $signatureParts['keyId'];
 | 
				
			||||||
 | 
					        $signedHeaders = explode(' ', $signatureParts['headers']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Fetch public key from `keyId` (usually actor URL + #main-key)
 | 
				
			||||||
 | 
					        [$publicKeyData, $info] = \Federator\Main::getFromRemote($keyId, ['Accept: application/activity+json']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if ($info['http_code'] !== 200) {
 | 
				
			||||||
 | 
					            http_response_code(500);
 | 
				
			||||||
 | 
					            throw new Exceptions\PermissionDenied("Failed to fetch public key from keyId: $keyId");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $actor = json_decode($publicKeyData, true);
 | 
				
			||||||
 | 
					        error_log("actor: " . $publicKeyData);
 | 
				
			||||||
 | 
					        $publicKeyPem = $actor['publicKey']['publicKeyPem'] ?? null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        error_log($publicKeyPem);
 | 
				
			||||||
 | 
					        error_log(json_encode($headers));
 | 
				
			||||||
 | 
					        if (!$publicKeyPem) {
 | 
				
			||||||
 | 
					            http_response_code(500);
 | 
				
			||||||
 | 
					            throw new Exceptions\PermissionDenied("Invalid public key format from actor with keyId: $keyId");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Reconstruct the signed string
 | 
				
			||||||
 | 
					        $signedString = '';
 | 
				
			||||||
 | 
					        foreach ($signedHeaders as $header) {
 | 
				
			||||||
 | 
					            $headerValue = '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if ($header === '(request-target)') {
 | 
				
			||||||
 | 
					                $method = strtolower($_SERVER['REQUEST_METHOD']);
 | 
				
			||||||
 | 
					                $path = $_SERVER['REQUEST_URI'];
 | 
				
			||||||
 | 
					                $headerValue = "$method $path";
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                $headerValue = $headers[ucwords($header, '-')] ?? '';
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            $signedString .= strtolower($header) . ": " . $headerValue . "\n";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        $signedString = rtrim($signedString);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Verify the signature
 | 
				
			||||||
 | 
					        $pubkeyRes = openssl_pkey_get_public($publicKeyPem);
 | 
				
			||||||
 | 
					        $verified = false;
 | 
				
			||||||
 | 
					        if ($pubkeyRes) {
 | 
				
			||||||
 | 
					            $verified = openssl_verify($signedString, $signature, $pubkeyRes, OPENSSL_ALGO_SHA256);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if ($verified !== 1) {
 | 
				
			||||||
 | 
					            http_response_code(500);
 | 
				
			||||||
 | 
					            throw new Exceptions\PermissionDenied("Signature verification failed for publicKey with keyId: $keyId");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Signature is valid!
 | 
				
			||||||
 | 
					        return "Signature verified from actor: " . $actor['id'];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * remove unwanted elements from html input
 | 
					     * remove unwanted elements from html input
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -22,7 +22,7 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * constructor
 | 
					     * constructor
 | 
				
			||||||
     * @param \Federator\Main $main main instance
 | 
					     * @param \Federator\Api $main api main instance
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function __construct($main)
 | 
					    public function __construct($main)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
| 
						 | 
					@ -51,6 +51,16 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
				
			||||||
        $inboxActivity = null;
 | 
					        $inboxActivity = null;
 | 
				
			||||||
        $_rawInput = file_get_contents('php://input');
 | 
					        $_rawInput = file_get_contents('php://input');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $allHeaders = getallheaders();
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            $result = $this->main->checkSignature($allHeaders);
 | 
				
			||||||
 | 
					            error_log($result); // Signature verified
 | 
				
			||||||
 | 
					        } catch (\Federator\Exceptions\PermissionDenied $e) {
 | 
				
			||||||
 | 
					            error_log("Inbox::post Signature check failed: " . $e->getMessage());
 | 
				
			||||||
 | 
					            http_response_code(403); // Or 401
 | 
				
			||||||
 | 
					            exit("Access denied");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $activity = json_decode($_rawInput, true);
 | 
					        $activity = json_decode($_rawInput, true);
 | 
				
			||||||
        $host = $_SERVER['SERVER_NAME'];
 | 
					        $host = $_SERVER['SERVER_NAME'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -124,6 +134,11 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
				
			||||||
                                $apNote->addTag($tagObj);
 | 
					                                $apNote->addTag($tagObj);
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
 | 
					                        if (array_key_exists('cc', $obj)) {
 | 
				
			||||||
 | 
					                            foreach ($obj['cc'] as $cc) {
 | 
				
			||||||
 | 
					                                $apNote->addCC($cc);
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
                        
 | 
					                        
 | 
				
			||||||
                        $create->setObject($apNote);
 | 
					                        $create->setObject($apNote);
 | 
				
			||||||
                        break;
 | 
					                        break;
 | 
				
			||||||
| 
						 | 
					@ -176,6 +191,7 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
				
			||||||
                    case 'Note':
 | 
					                    case 'Note':
 | 
				
			||||||
                        $note = new \Federator\Data\ActivityPub\Common\Note();
 | 
					                        $note = new \Federator\Data\ActivityPub\Common\Note();
 | 
				
			||||||
                        $note->setID($objData['id'])
 | 
					                        $note->setID($objData['id'])
 | 
				
			||||||
 | 
					                            ->setSummary($objData['summary'])
 | 
				
			||||||
                            ->setContent($objData['content'] ?? '')
 | 
					                            ->setContent($objData['content'] ?? '')
 | 
				
			||||||
                            ->setPublished(strtotime($objData['published'] ?? 'now'))
 | 
					                            ->setPublished(strtotime($objData['published'] ?? 'now'))
 | 
				
			||||||
                            ->setURL($objData['url'] ?? $objData['id'])
 | 
					                            ->setURL($objData['url'] ?? $objData['id'])
 | 
				
			||||||
| 
						 | 
					@ -290,7 +306,7 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
				
			||||||
                $users = array_merge($users, $this->fetchAllFollowers($receiver, $host));
 | 
					                $users = array_merge($users, $this->fetchAllFollowers($receiver, $host));
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if ($_user !== false && in_array($_user, $users)) {
 | 
					        if ($_user !== false && !in_array($_user, $users)) {
 | 
				
			||||||
            $users[] = $_user;
 | 
					            $users[] = $_user;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        foreach ($users as $user) {
 | 
					        foreach ($users as $user) {
 | 
				
			||||||
| 
						 | 
					@ -324,6 +340,7 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
				
			||||||
                $cache
 | 
					                $cache
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
            if ($user->id === null) {
 | 
					            if ($user->id === null) {
 | 
				
			||||||
 | 
					                error_log("Inbox::postForUser couldn't find user: $_user");
 | 
				
			||||||
                return false;
 | 
					                return false;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -64,7 +64,7 @@ class Outbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
				
			||||||
            $items = [];
 | 
					            $items = [];
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        $host = $_SERVER['SERVER_NAME'];
 | 
					        $host = $_SERVER['SERVER_NAME'];
 | 
				
			||||||
        $id = 'https://' . $host .'/' . $_user . '/outbox';
 | 
					        $id = 'https://' . $host . '/' . $_user . '/outbox';
 | 
				
			||||||
        $outbox->setPartOf($id);
 | 
					        $outbox->setPartOf($id);
 | 
				
			||||||
        $outbox->setID($id);
 | 
					        $outbox->setID($id);
 | 
				
			||||||
        if ($page !== '') {
 | 
					        if ($page !== '') {
 | 
				
			||||||
| 
						 | 
					@ -74,9 +74,9 @@ class Outbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
				
			||||||
            $outbox->setFirst($id);
 | 
					            $outbox->setFirst($id);
 | 
				
			||||||
            $outbox->setLast($id . '&min=0');
 | 
					            $outbox->setLast($id . '&min=0');
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if (sizeof($items)>0) {
 | 
					        if (sizeof($items) > 0) {
 | 
				
			||||||
            $newestId = $items[0]->getPublished();
 | 
					            $newestId = $items[0]->getPublished();
 | 
				
			||||||
            $oldestId = $items[sizeof($items)-1]->getPublished();
 | 
					            $oldestId = $items[sizeof($items) - 1]->getPublished();
 | 
				
			||||||
            $outbox->setNext($id . '&max=' . $newestId);
 | 
					            $outbox->setNext($id . '&max=' . $newestId);
 | 
				
			||||||
            $outbox->setPrev($id . '&min=' . $oldestId);
 | 
					            $outbox->setPrev($id . '&min=' . $oldestId);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
| 
						 | 
					@ -87,11 +87,359 @@ class Outbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * handle post call
 | 
					     * handle post call
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @param string $_user user to add data to outbox @unused-param
 | 
					     * @param string $_user user to add data to outbox
 | 
				
			||||||
     * @return string|false response
 | 
					     * @return string|false response
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function post($_user)
 | 
					    public function post($_user)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 | 
					        $outboxActivity = null;
 | 
				
			||||||
 | 
					        $_rawInput = file_get_contents('php://input');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $allHeaders = getallheaders();
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            $result = $this->main->checkSignature($allHeaders);
 | 
				
			||||||
 | 
					            error_log($result); // Signature verified
 | 
				
			||||||
 | 
					        } catch (\Federator\Exceptions\PermissionDenied $e) {
 | 
				
			||||||
 | 
					            error_log("Outbox::post Signature check failed: " . $e->getMessage());
 | 
				
			||||||
 | 
					            http_response_code(403); // Or 401
 | 
				
			||||||
 | 
					            exit("Access denied");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $activity = json_decode($_rawInput, true);
 | 
				
			||||||
 | 
					        $host = $_SERVER['SERVER_NAME'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $sendTo = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        switch ($activity['type']) {
 | 
				
			||||||
 | 
					            case 'Create':
 | 
				
			||||||
 | 
					                if (!isset($activity['object'])) {
 | 
				
			||||||
 | 
					                    break;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                $obj = $activity['object'];
 | 
				
			||||||
 | 
					                $create = new \Federator\Data\ActivityPub\Common\Create();
 | 
				
			||||||
 | 
					                $create->setID($activity['id'])
 | 
				
			||||||
 | 
					                    ->setURL($activity['id'])
 | 
				
			||||||
 | 
					                    ->setPublished(published: strtotime($activity['published'] ?? $obj['published'] ?? 'now'))
 | 
				
			||||||
 | 
					                    ->setAActor($activity['actor']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (array_key_exists('cc', $activity)) {
 | 
				
			||||||
 | 
					                    foreach ($activity['cc'] as $cc) {
 | 
				
			||||||
 | 
					                        $create->addCC($cc);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (array_key_exists('to', $activity)) {
 | 
				
			||||||
 | 
					                    foreach ($activity['to'] as $to) {
 | 
				
			||||||
 | 
					                        $create->addTo($to);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                switch ($obj['type']) {
 | 
				
			||||||
 | 
					                    case 'Note':
 | 
				
			||||||
 | 
					                        $apNote = new \Federator\Data\ActivityPub\Common\Note();
 | 
				
			||||||
 | 
					                        $apNote->setID($obj['id'])
 | 
				
			||||||
 | 
					                            ->setPublished(strtotime($obj['published'] ?? 'now'))
 | 
				
			||||||
 | 
					                            ->setContent($obj['content'] ?? '')
 | 
				
			||||||
 | 
					                            ->setSummary($obj['summary'])
 | 
				
			||||||
 | 
					                            ->setURL($obj['url'])
 | 
				
			||||||
 | 
					                            ->setAttributedTo($obj['attributedTo'] ?? $activity['actor'])
 | 
				
			||||||
 | 
					                            ->addTo("https://www.w3.org/ns/activitystreams#Public");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        if (!empty($obj['sensitive'])) {
 | 
				
			||||||
 | 
					                            $apNote->setSensitive($obj['sensitive']);
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        if (!empty($obj['conversation'])) {
 | 
				
			||||||
 | 
					                            $apNote->setConversation($obj['conversation']);
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        if (!empty($obj['inReplyTo'])) {
 | 
				
			||||||
 | 
					                            $apNote->setInReplyTo($obj['inReplyTo']);
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        // Handle attachments
 | 
				
			||||||
 | 
					                        if (!empty($obj['attachment']) && is_array($obj['attachment'])) {
 | 
				
			||||||
 | 
					                            foreach ($obj['attachment'] as $media) {
 | 
				
			||||||
 | 
					                                if (!isset($media['type'], $media['url']))
 | 
				
			||||||
 | 
					                                    continue;
 | 
				
			||||||
 | 
					                                $mediaObj = new \Federator\Data\ActivityPub\Common\APObject($media['type']);
 | 
				
			||||||
 | 
					                                $mediaObj->setURL($media['url']);
 | 
				
			||||||
 | 
					                                $apNote->addAttachment($mediaObj);
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        if (array_key_exists('tag', $obj)) {
 | 
				
			||||||
 | 
					                            foreach ($obj['tag'] as $tag) {
 | 
				
			||||||
 | 
					                                $tagName = is_array($tag) && isset($tag['name']) ? $tag['name'] : (string) $tag;
 | 
				
			||||||
 | 
					                                $cleanName = preg_replace('/\s+/', '', ltrim($tagName, '#')); // Remove space and leading #
 | 
				
			||||||
 | 
					                                $tagObj = new \Federator\Data\ActivityPub\Common\Tag();
 | 
				
			||||||
 | 
					                                $tagObj->setName('#' . $cleanName)
 | 
				
			||||||
 | 
					                                    ->setHref("https://$host/tags/" . urlencode($cleanName))
 | 
				
			||||||
 | 
					                                    ->setType('Hashtag');
 | 
				
			||||||
 | 
					                                $apNote->addTag($tagObj);
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        if (array_key_exists('cc', $obj)) {
 | 
				
			||||||
 | 
					                            foreach ($obj['cc'] as $cc) {
 | 
				
			||||||
 | 
					                                $apNote->addCC($cc);
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        $create->setObject($apNote);
 | 
				
			||||||
 | 
					                        break;
 | 
				
			||||||
 | 
					                    default:
 | 
				
			||||||
 | 
					                        error_log("Outbox::post we currently don't support the obj type " . $obj['type'] . "\n");
 | 
				
			||||||
 | 
					                        break;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                $outboxActivity = $create;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					            case 'Announce':
 | 
				
			||||||
 | 
					                if (!isset($activity['object'])) {
 | 
				
			||||||
 | 
					                    break;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                $objectURL = is_array($activity['object']) ? $activity['object']['id'] : $activity['object'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Fetch the original object (e.g. Note)
 | 
				
			||||||
 | 
					                [$response, $info] = \Federator\Main::getFromRemote($objectURL, ['Accept: application/activity+json']);
 | 
				
			||||||
 | 
					                if ($info['http_code'] != 200) {
 | 
				
			||||||
 | 
					                    print_r($info);
 | 
				
			||||||
 | 
					                    error_log("Outbox::post Failed to fetch original object for Announce: $objectURL\n");
 | 
				
			||||||
 | 
					                    break;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                $objData = json_decode($response, true);
 | 
				
			||||||
 | 
					                if ($objData === false || $objData === null || !is_array($objData)) {
 | 
				
			||||||
 | 
					                    break;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                $announce = new \Federator\Data\ActivityPub\Common\Announce();
 | 
				
			||||||
 | 
					                $announce->setID($activity['id'])
 | 
				
			||||||
 | 
					                    ->setURL($activity['id'])
 | 
				
			||||||
 | 
					                    ->setPublished(strtotime($activity['published'] ?? 'now'))
 | 
				
			||||||
 | 
					                    ->setAActor($activity['actor']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (array_key_exists('cc', $activity)) {
 | 
				
			||||||
 | 
					                    foreach ($activity['cc'] as $cc) {
 | 
				
			||||||
 | 
					                        $announce->addCC($cc);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                if (array_key_exists('to', $activity)) {
 | 
				
			||||||
 | 
					                    foreach ($activity['to'] as $to) {
 | 
				
			||||||
 | 
					                        $announce->addTo($to);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Parse the shared object as a Note or something else
 | 
				
			||||||
 | 
					                switch ($objData['type']) {
 | 
				
			||||||
 | 
					                    case 'Note':
 | 
				
			||||||
 | 
					                        $note = new \Federator\Data\ActivityPub\Common\Note();
 | 
				
			||||||
 | 
					                        $note->setID($objData['id'])
 | 
				
			||||||
 | 
					                            ->setSummary($objData['summary'])
 | 
				
			||||||
 | 
					                            ->setContent($objData['content'] ?? '')
 | 
				
			||||||
 | 
					                            ->setPublished(strtotime($objData['published'] ?? 'now'))
 | 
				
			||||||
 | 
					                            ->setURL($objData['url'] ?? $objData['id'])
 | 
				
			||||||
 | 
					                            ->setAttributedTo($objData['attributedTo'] ?? null)
 | 
				
			||||||
 | 
					                            ->addTo("https://www.w3.org/ns/activitystreams#Public");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        if (array_key_exists('cc', $objData)) {
 | 
				
			||||||
 | 
					                            foreach ($objData['cc'] as $cc) {
 | 
				
			||||||
 | 
					                                $note->addCC($cc);
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        $announce->setObject($note);
 | 
				
			||||||
 | 
					                        break;
 | 
				
			||||||
 | 
					                    default:
 | 
				
			||||||
 | 
					                        // fallback object
 | 
				
			||||||
 | 
					                        $fallback = new \Federator\Data\ActivityPub\Common\APObject($objData['type']);
 | 
				
			||||||
 | 
					                        $fallback->setID($objData['id'] ?? $objectURL);
 | 
				
			||||||
 | 
					                        $announce->setObject($fallback);
 | 
				
			||||||
 | 
					                        break;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                $outboxActivity = $announce;
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					            case 'Undo':
 | 
				
			||||||
 | 
					                if (!isset($activity['object'])) {
 | 
				
			||||||
 | 
					                    break;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                $undo = new \Federator\Data\ActivityPub\Common\Undo();
 | 
				
			||||||
 | 
					                $undo->setID($activity['id'] ?? "test")
 | 
				
			||||||
 | 
					                    ->setURL($activity['url'] ?? $activity['id'])
 | 
				
			||||||
 | 
					                    ->setActor($activity['actor'] ?? null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (array_key_exists('cc', $activity)) {
 | 
				
			||||||
 | 
					                    foreach ($activity['cc'] as $cc) {
 | 
				
			||||||
 | 
					                        $undo->addCC($cc);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                if (array_key_exists('to', $activity)) {
 | 
				
			||||||
 | 
					                    foreach ($activity['to'] as $to) {
 | 
				
			||||||
 | 
					                        $undo->addTo($to);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // what was undone
 | 
				
			||||||
 | 
					                $undone = $activity['object'];
 | 
				
			||||||
 | 
					                if (is_array($undone) && isset($undone['type'])) {
 | 
				
			||||||
 | 
					                    switch ($undone['type']) {
 | 
				
			||||||
 | 
					                        case 'Announce':
 | 
				
			||||||
 | 
					                            $announce = new \Federator\Data\ActivityPub\Common\Announce();
 | 
				
			||||||
 | 
					                            $announce->setID($undone['id'] ?? null)
 | 
				
			||||||
 | 
					                                ->setAActor($undone['actor'] ?? null)
 | 
				
			||||||
 | 
					                                ->setURL($undone['url'] ?? $undone['id'])
 | 
				
			||||||
 | 
					                                ->setPublished(strtotime($undone['published'] ?? 'now'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            if (array_key_exists('cc', $undone)) {
 | 
				
			||||||
 | 
					                                foreach ($undone['cc'] as $cc) {
 | 
				
			||||||
 | 
					                                    $announce->addCC($cc);
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                            $undo->setObject($announce);
 | 
				
			||||||
 | 
					                            break;
 | 
				
			||||||
 | 
					                        case 'Follow':
 | 
				
			||||||
 | 
					                            // Implement if needed
 | 
				
			||||||
 | 
					                            break;
 | 
				
			||||||
 | 
					                        default:
 | 
				
			||||||
 | 
					                            // Fallback for unknown types
 | 
				
			||||||
 | 
					                            $apObject = new \Federator\Data\ActivityPub\Common\APObject($undone['type']);
 | 
				
			||||||
 | 
					                            $apObject->setID($undone['id'] ?? null);
 | 
				
			||||||
 | 
					                            $undo->setObject($apObject);
 | 
				
			||||||
 | 
					                            break;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                $outboxActivity = $undo;
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					            default:
 | 
				
			||||||
 | 
					                error_log("Outbox::post we currently don't support the activity type " . $activity['type'] . "\n");
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $sendTo = $outboxActivity->getCC();
 | 
				
			||||||
 | 
					        if ($outboxActivity->getType() === 'Undo') {
 | 
				
			||||||
 | 
					            $sendTo = $outboxActivity->getObject()->getCC();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $users = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        foreach ($sendTo as $receiver) {
 | 
				
			||||||
 | 
					            if (!$receiver || !is_string($receiver)) {
 | 
				
			||||||
 | 
					                continue;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (str_ends_with($receiver, '/followers')) {
 | 
				
			||||||
 | 
					                $users = array_merge($users, $this->fetchAllFollowers($receiver, $host));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if ($_user !== false && !in_array($_user, $users)) {
 | 
				
			||||||
 | 
					            $users[] = $_user;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        foreach ($users as $user) {
 | 
				
			||||||
 | 
					            if (!$user)
 | 
				
			||||||
 | 
					                continue;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            $this->postForUser($user, $outboxActivity);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return json_encode($outboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * handle post call for specific user
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param string $_user user to add data to outbox
 | 
				
			||||||
 | 
					     * @param \Federator\Data\ActivityPub\Common\Activity $outboxActivity the activity that we received
 | 
				
			||||||
 | 
					     * @return string|false response
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private function postForUser($_user, $outboxActivity)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        if ($_user) {
 | 
				
			||||||
 | 
					            $dbh = $this->main->getDatabase();
 | 
				
			||||||
 | 
					            $cache = $this->main->getCache();
 | 
				
			||||||
 | 
					            $connector = $this->main->getConnector();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // get user
 | 
				
			||||||
 | 
					            $user = \Federator\DIO\User::getUserByName(
 | 
				
			||||||
 | 
					                $dbh,
 | 
				
			||||||
 | 
					                $_user,
 | 
				
			||||||
 | 
					                $connector,
 | 
				
			||||||
 | 
					                $cache
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            if ($user->id === null) {
 | 
				
			||||||
 | 
					                error_log("Outbox::postForUser couldn't find user: $_user");
 | 
				
			||||||
                return false;
 | 
					                return false;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $rootDir = $_SERVER['DOCUMENT_ROOT'] . '../';
 | 
				
			||||||
 | 
					        // Save the raw input and parsed JSON to a file for inspection
 | 
				
			||||||
 | 
					        file_put_contents(
 | 
				
			||||||
 | 
					            $rootDir . 'logs/outbox_' . $_user . '.log',
 | 
				
			||||||
 | 
					            date('Y-m-d H:i:s') . ": ==== POST " . $_user . " Outbox Activity ====\n" . json_encode($outboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n",
 | 
				
			||||||
 | 
					            FILE_APPEND
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * fetch all followers from url and return the ones that belong to our server
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param string $collectionUrl The url of f.e. the posters followers
 | 
				
			||||||
 | 
					     * @param string $host our current host-url
 | 
				
			||||||
 | 
					     * @return array|false the names of the followers that are hosted on our server
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private function fetchAllFollowers(string $collectionUrl, string $host): array
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $users = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        [$collectionResponse, $collectionInfo] = \Federator\Main::getFromRemote($collectionUrl, ['Accept: application/activity+json']);
 | 
				
			||||||
 | 
					        if ($collectionInfo['http_code'] !== 200) {
 | 
				
			||||||
 | 
					            error_log("Outbox::fetchAllFollowers Failed to fetch follower collection metadata from $collectionUrl");
 | 
				
			||||||
 | 
					            return [];
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $collectionData = json_decode($collectionResponse, true);
 | 
				
			||||||
 | 
					        $nextPage = $collectionData['first'] ?? $collectionData['current'] ?? null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!$nextPage) {
 | 
				
			||||||
 | 
					            error_log("Outbox::fetchAllFollowers No 'first' or 'current' page in collection at $collectionUrl");
 | 
				
			||||||
 | 
					            return [];
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Loop through all pages
 | 
				
			||||||
 | 
					        while ($nextPage) {
 | 
				
			||||||
 | 
					            [$pageResponse, $pageInfo] = \Federator\Main::getFromRemote($nextPage, ['Accept: application/activity+json']);
 | 
				
			||||||
 | 
					            if ($pageInfo['http_code'] !== 200) {
 | 
				
			||||||
 | 
					                error_log("Outbox::fetchAllFollowers Failed to fetch follower page at $nextPage");
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            $pageData = json_decode($pageResponse, true);
 | 
				
			||||||
 | 
					            $items = $pageData['orderedItems'] ?? $pageData['items'] ?? [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            foreach ($items as $followerUrl) {
 | 
				
			||||||
 | 
					                $parts = parse_url($followerUrl);
 | 
				
			||||||
 | 
					                if (!isset($parts['host']) || !str_ends_with($parts['host'], $host)) {
 | 
				
			||||||
 | 
					                    continue;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                [$actorResponse, $actorInfo] = \Federator\Main::getFromRemote($followerUrl, ['Accept: application/activity+json']);
 | 
				
			||||||
 | 
					                if ($actorInfo['http_code'] !== 200) {
 | 
				
			||||||
 | 
					                    error_log("Outbox::fetchAllFollowers Failed to fetch actor data for follower: $followerUrl");
 | 
				
			||||||
 | 
					                    continue;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                $actorData = json_decode($actorResponse, true);
 | 
				
			||||||
 | 
					                if (isset($actorData['preferredUsername'])) {
 | 
				
			||||||
 | 
					                    $users[] = $actorData['preferredUsername'];
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            $nextPage = $pageData['next'] ?? null;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return $users;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		
		Reference in a new issue