forked from grumpydevelop/federator
		
	very rudimental follower-support
- fixed rewrites to properly support @username/outbox, username/outbox, users/username/outbox, ... - initial support for sending follow to e.g. mastodon (code commented out in api.php) - database migration for follows table and fedusers table - save retrieved fedusers in cache and db - depend more on configs externaldomain, less on server_name - properly implemented following-logic in dio - made postForuUser static in newContent and inbox - provide rsaprivate for signing reuests -> actPub-Servers - change contenttype depending on use-case - changed user json-template to have loop-back between user-template and webfinger (mastodon f.e. needs this)
This commit is contained in:
		
							parent
							
								
									ce7aa5c72d
								
							
						
					
					
						commit
						da18d37a79
					
				
					 31 changed files with 1103 additions and 243 deletions
				
			
		| 
						 | 
				
			
			@ -53,9 +53,10 @@ To configure an apache server, add the following rewrite rules:
 | 
			
		|||
      RewriteRule ^@(.*)$ /federator.php?_call=fedusers/$1 [L,END]
 | 
			
		||||
      RewriteRule ^users/(.*)$ /federator.php?_call=fedusers/$1 [L,END]
 | 
			
		||||
      RewriteRule ^inbox[/]?$ /federator.php?_call=fedusers/inbox [L,END]
 | 
			
		||||
      RewriteRule ^api/federator/(.+)$ federator.php?_call=$1 [L,END]
 | 
			
		||||
      RewriteRule ^api/federator/(.+)$ /federator.php?_call=$1 [L,END]
 | 
			
		||||
      RewriteRule ^(\.well-known/.*)$ /federator.php?_call=$1 [L,END]
 | 
			
		||||
      RewriteRule ^(nodeinfo/2\.[01])$ /federator.php?_call=$1 [L,END]
 | 
			
		||||
      RewriteRule ^([a-zA-Z0-9_-]+.*)$ /federator.php?_call=fedusers/$1 [L,END]
 | 
			
		||||
    </Directory>
 | 
			
		||||
 | 
			
		||||
change your document root for the domain you want to use (or default one if using localhost) to the directory you installed it, with the /htdocs at the end. A user should only be able to open that file, not the other data.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,7 +17,7 @@
 | 
			
		|||
            <div class="request-box border p-4 rounded-lg mb-4 bg-gray-50 overflow-y-auto">
 | 
			
		||||
                <label class="block font-medium">API target link</label>
 | 
			
		||||
                <input type="text" class="target-link-input w-full p-2 border rounded-md mb-2"
 | 
			
		||||
                    placeholder="Enter target link" value="federator/fedusers/grumpydevelop/outbox?page=0">
 | 
			
		||||
                    placeholder="Enter target link" value="users/grumpydevelop/outbox?page=0">
 | 
			
		||||
 | 
			
		||||
                <label class="block font-medium">Request type</label>
 | 
			
		||||
                <input type="text" class="request-type-input w-full p-2 border rounded-md mb-2"
 | 
			
		||||
| 
						 | 
				
			
			@ -65,7 +65,7 @@
 | 
			
		|||
                ...(profile ? { "X-Profile": profile } : {})
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            fetch("http://localhost/api/" + targetLink, {
 | 
			
		||||
            fetch("http://localhost/" + targetLink, {
 | 
			
		||||
                method: requestType,
 | 
			
		||||
                headers
 | 
			
		||||
            })
 | 
			
		||||
| 
						 | 
				
			
			@ -94,7 +94,7 @@
 | 
			
		|||
            const container = document.getElementById("request-container");
 | 
			
		||||
            const requestBox = container.firstElementChild.cloneNode(true);
 | 
			
		||||
 | 
			
		||||
            requestBox.querySelector(".target-link-input").value = "federator/fedusers/grumpydevelop@contentnation.net/outbox?page=0";
 | 
			
		||||
            requestBox.querySelector(".target-link-input").value = "users/grumpydevelop@contentnation.net/outbox?page=0";
 | 
			
		||||
            requestBox.querySelector(".request-type-input").value = "GET";
 | 
			
		||||
            requestBox.querySelector(".session-input").value = "";
 | 
			
		||||
            requestBox.querySelector(".profile-input").value = "";
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -101,6 +101,7 @@ class Api extends Main
 | 
			
		|||
                break;
 | 
			
		||||
            case 'fedusers':
 | 
			
		||||
                $handler = new Api\FedUsers($this);
 | 
			
		||||
                $this->setContentType("application/activity+json");
 | 
			
		||||
                break;
 | 
			
		||||
            case 'v1':
 | 
			
		||||
                switch ($this->paths[1]) {
 | 
			
		||||
| 
						 | 
				
			
			@ -110,6 +111,39 @@ class Api extends Main
 | 
			
		|||
                    case 'newcontent':
 | 
			
		||||
                        $handler = new Api\V1\NewContent($this);
 | 
			
		||||
                        break;
 | 
			
		||||
                    /* case 'sendFollow': { // hacky implementation for testing purposes
 | 
			
		||||
                        $username = $this->paths[2];
 | 
			
		||||
                        $domain = $this->config['generic']['externaldomain'];
 | 
			
		||||
                        $response = \Federator\DIO\Followers::sendFollowRequest(
 | 
			
		||||
                            $this->dbh,
 | 
			
		||||
                            $this->connector,
 | 
			
		||||
                            $this->cache,
 | 
			
		||||
                            $username,
 | 
			
		||||
                            "admin@mastodon.local",
 | 
			
		||||
                            $domain
 | 
			
		||||
                        );
 | 
			
		||||
                        header("Content-type: " . $this->contentType);
 | 
			
		||||
                        header("Access-Control-Allow-Origin: *");
 | 
			
		||||
                        header("Cache-Control: no-cache, no-store, must-revalidate");
 | 
			
		||||
                        header("Pragma: no-cache");
 | 
			
		||||
                        header("Expires: 0");
 | 
			
		||||
                        if (is_string($response)) {
 | 
			
		||||
                            $this->setResponseCode(200);
 | 
			
		||||
                            $retval = json_encode(array(
 | 
			
		||||
                                "status" => "ok",
 | 
			
		||||
                                "message" => $response
 | 
			
		||||
                            ));
 | 
			
		||||
                        } else {
 | 
			
		||||
                            $this->setResponseCode(500);
 | 
			
		||||
                            $retval = json_encode(array(
 | 
			
		||||
                                "status" => "error",
 | 
			
		||||
                                "message" => "Failed to send follow request"
 | 
			
		||||
                            ));
 | 
			
		||||
                        }
 | 
			
		||||
                        http_response_code($this->responseCode);
 | 
			
		||||
                        echo $retval;
 | 
			
		||||
                        return;
 | 
			
		||||
                    } */
 | 
			
		||||
                }
 | 
			
		||||
                break;
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -50,21 +50,10 @@ class FedUsers implements APIInterface
 | 
			
		|||
        $method = $_SERVER["REQUEST_METHOD"];
 | 
			
		||||
        $handler = null;
 | 
			
		||||
        $_username = $paths[1];
 | 
			
		||||
        if (preg_match("#^([^@]+)@([^/]+)#", $_username, $matches) != 1) {
 | 
			
		||||
            $hostUrl = $this->main->getHost();
 | 
			
		||||
            if ($hostUrl !== false) {
 | 
			
		||||
                $host = parse_url($hostUrl, PHP_URL_HOST);
 | 
			
		||||
                $port = parse_url($hostUrl, PHP_URL_PORT);
 | 
			
		||||
                if ($port !== null) {
 | 
			
		||||
                    $host .= ":$port";
 | 
			
		||||
                }
 | 
			
		||||
                $_username = "$_username@$host";
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        switch (sizeof($paths)) {
 | 
			
		||||
            case 2:
 | 
			
		||||
                if ($method === 'GET') {
 | 
			
		||||
                    // /fedusers/username or /@username
 | 
			
		||||
                    // /users/username or /@username or /username
 | 
			
		||||
                    return $this->returnUserProfile($_username);
 | 
			
		||||
                } else {
 | 
			
		||||
                    switch ($paths[1]) {
 | 
			
		||||
| 
						 | 
				
			
			@ -78,7 +67,7 @@ class FedUsers implements APIInterface
 | 
			
		|||
                }
 | 
			
		||||
                break;
 | 
			
		||||
            case 3:
 | 
			
		||||
                // /fedusers/username/(inbox|outbox|following|followers)
 | 
			
		||||
                // /users/username/(inbox|outbox|following|followers)
 | 
			
		||||
                switch ($paths[2]) {
 | 
			
		||||
                    case 'following':
 | 
			
		||||
                        // $handler = new FedUsers\Following();
 | 
			
		||||
| 
						 | 
				
			
			@ -95,7 +84,7 @@ class FedUsers implements APIInterface
 | 
			
		|||
                }
 | 
			
		||||
                break;
 | 
			
		||||
            case 4:
 | 
			
		||||
                // /fedusers/username/collections/(features|tags)
 | 
			
		||||
                // /users/username/collections/(features|tags)
 | 
			
		||||
                // not yet implemented
 | 
			
		||||
                break;
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -153,7 +142,7 @@ class FedUsers implements APIInterface
 | 
			
		|||
            'publickey' => trim($jsonKey, '"'),
 | 
			
		||||
            'registered' => gmdate('Y-m-d\TH:i:s\Z', $user->registered), // 2021-03-25T00:00:00Z
 | 
			
		||||
            'summary' => $user->summary,
 | 
			
		||||
            'type' => $user->type
 | 
			
		||||
            'type' => ucfirst($user->type) // capitalized user type
 | 
			
		||||
        ];
 | 
			
		||||
        $this->response = $this->main->renderTemplate('user.json', $data);
 | 
			
		||||
        return true;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -60,7 +60,6 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
			
		|||
        }
 | 
			
		||||
 | 
			
		||||
        $activity = is_string($_rawInput) ? json_decode($_rawInput, true) : null;
 | 
			
		||||
        $host = $_SERVER['SERVER_NAME'];
 | 
			
		||||
 | 
			
		||||
        if (!is_array($activity)) {
 | 
			
		||||
            error_log("Inbox::post Input wasn't of type array");
 | 
			
		||||
| 
						 | 
				
			
			@ -69,10 +68,10 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
			
		|||
 | 
			
		||||
        $inboxActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($activity);
 | 
			
		||||
 | 
			
		||||
        $rootDir = $_SERVER['DOCUMENT_ROOT'] . '../';
 | 
			
		||||
 | 
			
		||||
        // Shared inbox
 | 
			
		||||
        if (!isset($_user)) {
 | 
			
		||||
            $rootDir = $_SERVER['DOCUMENT_ROOT'] . '../';
 | 
			
		||||
 | 
			
		||||
            // Save the raw input and parsed JSON to a file for inspection
 | 
			
		||||
            file_put_contents(
 | 
			
		||||
                $rootDir . 'logs/inbox.log',
 | 
			
		||||
| 
						 | 
				
			
			@ -89,12 +88,15 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
			
		|||
        $sendTo = $inboxActivity->getCC();
 | 
			
		||||
        if ($inboxActivity->getType() === 'Undo') { // for undo the object holds the proper cc
 | 
			
		||||
            $object = $inboxActivity->getObject();
 | 
			
		||||
            if ($object !== null) {
 | 
			
		||||
            if ($object !== null && is_object($object)) {
 | 
			
		||||
                $sendTo = $object->getCC();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $users = [];
 | 
			
		||||
        $dbh = $this->main->getDatabase();
 | 
			
		||||
        $cache = $this->main->getCache();
 | 
			
		||||
        $connector = $this->main->getConnector();
 | 
			
		||||
 | 
			
		||||
        foreach ($sendTo as $receiver) {
 | 
			
		||||
            if ($receiver === '' || !is_string($receiver)) {
 | 
			
		||||
| 
						 | 
				
			
			@ -102,9 +104,23 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
			
		|||
            }
 | 
			
		||||
 | 
			
		||||
            if (str_ends_with($receiver, '/followers')) {
 | 
			
		||||
                $followers = $this->fetchAllFollowers($receiver, $host);
 | 
			
		||||
                $actor = $inboxActivity->getAActor();
 | 
			
		||||
                if ($actor === null || !is_string($actor)) {
 | 
			
		||||
                    error_log("Inbox::post no actor found");
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Extract username from the actor URL
 | 
			
		||||
                $username = basename((string)(parse_url($actor, PHP_URL_PATH) ?? ''));
 | 
			
		||||
                $domain = parse_url($actor, PHP_URL_HOST);
 | 
			
		||||
                if ($username === null || $domain === null) {
 | 
			
		||||
                    error_log("Inbox::post no username or domain found");
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
                $followers = \Federator\DIO\Followers::getFollowersByFedUser($dbh, $connector, $cache, $username . '@' . $domain);
 | 
			
		||||
 | 
			
		||||
                if (is_array($followers)) {
 | 
			
		||||
                    $users = array_merge($users, $followers);
 | 
			
		||||
                    $users = array_merge($users, array_column($followers, 'id'));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -116,7 +132,7 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
			
		|||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $this->postForUser($user, $inboxActivity);
 | 
			
		||||
            $this->postForUser($dbh, $connector, $cache, $user, $inboxActivity);
 | 
			
		||||
        }
 | 
			
		||||
        return json_encode($inboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -124,17 +140,16 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
			
		|||
    /**
 | 
			
		||||
     * handle post call for specific user
 | 
			
		||||
     *
 | 
			
		||||
     * @param \mysqli $dbh database handle
 | 
			
		||||
     * @param \Federator\Connector\Connector $connector connector to use
 | 
			
		||||
     * @param \Federator\Cache\Cache|null $cache optional caching service
 | 
			
		||||
     * @param string $_user user to add data to inbox
 | 
			
		||||
     * @param \Federator\Data\ActivityPub\Common\Activity $inboxActivity the activity that we received
 | 
			
		||||
     * @return boolean response
 | 
			
		||||
     */
 | 
			
		||||
    private function postForUser($_user, $inboxActivity)
 | 
			
		||||
    private static function postForUser($dbh, $connector, $cache, $_user, $inboxActivity)
 | 
			
		||||
    {
 | 
			
		||||
        if (isset($_user)) {
 | 
			
		||||
            $dbh = $this->main->getDatabase();
 | 
			
		||||
            $cache = $this->main->getCache();
 | 
			
		||||
            $connector = $this->main->getConnector();
 | 
			
		||||
 | 
			
		||||
            // get user
 | 
			
		||||
            $user = \Federator\DIO\User::getUserByName(
 | 
			
		||||
                $dbh,
 | 
			
		||||
| 
						 | 
				
			
			@ -142,7 +157,7 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
			
		|||
                $connector,
 | 
			
		||||
                $cache
 | 
			
		||||
            );
 | 
			
		||||
            if ($user->id === null) {
 | 
			
		||||
            if ($user === null || $user->id === null) {
 | 
			
		||||
                error_log("Inbox::postForUser couldn't find user: $_user");
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
| 
						 | 
				
			
			@ -158,64 +173,4 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
			
		|||
 | 
			
		||||
        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 string[] the names of the followers that are hosted on our server
 | 
			
		||||
     */
 | 
			
		||||
    private static 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("Inbox::fetchAllFollowers Failed to fetch follower collection metadata from $collectionUrl");
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $collectionData = json_decode($collectionResponse, true);
 | 
			
		||||
        $nextPage = $collectionData['first'] ?? $collectionData['current'] ?? null;
 | 
			
		||||
 | 
			
		||||
        if (!isset($nextPage)) {
 | 
			
		||||
            error_log("Inbox::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("Inbox::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("Inbox::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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -66,15 +66,16 @@ class Outbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
			
		|||
        } else {
 | 
			
		||||
            $items = [];
 | 
			
		||||
        }
 | 
			
		||||
        $host = $_SERVER['SERVER_NAME'];
 | 
			
		||||
        $id = 'https://' . $host . '/users/' . $_user . '/outbox';
 | 
			
		||||
        $config = $this->main->getConfig();
 | 
			
		||||
        $domain = $config['generic']['externaldomain'];
 | 
			
		||||
        $id = 'https://' . $domain . '/users/' . $_user . '/outbox';
 | 
			
		||||
        $outbox->setPartOf($id);
 | 
			
		||||
        $outbox->setID($id);
 | 
			
		||||
        if ($page !== '') {
 | 
			
		||||
            $id .= '?page=' . urlencode($page);
 | 
			
		||||
        }
 | 
			
		||||
        if ($page === '' || $outbox->count() == 0) {
 | 
			
		||||
            $outbox->setFirst($id);
 | 
			
		||||
            $outbox->setFirst($id . '?page=0');
 | 
			
		||||
            $outbox->setLast($id . '&min=0');
 | 
			
		||||
        }
 | 
			
		||||
        if (sizeof($items) > 0) {
 | 
			
		||||
| 
						 | 
				
			
			@ -84,7 +85,7 @@ class Outbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
			
		|||
            $outbox->setPrev($id . '&min=' . $oldestId);
 | 
			
		||||
        }
 | 
			
		||||
        $obj = $outbox->toObject();
 | 
			
		||||
        return json_encode($obj);
 | 
			
		||||
        return json_encode($obj, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,12 +20,8 @@ class Dummy implements \Federator\Api\APIInterface
 | 
			
		|||
     */
 | 
			
		||||
    private $main;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * internal message to output
 | 
			
		||||
     *
 | 
			
		||||
     * @var string $message
 | 
			
		||||
     */
 | 
			
		||||
    private $message = '';
 | 
			
		||||
    /** @var array<string, string> $message internal message to output */
 | 
			
		||||
    private $message = [];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * constructor
 | 
			
		||||
| 
						 | 
				
			
			@ -47,9 +43,9 @@ class Dummy implements \Federator\Api\APIInterface
 | 
			
		|||
    public function exec($paths, $user): bool
 | 
			
		||||
    {
 | 
			
		||||
        // only for user with the 'publish' permission
 | 
			
		||||
        // if ($user === false || $user->hasPermission('publish') === false) {
 | 
			
		||||
        //     throw new \Federator\Exceptions\PermissionDenied();
 | 
			
		||||
        // }
 | 
			
		||||
        if ($user === false || $user->hasPermission('publish') === false) {
 | 
			
		||||
            throw new \Federator\Exceptions\PermissionDenied();
 | 
			
		||||
        }
 | 
			
		||||
        $method = $_SERVER["REQUEST_METHOD"];
 | 
			
		||||
        switch ($method) {
 | 
			
		||||
            case 'GET':
 | 
			
		||||
| 
						 | 
				
			
			@ -87,16 +83,13 @@ class Dummy implements \Federator\Api\APIInterface
 | 
			
		|||
     */
 | 
			
		||||
    public function getDummy()
 | 
			
		||||
    {
 | 
			
		||||
        $dummyResponse = json_encode([
 | 
			
		||||
        $this->message = [
 | 
			
		||||
            'r1' => '             (__)       ',
 | 
			
		||||
            'r2' => '      `------(oo)       ',
 | 
			
		||||
            'r3' => '       || __ (__)       ',
 | 
			
		||||
            'r4' => '       ||w  ||          ',
 | 
			
		||||
            'r5' => '                        '
 | 
			
		||||
        ], JSON_PRETTY_PRINT);
 | 
			
		||||
        if ($dummyResponse !== false) {
 | 
			
		||||
            $this->message = $dummyResponse;
 | 
			
		||||
        }
 | 
			
		||||
        ];
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -117,6 +110,6 @@ class Dummy implements \Federator\Api\APIInterface
 | 
			
		|||
     */
 | 
			
		||||
    public function toJson()
 | 
			
		||||
    {
 | 
			
		||||
        return $this->message;
 | 
			
		||||
        return json_encode($this->message, JSON_PRETTY_PRINT) . "\n";
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -71,12 +71,11 @@ class NewContent implements \Federator\Api\APIInterface
 | 
			
		|||
    /**
 | 
			
		||||
     * handle post call
 | 
			
		||||
     *
 | 
			
		||||
     * @param string|null $_user user that triggered the post
 | 
			
		||||
     * @param string|null $_user optional user that triggered the post
 | 
			
		||||
     * @return string|false response
 | 
			
		||||
     */
 | 
			
		||||
    public function post($_user)
 | 
			
		||||
    {
 | 
			
		||||
        error_log("NewContent::post called with user: $_user");
 | 
			
		||||
        $_rawInput = file_get_contents('php://input');
 | 
			
		||||
 | 
			
		||||
        $allHeaders = getallheaders();
 | 
			
		||||
| 
						 | 
				
			
			@ -89,7 +88,9 @@ class NewContent implements \Federator\Api\APIInterface
 | 
			
		|||
        }
 | 
			
		||||
 | 
			
		||||
        $input = is_string($_rawInput) ? json_decode($_rawInput, true) : null;
 | 
			
		||||
        $host = $_SERVER['SERVER_NAME'];
 | 
			
		||||
 | 
			
		||||
        $config = $this->main->getConfig();
 | 
			
		||||
        $domain = $config['generic']['externaldomain'];
 | 
			
		||||
        if (!is_array($input)) {
 | 
			
		||||
            error_log("NewContent::post Input wasn't of type array");
 | 
			
		||||
            return false;
 | 
			
		||||
| 
						 | 
				
			
			@ -98,7 +99,8 @@ class NewContent implements \Federator\Api\APIInterface
 | 
			
		|||
        if (isset($allHeaders['X-Sender'])) {
 | 
			
		||||
            $newActivity = $this->main->getConnector()->jsonToActivity($input);
 | 
			
		||||
        } else {
 | 
			
		||||
            $newActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($input);
 | 
			
		||||
            error_log("NewContent::post No X-Sender header found");
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($newActivity === false) {
 | 
			
		||||
| 
						 | 
				
			
			@ -106,27 +108,26 @@ class NewContent implements \Federator\Api\APIInterface
 | 
			
		|||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $sendTo = $newActivity->getCC();
 | 
			
		||||
        if ($newActivity->getType() === 'Undo') {
 | 
			
		||||
            $object = $newActivity->getObject();
 | 
			
		||||
            if ($object !== null) {
 | 
			
		||||
                $sendTo = $object->getCC();
 | 
			
		||||
            }
 | 
			
		||||
        $dbh = $this->main->getDatabase();
 | 
			
		||||
        $cache = $this->main->getCache();
 | 
			
		||||
        $connector = $this->main->getConnector();
 | 
			
		||||
 | 
			
		||||
        if (!isset($_user)) {
 | 
			
		||||
            $user = $newActivity->getAActor(); // url of the sender https://contentnation.net/username
 | 
			
		||||
            $user = str_replace(
 | 
			
		||||
                $domain,
 | 
			
		||||
                '',
 | 
			
		||||
                $user
 | 
			
		||||
            ); // retrieve only the last part of the url
 | 
			
		||||
        } else {
 | 
			
		||||
            $user = $dbh->real_escape_string($_user);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $users = [];
 | 
			
		||||
 | 
			
		||||
        foreach ($sendTo as $receiver) {
 | 
			
		||||
            if ($receiver === '' || !is_string($receiver)) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (str_ends_with($receiver, '/followers')) {
 | 
			
		||||
                $followers = $this->fetchAllFollowers($receiver, $host);
 | 
			
		||||
                if (is_array($followers)) {
 | 
			
		||||
                    $users = array_merge($users, $followers);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        $followers = $this->fetchAllFollowers($dbh, $connector, $cache, $user);
 | 
			
		||||
        if (!empty($followers)) {
 | 
			
		||||
            $users = array_merge($users, $followers);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (empty($users)) { // todo remove after proper implementation, debugging for now
 | 
			
		||||
| 
						 | 
				
			
			@ -147,7 +148,7 @@ class NewContent implements \Federator\Api\APIInterface
 | 
			
		|||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $this->postForUser($user, $newActivity);
 | 
			
		||||
            $this->postForUser($dbh, $connector, $cache, $user, $newActivity);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return json_encode($newActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
 | 
			
		||||
| 
						 | 
				
			
			@ -156,28 +157,33 @@ class NewContent implements \Federator\Api\APIInterface
 | 
			
		|||
    /**
 | 
			
		||||
     * handle post call for specific user
 | 
			
		||||
     *
 | 
			
		||||
     * @param \mysqli $dbh @unused-param
 | 
			
		||||
     *          database handle
 | 
			
		||||
     * @param \Federator\Connector\Connector $connector
 | 
			
		||||
     *          connector to fetch use with
 | 
			
		||||
     * @param \Federator\Cache\Cache|null $cache
 | 
			
		||||
     *          optional caching service
 | 
			
		||||
     * @param string $_user user that triggered the post
 | 
			
		||||
     * @param \Federator\Data\ActivityPub\Common\Activity $newActivity the activity that we received
 | 
			
		||||
     * @return boolean response
 | 
			
		||||
     */
 | 
			
		||||
    private function postForUser($_user, $newActivity)
 | 
			
		||||
    private static function postForUser($dbh, $connector, $cache, $_user, $newActivity)
 | 
			
		||||
    {
 | 
			
		||||
        if (isset($_user)) {
 | 
			
		||||
            $dbh = $this->main->getDatabase();
 | 
			
		||||
            $cache = $this->main->getCache();
 | 
			
		||||
            $connector = $this->main->getConnector();
 | 
			
		||||
        if (!isset($_user)) {
 | 
			
		||||
            error_log("NewContent::postForUser no user given");
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
            // get user
 | 
			
		||||
            $user = \Federator\DIO\User::getUserByName(
 | 
			
		||||
                $dbh,
 | 
			
		||||
                $_user,
 | 
			
		||||
                $connector,
 | 
			
		||||
                $cache
 | 
			
		||||
            );
 | 
			
		||||
            if ($user->id === null) {
 | 
			
		||||
                error_log("NewContent::postForUser couldn't find user: $_user");
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
        // get user
 | 
			
		||||
        $user = \Federator\DIO\User::getUserByName(
 | 
			
		||||
            $dbh,
 | 
			
		||||
            $_user,
 | 
			
		||||
            $connector,
 | 
			
		||||
            $cache
 | 
			
		||||
        );
 | 
			
		||||
        if ($user->id === null) {
 | 
			
		||||
            error_log("NewContent::postForUser couldn't find user: $_user");
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $rootDir = $_SERVER['DOCUMENT_ROOT'] . '../';
 | 
			
		||||
| 
						 | 
				
			
			@ -194,58 +200,32 @@ class NewContent implements \Federator\Api\APIInterface
 | 
			
		|||
    /**
 | 
			
		||||
     * 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
 | 
			
		||||
     * @param \mysqli $dbh
 | 
			
		||||
     *          database handle
 | 
			
		||||
     * @param \Federator\Connector\Connector $connector
 | 
			
		||||
     *          connector to fetch use with
 | 
			
		||||
     * @param \Federator\Cache\Cache|null $cache
 | 
			
		||||
     *          optional caching service
 | 
			
		||||
     * @param string $userId The id of the user
 | 
			
		||||
     * @return string[] the names of the followers that are hosted on our server
 | 
			
		||||
     */
 | 
			
		||||
    private static function fetchAllFollowers(string $collectionUrl, string $host): array
 | 
			
		||||
    private static function fetchAllFollowers($dbh, $connector, $cache, string $userId): array
 | 
			
		||||
    {
 | 
			
		||||
        if (empty($userId)) {
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $users = [];
 | 
			
		||||
 | 
			
		||||
        [$collectionResponse, $collectionInfo] = \Federator\Main::getFromRemote($collectionUrl, ['Accept: application/activity+json']);
 | 
			
		||||
        if ($collectionInfo['http_code'] != 200) {
 | 
			
		||||
            error_log("NewContent::fetchAllFollowers Failed to fetch follower collection metadata from $collectionUrl");
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
        $apFollowers = \Federator\DIO\Followers::getFollowersByUser(
 | 
			
		||||
            $dbh,
 | 
			
		||||
            $userId,
 | 
			
		||||
            $connector,
 | 
			
		||||
            cache: $cache,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $collectionData = json_decode($collectionResponse, true);
 | 
			
		||||
        $nextPage = $collectionData['first'] ?? $collectionData['current'] ?? null;
 | 
			
		||||
 | 
			
		||||
        if (!isset($nextPage)) {
 | 
			
		||||
            error_log("NewContent::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("NewContent::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("NewContent::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;
 | 
			
		||||
        foreach ($apFollowers as $follower) {
 | 
			
		||||
            $users[] = $follower->id;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $users;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -44,8 +44,10 @@ class WellKnown implements APIInterface
 | 
			
		|||
     */
 | 
			
		||||
    private function hostMeta()
 | 
			
		||||
    {
 | 
			
		||||
        $config = $this->main->getConfig();
 | 
			
		||||
        $domain = $config['generic']['externaldomain'];
 | 
			
		||||
        $data = [
 | 
			
		||||
          'fqdn' => $_SERVER['SERVER_NAME']
 | 
			
		||||
          'fqdn' => $domain
 | 
			
		||||
        ];
 | 
			
		||||
        $this->response = $this->main->renderTemplate('host-meta.xml', $data);
 | 
			
		||||
        return true;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -45,8 +45,10 @@ class NodeInfo
 | 
			
		|||
     */
 | 
			
		||||
    public function exec($paths)
 | 
			
		||||
    {
 | 
			
		||||
        $config = $this->main->getConfig();
 | 
			
		||||
        $domain = $config['generic']['externaldomain'];
 | 
			
		||||
        $data = [
 | 
			
		||||
            'fqdn' => $_SERVER['SERVER_NAME']
 | 
			
		||||
            'fqdn' => $domain
 | 
			
		||||
        ];
 | 
			
		||||
        $template = null;
 | 
			
		||||
        if (sizeof($paths) == 2 && $paths[0] === '.well-known' && $paths[1] === 'nodeinfo') {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										19
									
								
								php/federator/cache/cache.php
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										19
									
								
								php/federator/cache/cache.php
									
										
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -17,7 +17,7 @@ interface Cache extends \Federator\Connector\Connector
 | 
			
		|||
     * save remote followers of user
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $user user name
 | 
			
		||||
     * @param \Federator\Data\ActivityPub\Common\APObject[]|false $followers user followers
 | 
			
		||||
     * @param \Federator\Data\FedUser[]|false $followers user followers
 | 
			
		||||
     * @return void
 | 
			
		||||
     */
 | 
			
		||||
    public function saveRemoteFollowersOfUser($user, $followers);
 | 
			
		||||
| 
						 | 
				
			
			@ -48,6 +48,15 @@ interface Cache extends \Federator\Connector\Connector
 | 
			
		|||
     */
 | 
			
		||||
    public function saveRemoteUserByName($_name, $user);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * save remote federation user by given name
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $_name user/profile name
 | 
			
		||||
     * @param \Federator\Data\FedUser $user user data
 | 
			
		||||
     * @return void
 | 
			
		||||
     */
 | 
			
		||||
    public function saveRemoteFedUserByName(string $_name, \Federator\Data\FedUser $user);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * save remote user by given session
 | 
			
		||||
     *
 | 
			
		||||
| 
						 | 
				
			
			@ -67,6 +76,14 @@ interface Cache extends \Federator\Connector\Connector
 | 
			
		|||
     */
 | 
			
		||||
    public function savePublicKey(string $keyId, string $publicKeyPem);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * get remote federation user by given name
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $_name user/profile name
 | 
			
		||||
     * @return \Federator\Data\FedUser | false
 | 
			
		||||
     */
 | 
			
		||||
    public function getRemoteFedUserByName(string $_name);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Retrieve the public key for a given keyId
 | 
			
		||||
     *
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,7 +18,7 @@ interface Connector
 | 
			
		|||
     *
 | 
			
		||||
     * @param string $id user id
 | 
			
		||||
 | 
			
		||||
     * @return \Federator\Data\ActivityPub\Common\APObject[]|false
 | 
			
		||||
     * @return \Federator\Data\FedUser[]|false
 | 
			
		||||
     */
 | 
			
		||||
    public function getRemoteFollowersOfUser($id);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -25,8 +25,6 @@ class Announce extends Activity
 | 
			
		|||
    {
 | 
			
		||||
        $return = parent::toObject();
 | 
			
		||||
        $return['type'] = 'Announce';
 | 
			
		||||
        // overwrite id from url
 | 
			
		||||
        $return['id'] = $this->getURL();
 | 
			
		||||
        return $return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -25,8 +25,6 @@ class Undo extends Activity
 | 
			
		|||
    {
 | 
			
		||||
        $return = parent::toObject();
 | 
			
		||||
        $return['type'] = 'Undo';
 | 
			
		||||
        // overwrite id from url
 | 
			
		||||
        $return['id'] = $this->getURL();
 | 
			
		||||
        return $return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										18
									
								
								php/federator/data/activitypub/common/accept.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								php/federator/data/activitypub/common/accept.php
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,18 @@
 | 
			
		|||
<?php
 | 
			
		||||
/**
 | 
			
		||||
 * SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
 | 
			
		||||
 * SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
 *
 | 
			
		||||
 * @author Yannis Vogel (vogeldevelopment)
 | 
			
		||||
 **/
 | 
			
		||||
 | 
			
		||||
namespace Federator\Data\ActivityPub\Common;
 | 
			
		||||
 | 
			
		||||
class Accept extends Activity
 | 
			
		||||
{
 | 
			
		||||
    public function __construct()
 | 
			
		||||
    {
 | 
			
		||||
      parent::__construct('Accept');
 | 
			
		||||
      parent::addContext('https://www.w3.org/ns/activitystreams');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -74,6 +74,7 @@ class Activity extends APObject
 | 
			
		|||
    {
 | 
			
		||||
        if (array_key_exists('actor', $json)) {
 | 
			
		||||
            $this->actor = $json['actor'];
 | 
			
		||||
            $this->aactor = $json['actor'];
 | 
			
		||||
            unset($json['actor']);
 | 
			
		||||
        }
 | 
			
		||||
        if (!parent::fromJson($json)) {
 | 
			
		||||
| 
						 | 
				
			
			@ -102,7 +103,7 @@ class Activity extends APObject
 | 
			
		|||
    /**
 | 
			
		||||
     * get Child Object
 | 
			
		||||
     *
 | 
			
		||||
     * @return APObject|null
 | 
			
		||||
     * @return APObject|string|null
 | 
			
		||||
     */
 | 
			
		||||
    public function getObject()
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -27,7 +27,7 @@ class APObject implements \JsonSerializable
 | 
			
		|||
    /**
 | 
			
		||||
     * child object
 | 
			
		||||
     *
 | 
			
		||||
     * @var APObject|null $object
 | 
			
		||||
     * @var APObject|string|null $object
 | 
			
		||||
     */
 | 
			
		||||
    private $object = null;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -358,7 +358,7 @@ class APObject implements \JsonSerializable
 | 
			
		|||
    /**
 | 
			
		||||
     * get child object
 | 
			
		||||
     *
 | 
			
		||||
     * @return APObject|null child object
 | 
			
		||||
     * @return APObject|string|null child object
 | 
			
		||||
     */
 | 
			
		||||
    public function getObject()
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			@ -750,8 +750,8 @@ class APObject implements \JsonSerializable
 | 
			
		|||
        if (array_key_exists('mediaType', $json)) {
 | 
			
		||||
            $this->mediaType = $json['mediaType'];
 | 
			
		||||
        }
 | 
			
		||||
        if (array_key_exists('object', $json)) {
 | 
			
		||||
            $this->object = \Federator\Data\ActivityPub\Factory::newFromJson($json['object'], "");
 | 
			
		||||
        if (array_key_exists('object', $json)) { // some actPub servers send strings in the object field
 | 
			
		||||
            $this->object = is_array($json['object']) ? \Federator\Data\ActivityPub\Factory::newFromJson($json['object'], "") : $json['object'];
 | 
			
		||||
        }
 | 
			
		||||
        if (array_key_exists('sensitive', $json)) {
 | 
			
		||||
            $this->sensitive = $json['sensitive'];
 | 
			
		||||
| 
						 | 
				
			
			@ -890,7 +890,7 @@ class APObject implements \JsonSerializable
 | 
			
		|||
            $return['mediaType'] = $this->mediaType;
 | 
			
		||||
        }
 | 
			
		||||
        if ($this->object !== null) {
 | 
			
		||||
            $return['object'] = $this->object->toObject();
 | 
			
		||||
            $return['object'] = is_string($this->object) ? $this->object : $this->object->toObject();
 | 
			
		||||
        }
 | 
			
		||||
        if ($this->atomURI !== '') {
 | 
			
		||||
            $return['atomUri'] = $this->atomURI;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										54
									
								
								php/federator/data/activitypub/common/delete.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								php/federator/data/activitypub/common/delete.php
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,54 @@
 | 
			
		|||
<?php
 | 
			
		||||
/**
 | 
			
		||||
 * SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
 | 
			
		||||
 * SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
 *
 | 
			
		||||
 * @author Yannis Vogel (vogeldevelopment)
 | 
			
		||||
 **/
 | 
			
		||||
 | 
			
		||||
namespace Federator\Data\ActivityPub\Common;
 | 
			
		||||
 | 
			
		||||
class Delete extends Activity
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * object overwrite
 | 
			
		||||
     * @var string
 | 
			
		||||
     */
 | 
			
		||||
    private $object = "";
 | 
			
		||||
  
 | 
			
		||||
    public function setFObject(string $object): void
 | 
			
		||||
    {
 | 
			
		||||
      $this->object = $object;
 | 
			
		||||
    }
 | 
			
		||||
  
 | 
			
		||||
    public function __construct()
 | 
			
		||||
    {
 | 
			
		||||
      parent::__construct('Delete');
 | 
			
		||||
      parent::addContext('https://www.w3.org/ns/activitystreams');
 | 
			
		||||
    }
 | 
			
		||||
  
 | 
			
		||||
    /**
 | 
			
		||||
     * create from json/array
 | 
			
		||||
     * @param mixed $json
 | 
			
		||||
     */
 | 
			
		||||
    public function fromJson($json): bool
 | 
			
		||||
    {
 | 
			
		||||
      if (array_key_exists('object', $json)) {
 | 
			
		||||
        $this->object = $json['object'];
 | 
			
		||||
        unset($json['object']);
 | 
			
		||||
      }
 | 
			
		||||
      return parent::fromJson($json);
 | 
			
		||||
    }
 | 
			
		||||
    /**
 | 
			
		||||
     * convert internal state to php array
 | 
			
		||||
     * @return array<string,mixed>
 | 
			
		||||
     */
 | 
			
		||||
    public function toObject()
 | 
			
		||||
    {
 | 
			
		||||
      $return = parent::toObject();
 | 
			
		||||
      if ($this->object !== "") {
 | 
			
		||||
        $return['object'] = $this->object;
 | 
			
		||||
      }
 | 
			
		||||
      return $return;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										50
									
								
								php/federator/data/activitypub/common/follow.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								php/federator/data/activitypub/common/follow.php
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,50 @@
 | 
			
		|||
<?php
 | 
			
		||||
/**
 | 
			
		||||
 * SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
 | 
			
		||||
 * SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
 *
 | 
			
		||||
 * @author Yannis Vogel (vogeldevelopment)
 | 
			
		||||
 **/
 | 
			
		||||
 | 
			
		||||
namespace Federator\Data\ActivityPub\Common;
 | 
			
		||||
 | 
			
		||||
class Follow extends Activity
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * object overwrite
 | 
			
		||||
     * @var string
 | 
			
		||||
     */
 | 
			
		||||
    private $object = "";
 | 
			
		||||
 | 
			
		||||
    public function setFObject(string $object): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->object = $object;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function __construct()
 | 
			
		||||
    {
 | 
			
		||||
        parent::__construct("Follow");
 | 
			
		||||
        parent::addContext('https://www.w3.org/ns/activitystreams');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function fromJson($json): bool
 | 
			
		||||
    {
 | 
			
		||||
        if (array_key_exists('object', $json)) {
 | 
			
		||||
            $this->object = $json['object'];
 | 
			
		||||
            unset($json['object']);
 | 
			
		||||
        }
 | 
			
		||||
        return parent::fromJson($json);
 | 
			
		||||
    }
 | 
			
		||||
    /**
 | 
			
		||||
     * convert internal state to php array
 | 
			
		||||
     * @return array<string,mixed>
 | 
			
		||||
     */
 | 
			
		||||
    public function toObject()
 | 
			
		||||
    {
 | 
			
		||||
        $return = parent::toObject();
 | 
			
		||||
        if ($this->object !== "") {
 | 
			
		||||
            $return['object'] = $this->object;
 | 
			
		||||
        }
 | 
			
		||||
        return $return;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										18
									
								
								php/federator/data/activitypub/common/reject.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								php/federator/data/activitypub/common/reject.php
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,18 @@
 | 
			
		|||
<?php
 | 
			
		||||
/**
 | 
			
		||||
 * SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
 | 
			
		||||
 * SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
 *
 | 
			
		||||
 * @author Yannis Vogel (vogeldevelopment)
 | 
			
		||||
 **/
 | 
			
		||||
 | 
			
		||||
namespace Federator\Data\ActivityPub\Common;
 | 
			
		||||
 | 
			
		||||
class Reject extends Activity
 | 
			
		||||
{
 | 
			
		||||
    public function __construct()
 | 
			
		||||
    {
 | 
			
		||||
      parent::__construct('Reject');
 | 
			
		||||
      parent::addContext('https://www.w3.org/ns/activitystreams');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -31,6 +31,9 @@ class Factory
 | 
			
		|||
        }
 | 
			
		||||
        $return = null;
 | 
			
		||||
        switch ($json['type']) {
 | 
			
		||||
            case 'Announce':
 | 
			
		||||
                $return = new Common\Announce();
 | 
			
		||||
                break;
 | 
			
		||||
            case 'Article':
 | 
			
		||||
                $return = new Common\Article();
 | 
			
		||||
                break;
 | 
			
		||||
| 
						 | 
				
			
			@ -39,10 +42,10 @@ class Factory
 | 
			
		|||
                break;
 | 
			
		||||
            case 'Event':
 | 
			
		||||
                $return = new Common\Event();
 | 
			
		||||
                break;
 | 
			
		||||
                break;*/
 | 
			
		||||
            case 'Follow':
 | 
			
		||||
                $return = new Common\Follow();
 | 
			
		||||
                break;*/
 | 
			
		||||
                break;
 | 
			
		||||
            case 'Image':
 | 
			
		||||
                $return = new Common\Image();
 | 
			
		||||
                break;
 | 
			
		||||
| 
						 | 
				
			
			@ -83,21 +86,24 @@ class Factory
 | 
			
		|||
        }
 | 
			
		||||
        //$return = false;
 | 
			
		||||
        switch ($json['type']) {
 | 
			
		||||
            /* case 'Accept':
 | 
			
		||||
            case 'Accept':
 | 
			
		||||
                $return = new Common\Accept();
 | 
			
		||||
                break; */
 | 
			
		||||
                break;
 | 
			
		||||
            case 'Announce':
 | 
			
		||||
                $return = new Common\Announce();
 | 
			
		||||
                break;
 | 
			
		||||
            case 'Create':
 | 
			
		||||
                $return = new Common\Create();
 | 
			
		||||
                break;
 | 
			
		||||
            /*case 'Delete':
 | 
			
		||||
            case 'Delete':
 | 
			
		||||
                $return = new Common\Delete();
 | 
			
		||||
                break;
 | 
			
		||||
            case 'Follow':
 | 
			
		||||
                $return = new Common\Follow();
 | 
			
		||||
                break;*/
 | 
			
		||||
                break;
 | 
			
		||||
            case 'Reject':
 | 
			
		||||
                $return = new Common\Reject();
 | 
			
		||||
                break;
 | 
			
		||||
            case 'Undo':
 | 
			
		||||
                $return = new Common\Undo();
 | 
			
		||||
                break;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										151
									
								
								php/federator/data/feduser.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								php/federator/data/feduser.php
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,151 @@
 | 
			
		|||
<?php
 | 
			
		||||
/**
 | 
			
		||||
 * SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
 | 
			
		||||
 * SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
 *
 | 
			
		||||
 * @author Yannis Vogel (vogeldevelopment)
 | 
			
		||||
 **/
 | 
			
		||||
 | 
			
		||||
namespace Federator\Data;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * storage class for user attributes
 | 
			
		||||
 */
 | 
			
		||||
class FedUser
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * user id
 | 
			
		||||
     *
 | 
			
		||||
     * @var string $id
 | 
			
		||||
     */
 | 
			
		||||
    public $id;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * user url
 | 
			
		||||
     *
 | 
			
		||||
     * @var string $actorURL
 | 
			
		||||
     */
 | 
			
		||||
    public $actorURL;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * user name
 | 
			
		||||
     *
 | 
			
		||||
     * @var string $name
 | 
			
		||||
     */
 | 
			
		||||
    public $name = '';
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * user public key
 | 
			
		||||
     *
 | 
			
		||||
     * @var string $publicKey
 | 
			
		||||
     */
 | 
			
		||||
    public $publicKey;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * summary for user/profile
 | 
			
		||||
     *
 | 
			
		||||
     * @var string $summary
 | 
			
		||||
     */
 | 
			
		||||
    public $summary = '';
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * type of user (person/group)
 | 
			
		||||
     *
 | 
			
		||||
     * @var string $type
 | 
			
		||||
     */
 | 
			
		||||
    public $type = 'Person';
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * inbox URL
 | 
			
		||||
     *
 | 
			
		||||
     * @var string $inboxURL
 | 
			
		||||
     */
 | 
			
		||||
    public $inboxURL;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * shared inbox URL
 | 
			
		||||
     *
 | 
			
		||||
     * @var string $sharedInboxURL
 | 
			
		||||
     */
 | 
			
		||||
    public $sharedInboxURL;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * followers URL
 | 
			
		||||
     *
 | 
			
		||||
     * @var string $followersURL
 | 
			
		||||
     */
 | 
			
		||||
    public $followersURL;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * following URL
 | 
			
		||||
     *
 | 
			
		||||
     * @var string $followingURL
 | 
			
		||||
     */
 | 
			
		||||
    public $followingURL;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * public key ID
 | 
			
		||||
     *
 | 
			
		||||
     * @var string $publicKeyId
 | 
			
		||||
     */
 | 
			
		||||
    public $publicKeyId;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * outbox URL
 | 
			
		||||
     *
 | 
			
		||||
     * @var string $outboxURL
 | 
			
		||||
     */
 | 
			
		||||
    public $outboxURL;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * create new user object from json string
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $input input string
 | 
			
		||||
     * @return FedUser|false
 | 
			
		||||
     */
 | 
			
		||||
    public static function createFromJson($input)
 | 
			
		||||
    {
 | 
			
		||||
        $data = json_decode($input, true);
 | 
			
		||||
        if ($data === null) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        $user = new FedUser();
 | 
			
		||||
        $user->id = $data['id'] ?? '';
 | 
			
		||||
        $user->actorURL = $data['actorURL'] ?? '';
 | 
			
		||||
        $user->name = $data['name'] ?? '';
 | 
			
		||||
        $user->publicKey = $data['publicKey'] ?? '';
 | 
			
		||||
        $user->summary = $data['summary'] ?? '';
 | 
			
		||||
        $user->type = $data['type'] ?? 'Person';
 | 
			
		||||
        $user->inboxURL = $data['inbox'] ?? '';
 | 
			
		||||
        $user->sharedInboxURL = $data['sharedInbox'] ?? '';
 | 
			
		||||
        $user->followersURL = $data['followers'] ?? '';
 | 
			
		||||
        $user->followingURL = $data['following'] ?? '';
 | 
			
		||||
        $user->publicKeyId = $data['publicKeyId'] ?? '';
 | 
			
		||||
        $user->outboxURL = $data['outbox'] ?? '';
 | 
			
		||||
        return $user;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * convert internal data to json string
 | 
			
		||||
     *
 | 
			
		||||
     * @return string
 | 
			
		||||
     */
 | 
			
		||||
    public function toJson()
 | 
			
		||||
    {
 | 
			
		||||
        $data = [
 | 
			
		||||
            'id' => $this->id,
 | 
			
		||||
            'actorURL' => $this->actorURL,
 | 
			
		||||
            'name' => $this->name,
 | 
			
		||||
            'publicKey' => $this->publicKey,
 | 
			
		||||
            'summary' => $this->summary,
 | 
			
		||||
            'type' => $this->type,
 | 
			
		||||
            'inbox' => $this->inboxURL,
 | 
			
		||||
            'sharedInbox' => $this->sharedInboxURL,
 | 
			
		||||
            'followers' => $this->followersURL,
 | 
			
		||||
            'following' => $this->followingURL,
 | 
			
		||||
            'publicKeyId' => $this->publicKeyId,
 | 
			
		||||
            'outbox' => $this->outboxURL,
 | 
			
		||||
        ];
 | 
			
		||||
        return json_encode($data) ?: '';
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										242
									
								
								php/federator/dio/feduser.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										242
									
								
								php/federator/dio/feduser.php
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,242 @@
 | 
			
		|||
<?php
 | 
			
		||||
/**
 | 
			
		||||
 * SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
 | 
			
		||||
 * SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
 *
 | 
			
		||||
 * @author Yannis Vogel (vogeldevelopment)
 | 
			
		||||
 **/
 | 
			
		||||
 | 
			
		||||
namespace Federator\DIO;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * IO functions related to fedUsers
 | 
			
		||||
 */
 | 
			
		||||
class FedUser
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * add local user based on given user object received from remote service
 | 
			
		||||
     * @param \mysqli $dbh database handle
 | 
			
		||||
     * @param \Federator\Data\FedUser $user user object to use
 | 
			
		||||
     * @param string $_user user/profile name
 | 
			
		||||
     * @return void
 | 
			
		||||
     */
 | 
			
		||||
    protected static function addLocalUser($dbh, $user, $_user)
 | 
			
		||||
    {
 | 
			
		||||
        // check if it is timed out user
 | 
			
		||||
        $sql = 'select unix_timestamp(`validuntil`) from fedusers where id=?';
 | 
			
		||||
        $stmt = $dbh->prepare($sql);
 | 
			
		||||
        if ($stmt === false) {
 | 
			
		||||
            throw new \Federator\Exceptions\ServerError();
 | 
			
		||||
        }
 | 
			
		||||
        $stmt->bind_param("s", $_user);
 | 
			
		||||
        $validuntil = 0;
 | 
			
		||||
        $ret = $stmt->bind_result($validuntil);
 | 
			
		||||
        $stmt->execute();
 | 
			
		||||
        if ($ret) {
 | 
			
		||||
            $stmt->fetch();
 | 
			
		||||
        }
 | 
			
		||||
        $stmt->close();
 | 
			
		||||
        if ($validuntil == 0) {
 | 
			
		||||
            $sql = 'insert into fedusers (id, url, name, publickey, summary, type, inboxurl, sharedinboxurl,';
 | 
			
		||||
            $sql .= ' followersurl, followingurl, publickeyid, outboxurl, validuntil)';
 | 
			
		||||
            $sql .= ' values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, now() + interval 1 day)';
 | 
			
		||||
            $stmt = $dbh->prepare($sql);
 | 
			
		||||
            if ($stmt === false) {
 | 
			
		||||
                throw new \Federator\Exceptions\ServerError();
 | 
			
		||||
            }
 | 
			
		||||
            $stmt->bind_param(
 | 
			
		||||
                "ssssssssssss",
 | 
			
		||||
                $_user,
 | 
			
		||||
                $user->actorURL,
 | 
			
		||||
                $user->name,
 | 
			
		||||
                $user->publicKey,
 | 
			
		||||
                $user->summary,
 | 
			
		||||
                $user->type,
 | 
			
		||||
                $user->inboxURL,
 | 
			
		||||
                $user->sharedInboxURL,
 | 
			
		||||
                $user->followersURL,
 | 
			
		||||
                $user->followingURL,
 | 
			
		||||
                $user->publicKeyId,
 | 
			
		||||
                $user->outboxURL
 | 
			
		||||
            );
 | 
			
		||||
        } else {
 | 
			
		||||
            // update to existing user
 | 
			
		||||
            $sql = 'update fedusers set validuntil=now() + interval 1 day, url=?, name=?, publickey=?, summary=?,';
 | 
			
		||||
            $sql .= ' type=?, inboxurl=?, sharedinboxurl=?, followersurl=?, followingurl=?, publickeyid=?, outboxurl=?';
 | 
			
		||||
            $sql .= ' where id=?';
 | 
			
		||||
            $stmt = $dbh->prepare($sql);
 | 
			
		||||
            if ($stmt === false) {
 | 
			
		||||
                throw new \Federator\Exceptions\ServerError();
 | 
			
		||||
            }
 | 
			
		||||
            $stmt->bind_param(
 | 
			
		||||
                "ssssssssssss",
 | 
			
		||||
                $user->actorURL,
 | 
			
		||||
                $user->name,
 | 
			
		||||
                $user->publicKey,
 | 
			
		||||
                $user->summary,
 | 
			
		||||
                $user->type,
 | 
			
		||||
                $user->inboxURL,
 | 
			
		||||
                $user->sharedInboxURL,
 | 
			
		||||
                $user->followersURL,
 | 
			
		||||
                $user->followingURL,
 | 
			
		||||
                $user->publicKeyId,
 | 
			
		||||
                $user->outboxURL,
 | 
			
		||||
                $_user
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
        try {
 | 
			
		||||
            $stmt->execute();
 | 
			
		||||
            $stmt->close();
 | 
			
		||||
            $user->id = $_user;
 | 
			
		||||
        } catch (\mysqli_sql_exception $e) {
 | 
			
		||||
            error_log($sql);
 | 
			
		||||
            error_log(print_r($user, true));
 | 
			
		||||
            error_log($e->getMessage());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * extend the given user with internal data
 | 
			
		||||
     * @param \mysqli $dbh database  handle
 | 
			
		||||
     * @param \Federator\Data\FedUser $user user to extend
 | 
			
		||||
     * @param string $_user user/profile name
 | 
			
		||||
     */
 | 
			
		||||
    protected static function extendUser(\mysqli $dbh, \Federator\Data\FedUser $user, $_user): void
 | 
			
		||||
    {
 | 
			
		||||
        $sql = 'select id,unix_timestamp(`validuntil`) from fedusers where id=?';
 | 
			
		||||
        $stmt = $dbh->prepare($sql);
 | 
			
		||||
        if ($stmt === false) {
 | 
			
		||||
            throw new \Federator\Exceptions\ServerError();
 | 
			
		||||
        }
 | 
			
		||||
        $stmt->bind_param("s", $_user);
 | 
			
		||||
        $validuntil = 0;
 | 
			
		||||
        $ret = $stmt->bind_result($user->id, $validuntil);
 | 
			
		||||
        $stmt->execute();
 | 
			
		||||
        if ($ret) {
 | 
			
		||||
            $stmt->fetch();
 | 
			
		||||
        }
 | 
			
		||||
        $stmt->close();
 | 
			
		||||
        // if a new user, create own database entry with additionally needed info
 | 
			
		||||
        if ($user->id === null || $validuntil < time()) {
 | 
			
		||||
            self::addLocalUser($dbh, $user, $_user);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // no further processing for now
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * get user by name
 | 
			
		||||
     *
 | 
			
		||||
     * @param \mysqli $dbh
 | 
			
		||||
     *          database handle
 | 
			
		||||
     * @param string $_name
 | 
			
		||||
     *          user name
 | 
			
		||||
     * @param \Federator\Cache\Cache|null $cache
 | 
			
		||||
     *          optional caching service
 | 
			
		||||
     * @return \Federator\Data\FedUser
 | 
			
		||||
     */
 | 
			
		||||
    public static function getUserByName($dbh, $_name, $cache)
 | 
			
		||||
    {
 | 
			
		||||
        $user = false;
 | 
			
		||||
 | 
			
		||||
        // ask cache
 | 
			
		||||
        if ($cache !== null) {
 | 
			
		||||
            $user = $cache->getRemoteFedUserByName($_name);
 | 
			
		||||
        }
 | 
			
		||||
        if ($user !== false) {
 | 
			
		||||
            return $user;
 | 
			
		||||
        }
 | 
			
		||||
        // check our db
 | 
			
		||||
        $sql = 'select id, url, name, publickey, summary, type, inboxurl, sharedinboxurl, followersurl,';
 | 
			
		||||
        $sql .= ' followingurl,publickeyid,outboxurl';
 | 
			
		||||
        $sql .= ' from fedusers where id=? and validuntil>=now()';
 | 
			
		||||
        $stmt = $dbh->prepare($sql);
 | 
			
		||||
        if ($stmt === false) {
 | 
			
		||||
            throw new \Federator\Exceptions\ServerError();
 | 
			
		||||
        }
 | 
			
		||||
        $stmt->bind_param("s", $_name);
 | 
			
		||||
        $user = new \Federator\Data\FedUser();
 | 
			
		||||
        $ret = $stmt->bind_result(
 | 
			
		||||
            $user->id,
 | 
			
		||||
            $user->actorURL,
 | 
			
		||||
            $user->name,
 | 
			
		||||
            $user->publicKey,
 | 
			
		||||
            $user->summary,
 | 
			
		||||
            $user->type,
 | 
			
		||||
            $user->inboxURL,
 | 
			
		||||
            $user->sharedInboxURL,
 | 
			
		||||
            $user->followersURL,
 | 
			
		||||
            $user->followingURL,
 | 
			
		||||
            $user->publicKeyId,
 | 
			
		||||
            $user->outboxURL
 | 
			
		||||
        );
 | 
			
		||||
        $stmt->execute();
 | 
			
		||||
        if ($ret) {
 | 
			
		||||
            $stmt->fetch();
 | 
			
		||||
        }
 | 
			
		||||
        $stmt->close();
 | 
			
		||||
 | 
			
		||||
        if ($user->id === null) {
 | 
			
		||||
            // check if its a federated user with username@domain.ending
 | 
			
		||||
            if (preg_match("/^([^@]+)@(.*)$/", $_name, $matches) == 1) {
 | 
			
		||||
                // make webfinger request
 | 
			
		||||
                $remoteURL = 'https://' . $matches[2] . '/.well-known/webfinger?resource=acct:' . urlencode($_name);
 | 
			
		||||
                $headers = ['Accept: application/activity+json'];
 | 
			
		||||
                [$response, $info] = \Federator\Main::getFromRemote($remoteURL, $headers);
 | 
			
		||||
                if ($info['http_code'] != 200) {
 | 
			
		||||
                    throw new \Federator\Exceptions\ServerError();
 | 
			
		||||
                }
 | 
			
		||||
                $r = json_decode($response, true);
 | 
			
		||||
                if ($r === false || $r === null || !is_array($r)) {
 | 
			
		||||
                    throw new \Federator\Exceptions\ServerError();
 | 
			
		||||
                }
 | 
			
		||||
                // get the webwinger user url and fetch the user
 | 
			
		||||
                if (isset($r['links'])) {
 | 
			
		||||
                    foreach ($r['links'] as $link) {
 | 
			
		||||
                        if (isset($link['rel']) && $link['rel'] === 'self') {
 | 
			
		||||
                            $remoteURL = $link['href'];
 | 
			
		||||
                            break;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                if (!isset($remoteURL)) {
 | 
			
		||||
                    throw new \Federator\Exceptions\ServerError();
 | 
			
		||||
                }
 | 
			
		||||
                // fetch the user
 | 
			
		||||
                $headers = ['Accept: application/activity+json'];
 | 
			
		||||
                [$response, $info] = \Federator\Main::getFromRemote($remoteURL, $headers);
 | 
			
		||||
                if ($info['http_code'] != 200) {
 | 
			
		||||
                    throw new \Federator\Exceptions\ServerError();
 | 
			
		||||
                }
 | 
			
		||||
                $r = json_decode($response, true);
 | 
			
		||||
                if ($r === false || $r === null || !is_array($r)) {
 | 
			
		||||
                    throw new \Federator\Exceptions\ServerError();
 | 
			
		||||
                }
 | 
			
		||||
                $r['publicKeyId'] = $r['publicKey']['id'];
 | 
			
		||||
                $r['publicKey'] = $r['publicKey']['publicKeyPem'];
 | 
			
		||||
                if (isset($r['endpoints'])) {
 | 
			
		||||
                    if (isset($r['endpoints']['sharedInbox'])) {
 | 
			
		||||
                        $r['sharedInbox'] = $r['endpoints']['sharedInbox'];
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                $r['actorURL'] = $remoteURL;
 | 
			
		||||
                $data = json_encode($r);
 | 
			
		||||
                if ($data === false) {
 | 
			
		||||
                    throw new \Federator\Exceptions\ServerError();
 | 
			
		||||
                }
 | 
			
		||||
                $user = \Federator\Data\FedUser::createFromJson($data);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($cache !== null && $user !== false) {
 | 
			
		||||
            if ($user->id === null && $user->actorURL !== null) {
 | 
			
		||||
                self::addLocalUser($dbh, $user, $_name);
 | 
			
		||||
            }
 | 
			
		||||
            $cache->saveRemoteFedUserByName($_name, $user);
 | 
			
		||||
        }
 | 
			
		||||
        if ($user === false) {
 | 
			
		||||
            throw new \Federator\Exceptions\ServerError();
 | 
			
		||||
        }
 | 
			
		||||
        return $user;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -13,11 +13,10 @@ namespace Federator\DIO;
 | 
			
		|||
 */
 | 
			
		||||
class Followers
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * get followers of user
 | 
			
		||||
     *
 | 
			
		||||
     * @param \mysqli $dbh @unused-param
 | 
			
		||||
     * @param \mysqli $dbh
 | 
			
		||||
     *          database handle
 | 
			
		||||
     * @param string $id
 | 
			
		||||
     *          user id
 | 
			
		||||
| 
						 | 
				
			
			@ -25,7 +24,7 @@ class Followers
 | 
			
		|||
     *          connector to fetch use with
 | 
			
		||||
     * @param \Federator\Cache\Cache|null $cache
 | 
			
		||||
     *          optional caching service
 | 
			
		||||
     * @return \Federator\Data\ActivityPub\Common\APObject[]
 | 
			
		||||
     * @return \Federator\Data\FedUser[]
 | 
			
		||||
     */
 | 
			
		||||
    public static function getFollowersByUser($dbh, $id, $connector, $cache)
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			@ -37,7 +36,29 @@ class Followers
 | 
			
		|||
            }
 | 
			
		||||
        }
 | 
			
		||||
        $followers = [];
 | 
			
		||||
        // TODO: check our db
 | 
			
		||||
        $sql = 'select source_user from follows where target_user = ?';
 | 
			
		||||
        $stmt = $dbh->prepare($sql);
 | 
			
		||||
        if ($stmt === false) {
 | 
			
		||||
            throw new \Federator\Exceptions\ServerError();
 | 
			
		||||
        }
 | 
			
		||||
        $stmt->bind_param("s", $id);
 | 
			
		||||
        $stmt->execute();
 | 
			
		||||
        $followerIds = [];
 | 
			
		||||
        $stmt->bind_result($sourceUser);
 | 
			
		||||
        while ($stmt->fetch()) {
 | 
			
		||||
            $followerIds[] = $sourceUser;
 | 
			
		||||
        }
 | 
			
		||||
        $stmt->close();
 | 
			
		||||
        foreach ($followerIds as $followerId) {
 | 
			
		||||
            $user = \Federator\DIO\FedUser::getUserByName(
 | 
			
		||||
                $dbh,
 | 
			
		||||
                $followerId,
 | 
			
		||||
                $cache,
 | 
			
		||||
            );
 | 
			
		||||
            if ($user !== false && $user->id !== null) {
 | 
			
		||||
                $followers[] = $user;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($followers === []) {
 | 
			
		||||
            // ask connector for user-id
 | 
			
		||||
| 
						 | 
				
			
			@ -52,4 +73,263 @@ class Followers
 | 
			
		|||
        }
 | 
			
		||||
        return $followers;
 | 
			
		||||
    }
 | 
			
		||||
    /**
 | 
			
		||||
     * get followers of user
 | 
			
		||||
     *
 | 
			
		||||
     * @param \mysqli $dbh
 | 
			
		||||
     *          database handle
 | 
			
		||||
     * @param \Federator\Connector\Connector $connector
 | 
			
		||||
     *          connector to fetch use with
 | 
			
		||||
     * @param \Federator\Cache\Cache|null $cache
 | 
			
		||||
     *          optional caching service
 | 
			
		||||
     * @param string $id
 | 
			
		||||
     *          user id
 | 
			
		||||
     * @return \Federator\Data\User[]
 | 
			
		||||
     */
 | 
			
		||||
 | 
			
		||||
    public static function getFollowersByFedUser($dbh, $connector, $cache, $id)
 | 
			
		||||
    {
 | 
			
		||||
        $followers = [];
 | 
			
		||||
 | 
			
		||||
        $sql = 'select source_user from follows where target_user = ?';
 | 
			
		||||
        $stmt = $dbh->prepare($sql);
 | 
			
		||||
        if ($stmt === false) {
 | 
			
		||||
            throw new \Federator\Exceptions\ServerError();
 | 
			
		||||
        }
 | 
			
		||||
        $stmt->bind_param("s", $id);
 | 
			
		||||
        $stmt->execute();
 | 
			
		||||
        $followerIds = [];
 | 
			
		||||
        $stmt->bind_result($sourceUser);
 | 
			
		||||
        while ($stmt->fetch()) {
 | 
			
		||||
            $followerIds[] = $sourceUser;
 | 
			
		||||
        }
 | 
			
		||||
        foreach ($followerIds as $followerId) {
 | 
			
		||||
            $user = \Federator\DIO\User::getUserByName(
 | 
			
		||||
                $dbh,
 | 
			
		||||
                $followerId,
 | 
			
		||||
                $connector,
 | 
			
		||||
                $cache
 | 
			
		||||
            );
 | 
			
		||||
            if ($user !== false && $user->id !== null) {
 | 
			
		||||
                $followers[] = $user;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return $followers;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * send follow request
 | 
			
		||||
     *
 | 
			
		||||
     * @param \mysqli $dbh database handle
 | 
			
		||||
     * @param \Federator\Connector\Connector $connector connector to use
 | 
			
		||||
     * @param \Federator\Cache\Cache|null $cache optional caching service
 | 
			
		||||
     * @param string $_user source user
 | 
			
		||||
     * @param string $_targetUser target user id
 | 
			
		||||
     * @param string $host the host for generating the follow ID
 | 
			
		||||
     * @return string|false the generated follow ID on success, false on failure
 | 
			
		||||
     */
 | 
			
		||||
    public static function sendFollowRequest($dbh, $connector, $cache, $_user, $_targetUser, $host)
 | 
			
		||||
    {
 | 
			
		||||
        if ($dbh === false) {
 | 
			
		||||
            throw new \Federator\Exceptions\ServerError();
 | 
			
		||||
        }
 | 
			
		||||
        $user = \Federator\DIO\User::getUserByName(
 | 
			
		||||
            $dbh,
 | 
			
		||||
            $_user,
 | 
			
		||||
            $connector,
 | 
			
		||||
            $cache
 | 
			
		||||
        );
 | 
			
		||||
        if ($user === false || $user->id === null) {
 | 
			
		||||
            throw new \Federator\Exceptions\FileNotFound();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $fedUser = \Federator\DIO\FedUser::getUserByName(
 | 
			
		||||
            $dbh,
 | 
			
		||||
            $_targetUser,
 | 
			
		||||
            $cache
 | 
			
		||||
        );
 | 
			
		||||
        if ($fedUser === false || $fedUser->actorURL === null) {
 | 
			
		||||
            throw new \Federator\Exceptions\FileNotFound();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $sourceUser = $user->id;
 | 
			
		||||
        $idUrl = self::addFollow($dbh, $sourceUser, $fedUser->id, $host);
 | 
			
		||||
        if ($idUrl === false) {
 | 
			
		||||
            return false; // Failed to add follow
 | 
			
		||||
        }
 | 
			
		||||
        $followObj = new \Federator\Data\ActivityPub\Common\Follow();
 | 
			
		||||
        $sourceUserUrl = 'https://' . $host . '/' . $sourceUser;
 | 
			
		||||
        $followObj->setFObject($fedUser->actorURL);
 | 
			
		||||
        $followObj->setAActor($sourceUserUrl);
 | 
			
		||||
        $followObj->setID($idUrl);
 | 
			
		||||
 | 
			
		||||
        // Send the Follow activity
 | 
			
		||||
        $inboxUrl = $fedUser->inboxURL;
 | 
			
		||||
 | 
			
		||||
        $json = json_encode($followObj, JSON_UNESCAPED_SLASHES);
 | 
			
		||||
 | 
			
		||||
        if ($json === false) {
 | 
			
		||||
            self::removeFollow($dbh, $sourceUser, $fedUser->id);
 | 
			
		||||
            throw new \Exception('Failed to encode JSON: ' . json_last_error_msg());
 | 
			
		||||
        }
 | 
			
		||||
        $digest = 'SHA-256=' . base64_encode(hash('sha256', $json, true));
 | 
			
		||||
        $date = gmdate('D, d M Y H:i:s') . ' GMT';
 | 
			
		||||
        $parsed = parse_url($inboxUrl);
 | 
			
		||||
        if ($parsed === false) {
 | 
			
		||||
            self::removeFollow($dbh, $sourceUser, $fedUser->id);
 | 
			
		||||
            throw new \Exception('Failed to parse URL: ' . $inboxUrl);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!isset($parsed['host']) || !isset($parsed['path'])) {
 | 
			
		||||
            self::removeFollow($dbh, $sourceUser, $fedUser->id);
 | 
			
		||||
            throw new \Exception('Invalid inbox URL: missing host or path');
 | 
			
		||||
        }
 | 
			
		||||
        $extHost = $parsed['host'];
 | 
			
		||||
        $path = $parsed['path'];
 | 
			
		||||
 | 
			
		||||
        // Build the signature string
 | 
			
		||||
        $signatureString = "(request-target): post {$path}\n" .
 | 
			
		||||
            "host: {$extHost}\n" .
 | 
			
		||||
            "date: {$date}\n" .
 | 
			
		||||
            "digest: {$digest}";
 | 
			
		||||
 | 
			
		||||
        // Get rsa private key
 | 
			
		||||
        $privateKey = \Federator\DIO\User::getrsaprivate($dbh, $user->id); // OR from DB
 | 
			
		||||
        if ($privateKey === false) {
 | 
			
		||||
            self::removeFollow($dbh, $sourceUser, $fedUser->id);
 | 
			
		||||
            throw new \Exception('Failed to get private key');
 | 
			
		||||
        }
 | 
			
		||||
        $pkeyId = openssl_pkey_get_private($privateKey);
 | 
			
		||||
 | 
			
		||||
        if ($pkeyId === false) {
 | 
			
		||||
            self::removeFollow($dbh, $sourceUser, $fedUser->id);
 | 
			
		||||
            throw new \Exception('Invalid private key');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        openssl_sign($signatureString, $signature, $pkeyId, OPENSSL_ALGO_SHA256);
 | 
			
		||||
        $signature_b64 = base64_encode($signature);
 | 
			
		||||
 | 
			
		||||
        // Build keyId (public key ID from your actor object)
 | 
			
		||||
        $keyId = 'https://' . $host . '/' . $user->id . '#main-key';
 | 
			
		||||
 | 
			
		||||
        $signatureHeader = 'keyId="' . $keyId . '",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="' . $signature_b64 . '"';
 | 
			
		||||
 | 
			
		||||
        $ch = curl_init($inboxUrl);
 | 
			
		||||
        if ($ch === false) {
 | 
			
		||||
            self::removeFollow($dbh, $sourceUser, $fedUser->id);
 | 
			
		||||
            throw new \Exception('Failed to initialize cURL');
 | 
			
		||||
        }
 | 
			
		||||
        $headers = [
 | 
			
		||||
            'Host: ' . $extHost,
 | 
			
		||||
            'Date: ' . $date,
 | 
			
		||||
            'Digest: ' . $digest,
 | 
			
		||||
            'Content-Type: application/activity+json',
 | 
			
		||||
            'Signature: ' . $signatureHeader,
 | 
			
		||||
            'Accept: application/activity+json',
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
 | 
			
		||||
        curl_setopt($ch, CURLOPT_POST, true);
 | 
			
		||||
        curl_setopt($ch, CURLOPT_POSTFIELDS, $json);
 | 
			
		||||
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
 | 
			
		||||
        $response = curl_exec($ch);
 | 
			
		||||
        curl_close($ch);
 | 
			
		||||
 | 
			
		||||
        // Log the response for debugging if needed
 | 
			
		||||
        if ($response === false) {
 | 
			
		||||
            self::removeFollow($dbh, $sourceUser, $fedUser->id);
 | 
			
		||||
            throw new \Exception("Failed to send Follow activity: " . curl_error($ch));
 | 
			
		||||
        } else {
 | 
			
		||||
            $httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
 | 
			
		||||
            if ($httpcode != 200 && $httpcode != 202) {
 | 
			
		||||
                self::removeFollow($dbh, $sourceUser, $fedUser->id);
 | 
			
		||||
                throw new \Exception("Unexpected HTTP code $httpcode: $response");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return $idUrl;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * add follow
 | 
			
		||||
     *
 | 
			
		||||
     * @param \mysqli $dbh database handle
 | 
			
		||||
     * @param string $sourceUser source user id
 | 
			
		||||
     * @param string $targetUserId target user id
 | 
			
		||||
     * @param string $host the host for generating the follow ID
 | 
			
		||||
     * @return string|false the generated follow ID on success, false on failure
 | 
			
		||||
     */
 | 
			
		||||
    public static function addFollow($dbh, $sourceUser, $targetUserId, $host)
 | 
			
		||||
    {
 | 
			
		||||
        // Check if we already follow this user
 | 
			
		||||
        $sql = 'select id from follows where source_user = ? and target_user = ?';
 | 
			
		||||
        $stmt = $dbh->prepare($sql);
 | 
			
		||||
        if ($stmt === false) {
 | 
			
		||||
            throw new \Federator\Exceptions\ServerError();
 | 
			
		||||
        }
 | 
			
		||||
        $stmt->bind_param("ss", $sourceUser, $targetUserId);
 | 
			
		||||
        $foundId = 0;
 | 
			
		||||
        $ret = $stmt->bind_result($foundId);
 | 
			
		||||
        $stmt->execute();
 | 
			
		||||
        if ($ret) {
 | 
			
		||||
            $stmt->fetch();
 | 
			
		||||
        }
 | 
			
		||||
        $stmt->close();
 | 
			
		||||
        if ($foundId != 0) {
 | 
			
		||||
            return false; // Already following this user
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Generate a unique ID for the follow relationship
 | 
			
		||||
        do {
 | 
			
		||||
            $id = bin2hex(openssl_random_pseudo_bytes(16));
 | 
			
		||||
            $idurl = 'https://' . $host . '/' . $sourceUser . '/' . $id;
 | 
			
		||||
 | 
			
		||||
            // Check if the generated ID is unique
 | 
			
		||||
            $sql = 'select id from follows where id = ?';
 | 
			
		||||
            $stmt = $dbh->prepare($sql);
 | 
			
		||||
            if ($stmt === false) {
 | 
			
		||||
                throw new \Federator\Exceptions\ServerError();
 | 
			
		||||
            }
 | 
			
		||||
            $stmt->bind_param("s", $idurl);
 | 
			
		||||
            $foundId = 0;
 | 
			
		||||
            $ret = $stmt->bind_result($foundId);
 | 
			
		||||
            $stmt->execute();
 | 
			
		||||
            if ($ret) {
 | 
			
		||||
                $stmt->fetch();
 | 
			
		||||
            }
 | 
			
		||||
            $stmt->close();
 | 
			
		||||
        } while ($foundId > 0);
 | 
			
		||||
 | 
			
		||||
        // Add follow with created_at timestamp
 | 
			
		||||
        $sql = 'insert into follows (id, source_user, target_user, created_at) values (?, ?, ?, NOW())';
 | 
			
		||||
        $stmt = $dbh->prepare($sql);
 | 
			
		||||
        if ($stmt === false) {
 | 
			
		||||
            throw new \Federator\Exceptions\ServerError();
 | 
			
		||||
        }
 | 
			
		||||
        $stmt->bind_param("sss", $idurl, $sourceUser, $targetUserId);
 | 
			
		||||
        $stmt->execute();
 | 
			
		||||
        $stmt->close();
 | 
			
		||||
        return $idurl; // Return the generated follow ID
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * remove follow
 | 
			
		||||
     *
 | 
			
		||||
     * @param \mysqli $dbh database handle
 | 
			
		||||
     * @param string $sourceUser source user id
 | 
			
		||||
     * @param string $targetUserId target user id
 | 
			
		||||
     * @return bool true on success
 | 
			
		||||
     */
 | 
			
		||||
    public static function removeFollow($dbh, $sourceUser, $targetUserId)
 | 
			
		||||
    {
 | 
			
		||||
        $sql = 'delete from follows where source_user = ? and target_user = ?';
 | 
			
		||||
        $stmt = $dbh->prepare($sql);
 | 
			
		||||
        if ($stmt === false) {
 | 
			
		||||
            throw new \Federator\Exceptions\ServerError();
 | 
			
		||||
        }
 | 
			
		||||
        $stmt->bind_param("ss", $sourceUser, $targetUserId);
 | 
			
		||||
        $stmt->execute();
 | 
			
		||||
        $affectedRows = $stmt->affected_rows;
 | 
			
		||||
        $stmt->close();
 | 
			
		||||
        return $affectedRows > 0;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -42,6 +42,7 @@ class User
 | 
			
		|||
                throw new \Federator\Exceptions\ServerError();
 | 
			
		||||
            }
 | 
			
		||||
            $public = openssl_pkey_get_details($private_key)['key'];
 | 
			
		||||
            $user->publicKey = $public;
 | 
			
		||||
            $private = '';
 | 
			
		||||
            openssl_pkey_export($private_key, $private);
 | 
			
		||||
            $sql = 'insert into users (id, externalid, rsapublic, rsaprivate, validuntil,';
 | 
			
		||||
| 
						 | 
				
			
			@ -100,13 +101,37 @@ class User
 | 
			
		|||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * get private rsa key
 | 
			
		||||
     * @return string|false key or false
 | 
			
		||||
     */
 | 
			
		||||
    public static function getrsaprivate(\mysqli $dbh, string $_user)
 | 
			
		||||
    {
 | 
			
		||||
        $sql = "select rsaprivate from users where id=?";
 | 
			
		||||
        $stmt = $dbh->prepare($sql);
 | 
			
		||||
        if ($stmt === false) {
 | 
			
		||||
            throw new \Federator\Exceptions\ServerError();
 | 
			
		||||
        }
 | 
			
		||||
        $stmt->bind_param("s", $_user);
 | 
			
		||||
        $ret = $stmt->bind_result($rsaPrivateKey);
 | 
			
		||||
        $stmt->execute();
 | 
			
		||||
        if ($ret) {
 | 
			
		||||
            $stmt->fetch();
 | 
			
		||||
        }
 | 
			
		||||
        $stmt->close();
 | 
			
		||||
        if ($rsaPrivateKey !== null) {
 | 
			
		||||
            return $rsaPrivateKey;
 | 
			
		||||
        }
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * extend the given user with internal data
 | 
			
		||||
     * @param \mysqli $dbh database  handle
 | 
			
		||||
     * @param \Federator\Data\User $user user to extend
 | 
			
		||||
     * @param string $_user user/profile name
 | 
			
		||||
     */
 | 
			
		||||
    protected static function extendUser(\mysqli $dbh, $user, $_user) : void
 | 
			
		||||
    protected static function extendUser(\mysqli $dbh, $user, $_user): void
 | 
			
		||||
    {
 | 
			
		||||
        $sql = 'select id,unix_timestamp(`validuntil`) from users where id=?';
 | 
			
		||||
        $stmt = $dbh->prepare($sql);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -280,6 +280,16 @@ class Main
 | 
			
		|||
        $this->host = $host;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * set content type
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $_type content type
 | 
			
		||||
     */
 | 
			
		||||
    public function setContentType($_type): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->contentType = $_type;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * set response code
 | 
			
		||||
     *
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -54,11 +54,11 @@ class ContentNation implements Connector
 | 
			
		|||
     * get followers of given user
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $userId user id
 | 
			
		||||
     * @return \Federator\Data\ActivityPub\Common\APObject[]|false
 | 
			
		||||
     * @return \Federator\Data\FedUser[]|false
 | 
			
		||||
     */
 | 
			
		||||
    public function getRemoteFollowersOfUser($userId)
 | 
			
		||||
    {
 | 
			
		||||
        // todo implement queue for this, move to DIO
 | 
			
		||||
        // todo implement queue for this
 | 
			
		||||
        if (preg_match("#^([^@]+)@([^/]+)#", $userId, $matches) == 1) {
 | 
			
		||||
            $userId = $matches[1];
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -109,25 +109,25 @@ class ContentNation implements Connector
 | 
			
		|||
        $posts = [];
 | 
			
		||||
        if (array_key_exists('activities', $r)) {
 | 
			
		||||
            $activities = $r['activities'];
 | 
			
		||||
            $host = $_SERVER['SERVER_NAME'];
 | 
			
		||||
            $config = $this->main->getConfig();
 | 
			
		||||
            $domain = $config['generic']['externaldomain'];
 | 
			
		||||
            $imgpath = $this->config['userdata']['path'];
 | 
			
		||||
            $userdata = $this->config['userdata']['url'];
 | 
			
		||||
            foreach ($activities as $activity) {
 | 
			
		||||
                $create = new \Federator\Data\ActivityPub\Common\Create();
 | 
			
		||||
                $create->setAActor('https://' . $host . '/' . $userId);
 | 
			
		||||
                $create->setAActor('https://' . $domain . '/' . $userId);
 | 
			
		||||
                $create->setID($activity['id'])
 | 
			
		||||
                    ->setPublished($activity['timestamp'])
 | 
			
		||||
                    ->setPublished($activity['published'] ?? $activity['timestamp'])
 | 
			
		||||
                    ->addTo("https://www.w3.org/ns/activitystreams#Public")
 | 
			
		||||
                    ->addCC('https://' . $host . '/' . $userId . '/followers.json');
 | 
			
		||||
                    ->addCC('https://' . $domain . '/' . $userId . '/followers');
 | 
			
		||||
 | 
			
		||||
                switch ($activity['type']) {
 | 
			
		||||
                    case 'Article':
 | 
			
		||||
                        $create->setURL('https://' . $host . '/' . $activity['language'] . '/' . $userId . '/'
 | 
			
		||||
                            . $activity['name']);
 | 
			
		||||
                        $create->setURL('https://' . $domain . '/' . $activity['name']);
 | 
			
		||||
                        $apArticle = new \Federator\Data\ActivityPub\Common\Article();
 | 
			
		||||
                        if (array_key_exists('tags', $activity)) {
 | 
			
		||||
                            foreach ($activity['tags'] as $tag) {
 | 
			
		||||
                                $href = 'https://' . $host . '/' . $activity['language']
 | 
			
		||||
                                    . '/search.htm?tagsearch=' . urlencode($tag);
 | 
			
		||||
                                $href = 'https://' . $domain . '/search.htm?tagsearch=' . urlencode($tag);
 | 
			
		||||
                                $tagObj = new \Federator\Data\ActivityPub\Common\Tag();
 | 
			
		||||
                                $tagObj->setHref($href)
 | 
			
		||||
                                    ->setName('#' . urlencode(str_replace(' ', '', $tag)))
 | 
			
		||||
| 
						 | 
				
			
			@ -137,7 +137,7 @@ class ContentNation implements Connector
 | 
			
		|||
                        }
 | 
			
		||||
                        $apArticle->setPublished($activity['published'])
 | 
			
		||||
                            ->setName($activity['title'])
 | 
			
		||||
                            ->setAttributedTo('https://' . $host . '/' . $activity['profilename'])
 | 
			
		||||
                            ->setAttributedTo('https://' . $domain . '/' . $activity['profilename'])
 | 
			
		||||
                            ->setContent(
 | 
			
		||||
                                $activity['teaser'] ??
 | 
			
		||||
                                $this->main->translate(
 | 
			
		||||
| 
						 | 
				
			
			@ -147,11 +147,10 @@ class ContentNation implements Connector
 | 
			
		|||
                                )
 | 
			
		||||
                            )
 | 
			
		||||
                            ->addTo("https://www.w3.org/ns/activitystreams#Public")
 | 
			
		||||
                            ->addCC('https://' . $host . '/' . $userId . '/followers.json');
 | 
			
		||||
                            ->addCC('https://' . $domain . '/' . $userId . '/followers.json');
 | 
			
		||||
                        $articleimage = $activity['imagealt'] ??
 | 
			
		||||
                            $this->main->translate($activity['language'], 'article', 'image');
 | 
			
		||||
                        $idurl = 'https://' . $host . '/' . $activity['language']
 | 
			
		||||
                            . '/' . $userId . '/' . $activity['name'];
 | 
			
		||||
                        $idurl = 'https://' . $domain . '/' . $userId . '/' . $activity['name'];
 | 
			
		||||
                        $apArticle->setID($idurl)
 | 
			
		||||
                            ->setURL($idurl);
 | 
			
		||||
                        $image = $activity['image'] ?? $activity['profileimg'];
 | 
			
		||||
| 
						 | 
				
			
			@ -176,7 +175,7 @@ class ContentNation implements Connector
 | 
			
		|||
                        $posts[] = $create;
 | 
			
		||||
                        break; // Comment
 | 
			
		||||
                    case 'Vote':
 | 
			
		||||
                        $url = 'https://' . $host . '/' . $activity['articlelang'] . $userId . '/'
 | 
			
		||||
                        $url = 'https://' . $domain . '/' . $userId . '/'
 | 
			
		||||
                            . $activity['articlename'];
 | 
			
		||||
                        $url .= '/vote/' . $activity['id'];
 | 
			
		||||
                        $create->setURL($url);
 | 
			
		||||
| 
						 | 
				
			
			@ -204,8 +203,7 @@ class ContentNation implements Connector
 | 
			
		|||
                        $actor = new \Federator\Data\ActivityPub\Common\APObject('Person');
 | 
			
		||||
                        $actor->setName($activity['username']);
 | 
			
		||||
                        $like->setActor($actor);
 | 
			
		||||
                        $url = 'https://' . $host . '/' . $activity['articlelang']
 | 
			
		||||
                            . '/' . $userId . '/' . $activity['articlename'];
 | 
			
		||||
                        $url = 'https://' . $domain . '/' . $userId . '/' . $activity['articlename'];
 | 
			
		||||
                        if ($activity['comment'] !== '') {
 | 
			
		||||
                            $url .= '/comment/' . $activity['comment'];
 | 
			
		||||
                        }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,7 +23,7 @@ class DummyConnector implements Connector
 | 
			
		|||
     * get followers of given user
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $userId user id @unused-param
 | 
			
		||||
     * @return \Federator\Data\ActivityPub\Common\APObject[]|false
 | 
			
		||||
     * @return \Federator\Data\FedUser[]|false
 | 
			
		||||
     */
 | 
			
		||||
    public function getRemoteFollowersOfUser($userId)
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -90,7 +90,7 @@ class RedisCache implements Cache
 | 
			
		|||
     *
 | 
			
		||||
     * @param string $id user id @unused-param
 | 
			
		||||
 | 
			
		||||
     * @return \Federator\Data\ActivityPub\Common\APObject[]|false
 | 
			
		||||
     * @return \Federator\Data\FedUser[]|false
 | 
			
		||||
     */
 | 
			
		||||
    public function getRemoteFollowersOfUser($id)
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			@ -163,6 +163,26 @@ class RedisCache implements Cache
 | 
			
		|||
        return $user;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * get remote federation user by given name
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $_name user/profile name
 | 
			
		||||
     * @return \Federator\Data\FedUser | false
 | 
			
		||||
     */
 | 
			
		||||
    public function getRemoteFedUserByName(string $_name)
 | 
			
		||||
    {
 | 
			
		||||
        if (!$this->connected) {
 | 
			
		||||
            $this->connect();
 | 
			
		||||
        }
 | 
			
		||||
        $key = self::createKey('u', $_name);
 | 
			
		||||
        $data = $this->redis->get($key);
 | 
			
		||||
        if ($data === false) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        $user = \Federator\Data\FedUser::createFromJson($data);
 | 
			
		||||
        return $user;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * get remote user by given session
 | 
			
		||||
     *
 | 
			
		||||
| 
						 | 
				
			
			@ -203,7 +223,7 @@ class RedisCache implements Cache
 | 
			
		|||
     * save remote followers by user
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $user user name @unused-param
 | 
			
		||||
     * @param \Federator\Data\ActivityPub\Common\APObject[]|false $followers user followers @unused-param
 | 
			
		||||
     * @param \Federator\Data\FedUser[]|false $followers user followers @unused-param
 | 
			
		||||
     * @return void
 | 
			
		||||
     */
 | 
			
		||||
    public function saveRemoteFollowersOfUser($user, $followers)
 | 
			
		||||
| 
						 | 
				
			
			@ -252,6 +272,20 @@ class RedisCache implements Cache
 | 
			
		|||
        $this->redis->setEx($key, $this->userTTL, $serialized);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * save remote federation user by given name
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $_name user/profile name
 | 
			
		||||
     * @param \Federator\Data\FedUser $user user data
 | 
			
		||||
     * @return void
 | 
			
		||||
     */
 | 
			
		||||
    public function saveRemoteFedUserByName(string $_name, \Federator\Data\FedUser $user)
 | 
			
		||||
    {
 | 
			
		||||
        $key = self::createKey('u', $_name);
 | 
			
		||||
        $serialized = $user->toJson();
 | 
			
		||||
        $this->redis->setEx($key, $this->userTTL, $serialized);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * save remote user by given session
 | 
			
		||||
     *
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										3
									
								
								sql/2025-05-06.sql
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								sql/2025-05-06.sql
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
create table follows(`id` varchar(255) unique primary key,`source_user` varchar(255) not null,`target_user` varchar(255) not null,`created_at` timestamp default current_timestamp,unique key `unique_follow` (`source_user`, `target_user`));
 | 
			
		||||
create table fedusers(`id` varchar(255) unique primary key,`url` varchar(255) not null,`name` varchar(255) default '',`publickey` text default '',`summary` text default '',`validuntil` timestamp null default null,`type` enum('person', 'group') default 'person',`inboxurl` varchar(255) default null,`sharedinboxurl` varchar(255) default null,`followersurl` varchar(255) default null,`followingurl` varchar(255) default null,`publickeyid` varchar(255) default null,`outboxurl` varchar(255) default null,unique key `unique_feduser` (`url`),unique key `unique_feduser_id` (`url`));
 | 
			
		||||
update settings set `value`="2025-05-06" where `key`="database_version";
 | 
			
		||||
| 
						 | 
				
			
			@ -58,7 +58,7 @@
 | 
			
		|||
            {rdelim}
 | 
			
		||||
        {rdelim}
 | 
			
		||||
    ],
 | 
			
		||||
    "id":"https://{$fqdn}/users/{$username}",
 | 
			
		||||
    "id":"https://{$fqdn}/{$username}",
 | 
			
		||||
    "type":"{$type}",
 | 
			
		||||
    "following":"https://{$fqdn}/users/{$username}/following",
 | 
			
		||||
    "followers":"https://{$fqdn}/users/{$username}/followers",
 | 
			
		||||
| 
						 | 
				
			
			@ -74,8 +74,8 @@
 | 
			
		|||
    "discoverable":true,
 | 
			
		||||
    "published":"{$registered}",
 | 
			
		||||
    "publicKey":{ldelim}
 | 
			
		||||
    "id":"https://{$fqdn}/users/{$username}#main-key",
 | 
			
		||||
    "owner":"https://{$fqdn}/users/{$username}",
 | 
			
		||||
    "id":"https://{$fqdn}/{$username}#main-key",
 | 
			
		||||
    "owner":"https://{$fqdn}/{$username}",
 | 
			
		||||
    "publicKeyPem":"{$publickey}"
 | 
			
		||||
    {rdelim},
 | 
			
		||||
    "tag":[],
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue