forked from grumpydevelop/federator
		
	initial mastodon support and minor improvements
- reworked plugin handling. Main now registers and keeps all connectors and based on request we select the correct one and pass it (mainly for clean async-request-handling) - added outbox functionality for mastodon - changed image-mime-type approach to retrieve mime-type from the url, this way we don't need to store the images on our server/download each image - added host for connector for better debugging - minor bug-fixes
This commit is contained in:
		
							parent
							
								
									a21345c3c7
								
							
						
					
					
						commit
						530caa7ea6
					
				
					 25 changed files with 614 additions and 102 deletions
				
			
		
							
								
								
									
										1
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							| 
						 | 
					@ -5,3 +5,4 @@ php-docs
 | 
				
			||||||
.phpdoc
 | 
					.phpdoc
 | 
				
			||||||
phpdoc
 | 
					phpdoc
 | 
				
			||||||
html
 | 
					html
 | 
				
			||||||
 | 
					/cache
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,6 +14,8 @@ compiledir = '../cache'
 | 
				
			||||||
[plugins]
 | 
					[plugins]
 | 
				
			||||||
rediscache = 'rediscache.php'
 | 
					rediscache = 'rediscache.php'
 | 
				
			||||||
dummy = 'dummyconnector.php'
 | 
					dummy = 'dummyconnector.php'
 | 
				
			||||||
 | 
					contentnation = 'contentnation.php'
 | 
				
			||||||
 | 
					mastodon = 'mastodon.php'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[maintenance]
 | 
					[maintenance]
 | 
				
			||||||
username = 'federatoradmin'
 | 
					username = 'federatoradmin'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										6
									
								
								contentnation.ini
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								contentnation.ini
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,6 @@
 | 
				
			||||||
 | 
					[contentnation]
 | 
				
			||||||
 | 
					service-uri = https://contentnation.net
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[userdata]
 | 
				
			||||||
 | 
					path = '/home/net/contentnation/userdata/htdocs/' // need to download local copy of image and put img-path here
 | 
				
			||||||
 | 
					url = 'https://userdata.contentnation.net'
 | 
				
			||||||
							
								
								
									
										112
									
								
								htdocs/index.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								htdocs/index.html
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,112 @@
 | 
				
			||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html lang="en">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<head>
 | 
				
			||||||
 | 
					    <meta charset="UTF-8">
 | 
				
			||||||
 | 
					    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
				
			||||||
 | 
					    <title>API Request UI</title>
 | 
				
			||||||
 | 
					    <script src="https://cdn.tailwindcss.com"></script>
 | 
				
			||||||
 | 
					</head>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<body class="flex justify-center items-center min-h-screen bg-gray-100">
 | 
				
			||||||
 | 
					    <div class="w-full max-w-3xl bg-white shadow-lg rounded-lg p-6 m-6">
 | 
				
			||||||
 | 
					        <h2 class="text-2xl font-semibold mb-4 text-center">API Request UI</h2>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div id="request-container">
 | 
				
			||||||
 | 
					            <!-- Request Form Template -->
 | 
				
			||||||
 | 
					            <div class="request-box border p-4 rounded-lg mb-4 bg-gray-50">
 | 
				
			||||||
 | 
					                <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">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <label class="block font-medium">Request type</label>
 | 
				
			||||||
 | 
					                <input type="text" class="request-type-input w-full p-2 border rounded-md mb-2"
 | 
				
			||||||
 | 
					                    placeholder="POST or GET" value="GET">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <label class="block font-medium">X-Session:</label>
 | 
				
			||||||
 | 
					                <input type="text" class="session-input w-full p-2 border rounded-md mb-2"
 | 
				
			||||||
 | 
					                    placeholder="Enter X-Session token" value="">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <label class="block font-medium">X-Profile:</label>
 | 
				
			||||||
 | 
					                <input type="text" class="profile-input w-full p-2 border rounded-md mb-2" placeholder="Enter X-Profile"
 | 
				
			||||||
 | 
					                    value="">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <div class="buttonContainer">
 | 
				
			||||||
 | 
					                    <button class="send-btn bg-blue-500 text-white px-4 py-2 rounded-md w-full hover:bg-blue-600">
 | 
				
			||||||
 | 
					                        Send Request
 | 
				
			||||||
 | 
					                    </button>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <p class="mt-2 text-sm text-gray-700">Response:</p>
 | 
				
			||||||
 | 
					                <p class="response mt-2 text-sm text-gray-700 font-mono whitespace-pre">Waiting for response</p>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <button id="add-request" class="mt-4 bg-green-500 text-white px-4 py-2 rounded-md w-full hover:bg-green-600">
 | 
				
			||||||
 | 
					            Add Another Request
 | 
				
			||||||
 | 
					        </button>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <script>
 | 
				
			||||||
 | 
					        function sendRequest(button) {
 | 
				
			||||||
 | 
					            const container = button.parentElement.parentElement;
 | 
				
			||||||
 | 
					            const targetLink = container.querySelector(".target-link-input").value;
 | 
				
			||||||
 | 
					            const requestType = container.querySelector(".request-type-input").value;
 | 
				
			||||||
 | 
					            const session = container.querySelector(".session-input").value;
 | 
				
			||||||
 | 
					            const profile = container.querySelector(".profile-input").value;
 | 
				
			||||||
 | 
					            const responseField = container.querySelector(".response");
 | 
				
			||||||
 | 
					            button.parentElement.style.cursor = "not-allowed";
 | 
				
			||||||
 | 
					            button.style.pointerEvents = "none";
 | 
				
			||||||
 | 
					            button.textContent = "Sending...";
 | 
				
			||||||
 | 
					            responseField.textContent = "Waiting for response";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const headers = {
 | 
				
			||||||
 | 
					                ...(session ? { "X-Session": session } : {}),
 | 
				
			||||||
 | 
					                ...(profile ? { "X-Profile": profile } : {})
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            fetch("http://localhost/api/" + targetLink, {
 | 
				
			||||||
 | 
					                method: requestType,
 | 
				
			||||||
 | 
					                headers
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					                .then(response => response.text())
 | 
				
			||||||
 | 
					                .then(data => {
 | 
				
			||||||
 | 
					                    responseField.textContent = data;
 | 
				
			||||||
 | 
					                    button.parentElement.style.cursor = "";
 | 
				
			||||||
 | 
					                    button.style.pointerEvents = "";
 | 
				
			||||||
 | 
					                    button.textContent = "Send Request";
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					                .catch(error => {
 | 
				
			||||||
 | 
					                    responseField.textContent = "Error: " + error;
 | 
				
			||||||
 | 
					                    button.parentElement.style.cursor = "";
 | 
				
			||||||
 | 
					                    button.style.pointerEvents = "";
 | 
				
			||||||
 | 
					                    button.textContent = "Send Request";
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        document.querySelectorAll(".send-btn").forEach(btn => {
 | 
				
			||||||
 | 
					            btn.addEventListener("click", function () {
 | 
				
			||||||
 | 
					                sendRequest(this);
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        document.getElementById("add-request").addEventListener("click", function () {
 | 
				
			||||||
 | 
					            const container = document.getElementById("request-container");
 | 
				
			||||||
 | 
					            const requestBox = container.firstElementChild.cloneNode(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            requestBox.querySelector(".target-link-input").value = "federator/v1/dummy/moo";
 | 
				
			||||||
 | 
					            requestBox.querySelector(".request-type-input").value = "GET";
 | 
				
			||||||
 | 
					            requestBox.querySelector(".session-input").value = "somethingvalider";
 | 
				
			||||||
 | 
					            requestBox.querySelector(".profile-input").value = "ihaveone";
 | 
				
			||||||
 | 
					            requestBox.querySelector(".response").textContent = "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            requestBox.querySelector(".send-btn").addEventListener("click", function () {
 | 
				
			||||||
 | 
					                sendRequest(this);
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            container.appendChild(requestBox);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    </script>
 | 
				
			||||||
 | 
					</body>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
							
								
								
									
										3
									
								
								htdocs/info.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								htdocs/info.php
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,3 @@
 | 
				
			||||||
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					echo phpinfo();
 | 
				
			||||||
							
								
								
									
										6
									
								
								mastodon.ini
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								mastodon.ini
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,6 @@
 | 
				
			||||||
 | 
					[mastodon]
 | 
				
			||||||
 | 
					service-uri = https://mastodon.social
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[userdata]
 | 
				
			||||||
 | 
					path = '/home/net/contentnation/userdata/htdocs/' // need to download local copy of image and put img-path here
 | 
				
			||||||
 | 
					url = 'https://files.mastodon.net'
 | 
				
			||||||
| 
						 | 
					@ -6,7 +6,7 @@
 | 
				
			||||||
 * @author Sascha Nitsch (grumpydeveloper)
 | 
					 * @author Sascha Nitsch (grumpydeveloper)
 | 
				
			||||||
 **/
 | 
					 **/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 namespace Federator;
 | 
					namespace Federator;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * main API class
 | 
					 * main API class
 | 
				
			||||||
| 
						 | 
					@ -69,14 +69,26 @@ class Api extends Main
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * main API function
 | 
					     * main API function
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function run() : void
 | 
					    public function run(): void
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $this->setPath((string)$_REQUEST['_call']);
 | 
					        $this->setPath((string) $_REQUEST['_call']);
 | 
				
			||||||
        $this->openDatabase();
 | 
					        $this->openDatabase();
 | 
				
			||||||
        $this->loadPlugins();
 | 
					        $this->loadPlugins();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $host = 'dummy'; // fallback
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Check if path matches something like fedusers/username@domain.tld
 | 
				
			||||||
 | 
					        if (preg_match("#^fedusers/([^@]+)@([^/]+)$#", $this->path, $matches) === 1) {
 | 
				
			||||||
 | 
					            $host = strtolower($matches[2]); // extract domain
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            $host = 'dummy';
 | 
				
			||||||
 | 
					            echo "using dummy host in API::run\n";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $connector = $this->getConnectorForHost($host);
 | 
				
			||||||
        $retval = "";
 | 
					        $retval = "";
 | 
				
			||||||
        $handler = null;
 | 
					        $handler = null;
 | 
				
			||||||
        if ($this->connector === null) {
 | 
					        if ($connector === null) {
 | 
				
			||||||
            http_response_code(500);
 | 
					            http_response_code(500);
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
| 
						 | 
					@ -85,7 +97,7 @@ class Api extends Main
 | 
				
			||||||
                $this->dbh,
 | 
					                $this->dbh,
 | 
				
			||||||
                $_SERVER['HTTP_X_SESSION'],
 | 
					                $_SERVER['HTTP_X_SESSION'],
 | 
				
			||||||
                $_SERVER['HTTP_X_PROFILE'],
 | 
					                $_SERVER['HTTP_X_PROFILE'],
 | 
				
			||||||
                $this->connector,
 | 
					                $connector,
 | 
				
			||||||
                $this->cache
 | 
					                $this->cache
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
            if ($this->user === false) {
 | 
					            if ($this->user === false) {
 | 
				
			||||||
| 
						 | 
					@ -112,7 +124,7 @@ class Api extends Main
 | 
				
			||||||
        $printresponse = true;
 | 
					        $printresponse = true;
 | 
				
			||||||
        if ($handler !== null) {
 | 
					        if ($handler !== null) {
 | 
				
			||||||
            try {
 | 
					            try {
 | 
				
			||||||
                $printresponse = $handler->exec($this->paths, $this->user);
 | 
					                $printresponse = $handler->exec($this->paths, $this->user, $connector);
 | 
				
			||||||
                if ($printresponse) {
 | 
					                if ($printresponse) {
 | 
				
			||||||
                    $retval = $handler->toJson();
 | 
					                    $retval = $handler->toJson();
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
| 
						 | 
					@ -168,7 +180,7 @@ class Api extends Main
 | 
				
			||||||
     * @param string $message optional message
 | 
					     * @param string $message optional message
 | 
				
			||||||
     * @throws Exceptions\PermissionDenied
 | 
					     * @throws Exceptions\PermissionDenied
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function checkPermission($permission, $exception = "\Exceptions\PermissionDenied", $message = null) : void
 | 
					    public function checkPermission($permission, $exception = "\Exceptions\PermissionDenied", $message = null): void
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        // generic check first
 | 
					        // generic check first
 | 
				
			||||||
        if ($this->user === false) {
 | 
					        if ($this->user === false) {
 | 
				
			||||||
| 
						 | 
					@ -198,7 +210,7 @@ class Api extends Main
 | 
				
			||||||
     *          input to strip
 | 
					     *          input to strip
 | 
				
			||||||
     * @return string stripped input
 | 
					     * @return string stripped input
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public static function stripHTML(string $_input) : string
 | 
					    public static function stripHTML(string $_input): string
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $out = preg_replace('/<(script[^>]*)>/i', '<${1}>', $_input);
 | 
					        $out = preg_replace('/<(script[^>]*)>/i', '<${1}>', $_input);
 | 
				
			||||||
        $out = preg_replace('/<\/(script)>/i', '</${1};>', $out);
 | 
					        $out = preg_replace('/<\/(script)>/i', '</${1};>', $out);
 | 
				
			||||||
| 
						 | 
					@ -212,7 +224,7 @@ class Api extends Main
 | 
				
			||||||
     *          parameter to check
 | 
					     *          parameter to check
 | 
				
			||||||
     * @return bool true if in
 | 
					     * @return bool true if in
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public static function hasPost(string $_key) : bool
 | 
					    public static function hasPost(string $_key): bool
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        return array_key_exists($_key, $_POST);
 | 
					        return array_key_exists($_key, $_POST);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -228,13 +240,13 @@ class Api extends Main
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function escapePost(string $key, $int = false)
 | 
					    public function escapePost(string $key, $int = false)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        if (! array_key_exists($key, $_POST)) {
 | 
					        if (!array_key_exists($key, $_POST)) {
 | 
				
			||||||
            return $int ? 0 : "";
 | 
					            return $int ? 0 : "";
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if ($int === true) {
 | 
					        if ($int === true) {
 | 
				
			||||||
            return intval($_POST[$key]);
 | 
					            return intval($_POST[$key]);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        $ret = $this->dbh->escape_string($this->stripHTML((string)$_POST[$key]));
 | 
					        $ret = $this->dbh->escape_string($this->stripHTML((string) $_POST[$key]));
 | 
				
			||||||
        return $ret;
 | 
					        return $ret;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,9 +19,10 @@ interface APIInterface
 | 
				
			||||||
     * @param array<string> $paths path array split by /
 | 
					     * @param array<string> $paths path array split by /
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @param \Federator\Data\User|false $user user who is calling us
 | 
					     * @param \Federator\Data\User|false $user user who is calling us
 | 
				
			||||||
 | 
					     * @param \Federator\Connector\Connector $connector connector to use
 | 
				
			||||||
     * @return bool true on success
 | 
					     * @return bool true on success
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function exec($paths, $user);
 | 
					    public function exec($paths, $user, $connector);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * get internal represenation as json string
 | 
					     * get internal represenation as json string
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -43,9 +43,10 @@ class FedUsers implements APIInterface
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @param array<string> $paths path array split by /
 | 
					     * @param array<string> $paths path array split by /
 | 
				
			||||||
     * @param \Federator\Data\User|false $user user who is calling us @unused-param
 | 
					     * @param \Federator\Data\User|false $user user who is calling us @unused-param
 | 
				
			||||||
 | 
					     * @param \Federator\Connector\Connector $connector connector to use
 | 
				
			||||||
     * @return bool true on success
 | 
					     * @return bool true on success
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function exec($paths, $user)
 | 
					    public function exec($paths, $user, $connector)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $method = $_SERVER["REQUEST_METHOD"];
 | 
					        $method = $_SERVER["REQUEST_METHOD"];
 | 
				
			||||||
        $handler = null;
 | 
					        $handler = null;
 | 
				
			||||||
| 
						 | 
					@ -53,7 +54,7 @@ class FedUsers implements APIInterface
 | 
				
			||||||
            case 2:
 | 
					            case 2:
 | 
				
			||||||
                if ($method === 'GET') {
 | 
					                if ($method === 'GET') {
 | 
				
			||||||
                    // /users/username or /@username
 | 
					                    // /users/username or /@username
 | 
				
			||||||
                    return $this->returnUserProfile($paths[1]);
 | 
					                    return $this->returnUserProfile($paths[1], $connector);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                break;
 | 
					                break;
 | 
				
			||||||
            case 3:
 | 
					            case 3:
 | 
				
			||||||
| 
						 | 
					@ -82,10 +83,10 @@ class FedUsers implements APIInterface
 | 
				
			||||||
            $ret = false;
 | 
					            $ret = false;
 | 
				
			||||||
            switch ($method) {
 | 
					            switch ($method) {
 | 
				
			||||||
                case 'GET':
 | 
					                case 'GET':
 | 
				
			||||||
                    $ret = $handler->get($paths[1]);
 | 
					                    $ret = $handler->get($paths[1], $connector);
 | 
				
			||||||
                    break;
 | 
					                    break;
 | 
				
			||||||
                case 'POST':
 | 
					                case 'POST':
 | 
				
			||||||
                    $ret = $handler->post($paths[1]);
 | 
					                    $ret = $handler->post($paths[1], $connector);
 | 
				
			||||||
                    break;
 | 
					                    break;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            if ($ret !== false) {
 | 
					            if ($ret !== false) {
 | 
				
			||||||
| 
						 | 
					@ -102,14 +103,15 @@ class FedUsers implements APIInterface
 | 
				
			||||||
     * return user profile
 | 
					     * return user profile
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @param string $_name
 | 
					     * @param string $_name
 | 
				
			||||||
 | 
					     * @param \Federator\Connector\Connector $connector connector to use
 | 
				
			||||||
     * @return boolean true on success
 | 
					     * @return boolean true on success
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    private function returnUserProfile($_name)
 | 
					    private function returnUserProfile($_name, $connector)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $user = \Federator\DIO\User::getUserByName(
 | 
					        $user = \Federator\DIO\User::getUserByName(
 | 
				
			||||||
            $this->main->getDatabase(),
 | 
					            $this->main->getDatabase(),
 | 
				
			||||||
            $_name,
 | 
					            $_name,
 | 
				
			||||||
            $this->main->getConnector(),
 | 
					            $connector,
 | 
				
			||||||
            $this->main->getCache()
 | 
					            $this->main->getCache()
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
        if ($user === false || $user->id === null) {
 | 
					        if ($user === false || $user->id === null) {
 | 
				
			||||||
| 
						 | 
					@ -123,7 +125,7 @@ class FedUsers implements APIInterface
 | 
				
			||||||
            'fqdn' => $_SERVER['SERVER_NAME'],
 | 
					            'fqdn' => $_SERVER['SERVER_NAME'],
 | 
				
			||||||
            'name' => $user->name,
 | 
					            'name' => $user->name,
 | 
				
			||||||
            'username' => $user->id,
 | 
					            'username' => $user->id,
 | 
				
			||||||
            'publickey' => str_replace("\n", "\\n", $user->publicKey),
 | 
					            'publickey' => $user->publicKey,
 | 
				
			||||||
            'registered' => gmdate('Y-m-d\TH:i:s\Z', $user->registered), // 2021-03-25T00:00:00Z
 | 
					            'registered' => gmdate('Y-m-d\TH:i:s\Z', $user->registered), // 2021-03-25T00:00:00Z
 | 
				
			||||||
            'summary' => $user->summary,
 | 
					            'summary' => $user->summary,
 | 
				
			||||||
            'type' => $user->type
 | 
					            'type' => $user->type
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,15 +14,17 @@ interface FedUsersInterface
 | 
				
			||||||
     * get call for user
 | 
					     * get call for user
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @param string $_user user to fetch data for
 | 
					     * @param string $_user user to fetch data for
 | 
				
			||||||
 | 
					     * @param \Federator\Connector\Connector $connector connector to use
 | 
				
			||||||
     * @return string|false response or false in case of error
 | 
					     * @return string|false response or false in case of error
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function get($_user);
 | 
					    public function get($_user, $connector);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * post call for user
 | 
					     * post call for user
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @param string $_user user to add data to
 | 
					     * @param string $_user user to add data to
 | 
				
			||||||
 | 
					     * @param \Federator\Connector\Connector $connector connector to use
 | 
				
			||||||
     * @return string|false response or false in case of error
 | 
					     * @return string|false response or false in case of error
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function post($_user);
 | 
					    public function post($_user, $connector);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -33,13 +33,13 @@ class Outbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
				
			||||||
     * handle get call
 | 
					     * handle get call
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @param string $_user user to fetch outbox for
 | 
					     * @param string $_user user to fetch outbox for
 | 
				
			||||||
 | 
					     * @param \Federator\Connector\Connector $connector connector to use
 | 
				
			||||||
     * @return string|false response
 | 
					     * @return string|false response
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function get($_user)
 | 
					    public function get($_user, $connector)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $dbh = $this->main->getDatabase();
 | 
					        $dbh = $this->main->getDatabase();
 | 
				
			||||||
        $cache = $this->main->getCache();
 | 
					        $cache = $this->main->getCache();
 | 
				
			||||||
        $connector = $this->main->getConnector();
 | 
					 | 
				
			||||||
        // get user
 | 
					        // get user
 | 
				
			||||||
        $user = \Federator\DIO\User::getUserByName(
 | 
					        $user = \Federator\DIO\User::getUserByName(
 | 
				
			||||||
            $dbh,
 | 
					            $dbh,
 | 
				
			||||||
| 
						 | 
					@ -50,6 +50,7 @@ class Outbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
				
			||||||
        if ($user->id === null) {
 | 
					        if ($user->id === null) {
 | 
				
			||||||
            return false;
 | 
					            return false;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // get posts from user
 | 
					        // get posts from user
 | 
				
			||||||
        $outbox = new \Federator\Data\ActivityPub\Common\Outbox();
 | 
					        $outbox = new \Federator\Data\ActivityPub\Common\Outbox();
 | 
				
			||||||
        $min = $this->main->extractFromURI("min", "");
 | 
					        $min = $this->main->extractFromURI("min", "");
 | 
				
			||||||
| 
						 | 
					@ -79,16 +80,17 @@ class Outbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
				
			||||||
            $outbox->setPrev($id . '&min=' . $oldestId);
 | 
					            $outbox->setPrev($id . '&min=' . $oldestId);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        $obj = $outbox->toObject();
 | 
					        $obj = $outbox->toObject();
 | 
				
			||||||
        return json_encode($obj);
 | 
					        return json_encode($obj, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * handle post call
 | 
					     * handle post call
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @param string $_user user to add data to outbox @unused-param
 | 
					     * @param string $_user user to add data to outbox @unused-param
 | 
				
			||||||
 | 
					     * @param \Federator\Connector\Connector $connector connector to use
 | 
				
			||||||
     * @return string|false response
 | 
					     * @return string|false response
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function post($_user)
 | 
					    public function post($_user, $connector)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        return false;
 | 
					        return false;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -23,7 +23,7 @@ class Dummy implements \Federator\Api\APIInterface
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * internal message to output
 | 
					     * internal message to output
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @var Array<string, mixed> $message
 | 
					     * @var array<string, mixed> $message
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    private $message = [];
 | 
					    private $message = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -42,9 +42,10 @@ class Dummy implements \Federator\Api\APIInterface
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @param array<string> $paths path array split by /
 | 
					     * @param array<string> $paths path array split by /
 | 
				
			||||||
     * @param \Federator\Data\User|false $user user who is calling us
 | 
					     * @param \Federator\Data\User|false $user user who is calling us
 | 
				
			||||||
 | 
					     * @param \Federator\Connector\Connector $connector connector to use
 | 
				
			||||||
     * @return bool true on success
 | 
					     * @return bool true on success
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function exec($paths, $user) : bool
 | 
					    public function exec($paths, $user, $connector) : bool
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        // only for user with the 'publish' permission
 | 
					        // only for user with the 'publish' permission
 | 
				
			||||||
        if ($user === false || $user->hasPermission('publish') === false) {
 | 
					        if ($user === false || $user->hasPermission('publish') === false) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -57,7 +57,7 @@ class WellKnown implements APIInterface
 | 
				
			||||||
     * @param \Federator\Data\User|false $user user who is calling us @unused-param
 | 
					     * @param \Federator\Data\User|false $user user who is calling us @unused-param
 | 
				
			||||||
     * @return bool true on success
 | 
					     * @return bool true on success
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function exec($paths, $user)
 | 
					    public function exec($paths, $user, $connector)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $method = $_SERVER["REQUEST_METHOD"];
 | 
					        $method = $_SERVER["REQUEST_METHOD"];
 | 
				
			||||||
        switch ($method) {
 | 
					        switch ($method) {
 | 
				
			||||||
| 
						 | 
					@ -66,14 +66,14 @@ class WellKnown implements APIInterface
 | 
				
			||||||
                    case 2:
 | 
					                    case 2:
 | 
				
			||||||
                        if ($paths[0] === 'nodeinfo') {
 | 
					                        if ($paths[0] === 'nodeinfo') {
 | 
				
			||||||
                            $ni = new WellKnown\NodeInfo($this, $this->main);
 | 
					                            $ni = new WellKnown\NodeInfo($this, $this->main);
 | 
				
			||||||
                            return $ni->exec($paths);
 | 
					                            return $ni->exec($paths, $connector);
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                        switch ($paths[1]) {
 | 
					                        switch ($paths[1]) {
 | 
				
			||||||
                            case 'host-meta':
 | 
					                            case 'host-meta':
 | 
				
			||||||
                                return $this->hostMeta();
 | 
					                                return $this->hostMeta();
 | 
				
			||||||
                            case 'nodeinfo':
 | 
					                            case 'nodeinfo':
 | 
				
			||||||
                                $ni = new WellKnown\NodeInfo($this, $this->main);
 | 
					                                $ni = new WellKnown\NodeInfo($this, $this->main);
 | 
				
			||||||
                                return $ni->exec($paths);
 | 
					                                return $ni->exec($paths, $connector);
 | 
				
			||||||
                            case 'webfinger':
 | 
					                            case 'webfinger':
 | 
				
			||||||
                                $wf = new WellKnown\WebFinger($this, $this->main);
 | 
					                                $wf = new WellKnown\WebFinger($this, $this->main);
 | 
				
			||||||
                                return $wf->exec();
 | 
					                                return $wf->exec();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -41,9 +41,10 @@ class NodeInfo
 | 
				
			||||||
     * handle nodeinfo request
 | 
					     * handle nodeinfo request
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @param string[] $paths path of us
 | 
					     * @param string[] $paths path of us
 | 
				
			||||||
 | 
					     * @param \Federator\Connector\Connector $connector connector to use
 | 
				
			||||||
     * @return bool true on success
 | 
					     * @return bool true on success
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function exec($paths)
 | 
					    public function exec($paths, $connector)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $data = [
 | 
					        $data = [
 | 
				
			||||||
            'fqdn' => $_SERVER['SERVER_NAME']
 | 
					            'fqdn' => $_SERVER['SERVER_NAME']
 | 
				
			||||||
| 
						 | 
					@ -64,7 +65,7 @@ class NodeInfo
 | 
				
			||||||
                default:
 | 
					                default:
 | 
				
			||||||
                    $template = 'nodeinfo2.0.json';
 | 
					                    $template = 'nodeinfo2.0.json';
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            $stats = \Federator\DIO\Stats::getStats($this->main);
 | 
					            $stats = \Federator\DIO\Stats::getStats($this->main, $connector);
 | 
				
			||||||
            echo "fetch usercount via connector\n";
 | 
					            echo "fetch usercount via connector\n";
 | 
				
			||||||
            $data['usercount'] = $stats->userCount;
 | 
					            $data['usercount'] = $stats->userCount;
 | 
				
			||||||
            $data['postcount'] = $stats->postCount;
 | 
					            $data['postcount'] = $stats->postCount;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -46,18 +46,18 @@ class WebFinger
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $_resource = $this->main->extractFromURI('resource');
 | 
					        $_resource = $this->main->extractFromURI('resource');
 | 
				
			||||||
        $matches = [];
 | 
					        $matches = [];
 | 
				
			||||||
        $config = $this->main->getConfig();
 | 
					        if (preg_match("/^acct:([^@]+)@(.*)$/", $_resource, $matches) != 1) {
 | 
				
			||||||
        $domain = $config['generic']['externaldomain'];
 | 
					 | 
				
			||||||
        if (preg_match("/^acct:([^@]+)@(.*)$/", $_resource, $matches) != 1 || $matches[2] !== $domain) {
 | 
					 | 
				
			||||||
            throw new \Federator\Exceptions\InvalidArgument();
 | 
					            throw new \Federator\Exceptions\InvalidArgument();
 | 
				
			||||||
        }
 | 
					        } 
 | 
				
			||||||
 | 
					        $domain = $matches[2];
 | 
				
			||||||
        $user = \Federator\DIO\User::getUserByName(
 | 
					        $user = \Federator\DIO\User::getUserByName(
 | 
				
			||||||
            $this->main->getDatabase(),
 | 
					            $this->main->getDatabase(),
 | 
				
			||||||
            $matches[1],
 | 
					            $matches[1],
 | 
				
			||||||
            $this->main->getConnector(),
 | 
					            $this->main->getConnectorForHost($domain),
 | 
				
			||||||
            $this->main->getCache()
 | 
					            $this->main->getCache()
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
        if ($user->id == 0) {
 | 
					        if ($user->id == 0) {
 | 
				
			||||||
 | 
					            echo "not found";
 | 
				
			||||||
            throw new \Federator\Exceptions\FileNotFound();
 | 
					            throw new \Federator\Exceptions\FileNotFound();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        $data = [
 | 
					        $data = [
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,6 +13,12 @@ namespace Federator\Connector;
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
interface Connector
 | 
					interface Connector
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * get the host this connector is dedicated to
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @return string
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function getHost();
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * get posts by given user
 | 
					     * get posts by given user
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -773,7 +773,7 @@ class APObject implements \JsonSerializable
 | 
				
			||||||
     * {@inheritDoc}
 | 
					     * {@inheritDoc}
 | 
				
			||||||
     * @see JsonSerializable::jsonSerialize()
 | 
					     * @see JsonSerializable::jsonSerialize()
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function jsonSerialize()
 | 
					    public function jsonSerialize(): mixed
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        return $this->toObject();
 | 
					        return $this->toObject();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -145,7 +145,7 @@ class User
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function hasPermission(string $p)
 | 
					    public function hasPermission(string $p)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        return in_array($p, $this->permissions, false);
 | 
					        return in_array(strtolower($p), array_map('strtolower', $this->permissions), true);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -17,11 +17,11 @@ class Stats
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * get remote stats
 | 
					     * get remote stats
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @param \Federator\Main $main
 | 
					     * @param \Federator\Main $main main instance
 | 
				
			||||||
     *          main instance
 | 
					     * @param \Federator\Connector\Connector $connector connector to use
 | 
				
			||||||
     * @return \Federator\Data\Stats
 | 
					     * @return \Federator\Data\Stats
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public static function getStats($main)
 | 
					    public static function getStats($main, $connector)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $cache = $main->getCache();
 | 
					        $cache = $main->getCache();
 | 
				
			||||||
        // ask cache
 | 
					        // ask cache
 | 
				
			||||||
| 
						 | 
					@ -31,7 +31,6 @@ class Stats
 | 
				
			||||||
                return $stats;
 | 
					                return $stats;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        $connector = $main->getConnector();
 | 
					 | 
				
			||||||
        // ask connector for stats
 | 
					        // ask connector for stats
 | 
				
			||||||
        $stats = $connector->getRemoteStats();
 | 
					        $stats = $connector->getRemoteStats();
 | 
				
			||||||
        if ($cache !== null && $stats !== false) {
 | 
					        if ($cache !== null && $stats !== false) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -145,6 +145,7 @@ class User
 | 
				
			||||||
    public static function getUserByName($dbh, $_name, $connector, $cache)
 | 
					    public static function getUserByName($dbh, $_name, $connector, $cache)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $user = false;
 | 
					        $user = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // ask cache
 | 
					        // ask cache
 | 
				
			||||||
        if ($cache !== null) {
 | 
					        if ($cache !== null) {
 | 
				
			||||||
            $user = $cache->getRemoteUserByName($_name);
 | 
					            $user = $cache->getRemoteUserByName($_name);
 | 
				
			||||||
| 
						 | 
					@ -179,6 +180,7 @@ class User
 | 
				
			||||||
            $stmt->fetch();
 | 
					            $stmt->fetch();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        $stmt->close();
 | 
					        $stmt->close();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if ($user->id === null) {
 | 
					        if ($user->id === null) {
 | 
				
			||||||
            // ask connector for user-id
 | 
					            // ask connector for user-id
 | 
				
			||||||
            $ruser = $connector->getRemoteUserByName($_name);
 | 
					            $ruser = $connector->getRemoteUserByName($_name);
 | 
				
			||||||
| 
						 | 
					@ -190,7 +192,7 @@ class User
 | 
				
			||||||
            if ($user->id === null && $user->externalid !== null) {
 | 
					            if ($user->id === null && $user->externalid !== null) {
 | 
				
			||||||
                self::addLocalUser($dbh, $user, $_name);
 | 
					                self::addLocalUser($dbh, $user, $_name);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            $cache->saveRemoteUserByName($_name, $user);
 | 
					            $cache->saveRemoteUserByName($_name, user: $user);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        return $user;
 | 
					        return $user;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -28,11 +28,11 @@ class Main
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    protected $config;
 | 
					    protected $config;
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * remote connector
 | 
					     * remote connectors
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @var Connector\Connector $connector
 | 
					     * @var array<string, Connector\Connector> $connectors
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    protected $connector = null;
 | 
					    protected $connectors = [];
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * response content type
 | 
					     * response content type
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
| 
						 | 
					@ -93,7 +93,7 @@ class Main
 | 
				
			||||||
    public static function extractFromURI($param, $fallback = '')
 | 
					    public static function extractFromURI($param, $fallback = '')
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $uri = $_SERVER['REQUEST_URI'];
 | 
					        $uri = $_SERVER['REQUEST_URI'];
 | 
				
			||||||
        $params = substr($uri, (int)(strpos($uri, '?') + 1));
 | 
					        $params = substr($uri, (int) (strpos($uri, '?') + 1));
 | 
				
			||||||
        $params = explode('&', $params);
 | 
					        $params = explode('&', $params);
 | 
				
			||||||
        foreach ($params as $p) {
 | 
					        foreach ($params as $p) {
 | 
				
			||||||
            $tokens = explode('=', $p);
 | 
					            $tokens = explode('=', $p);
 | 
				
			||||||
| 
						 | 
					@ -141,18 +141,20 @@ class Main
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * get connector
 | 
					     * Get the connector for a given remote host
 | 
				
			||||||
     *
 | 
					     * 
 | 
				
			||||||
     * @return \Federator\Connector\Connector
 | 
					     * @param string $remoteHost The host from the actor URL (e.g. mastodon.social)
 | 
				
			||||||
 | 
					     * @return Connector\Connector|null
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function getConnector()
 | 
					    public function getConnectorForHost(string $remoteHost): ?Connector\Connector
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        return $this->connector;
 | 
					        $host = strtolower(parse_url($remoteHost, PHP_URL_HOST) ?? $remoteHost);
 | 
				
			||||||
 | 
					        return $this->connectors[$host] ?? null;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * get config
 | 
					     * get config
 | 
				
			||||||
     * @return Array<String, Mixed>
 | 
					     * @return array<string, mixed>
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function getConfig()
 | 
					    public function getConfig()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
| 
						 | 
					@ -172,7 +174,7 @@ class Main
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * load plugins
 | 
					     * load plugins
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function loadPlugins() : void
 | 
					    public function loadPlugins(): void
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        if (array_key_exists('plugins', $this->config)) {
 | 
					        if (array_key_exists('plugins', $this->config)) {
 | 
				
			||||||
            $basepath = $_SERVER['DOCUMENT_ROOT'] . '../plugins/federator/';
 | 
					            $basepath = $_SERVER['DOCUMENT_ROOT'] . '../plugins/federator/';
 | 
				
			||||||
| 
						 | 
					@ -199,8 +201,8 @@ class Main
 | 
				
			||||||
        $dbconf = $this->config["database"];
 | 
					        $dbconf = $this->config["database"];
 | 
				
			||||||
        $this->dbh = new \mysqli(
 | 
					        $this->dbh = new \mysqli(
 | 
				
			||||||
            $dbconf['host'],
 | 
					            $dbconf['host'],
 | 
				
			||||||
            $usernameOverride ?? (string)$dbconf['username'],
 | 
					            $usernameOverride ?? (string) $dbconf['username'],
 | 
				
			||||||
            $passwordOverride ?? (string)$dbconf['password'],
 | 
					            $passwordOverride ?? (string) $dbconf['password'],
 | 
				
			||||||
            $dbconf['database']
 | 
					            $dbconf['database']
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
        if ($this->dbh->connect_error !== null) {
 | 
					        if ($this->dbh->connect_error !== null) {
 | 
				
			||||||
| 
						 | 
					@ -221,7 +223,7 @@ class Main
 | 
				
			||||||
        $smarty = new \Smarty\Smarty();
 | 
					        $smarty = new \Smarty\Smarty();
 | 
				
			||||||
        $root = $_SERVER['DOCUMENT_ROOT'];
 | 
					        $root = $_SERVER['DOCUMENT_ROOT'];
 | 
				
			||||||
        $smarty->setCompileDir($root . $this->config['templates']['compiledir']);
 | 
					        $smarty->setCompileDir($root . $this->config['templates']['compiledir']);
 | 
				
			||||||
        $smarty->setTemplateDir((string)realpath($root . $this->config['templates']['path']));
 | 
					        $smarty->setTemplateDir((string) realpath($root . $this->config['templates']['path']));
 | 
				
			||||||
        $smarty->assign('database', $this->dbh);
 | 
					        $smarty->assign('database', $this->dbh);
 | 
				
			||||||
        $smarty->assign('maininstance', $this);
 | 
					        $smarty->assign('maininstance', $this);
 | 
				
			||||||
        foreach ($data as $key => $value) {
 | 
					        foreach ($data as $key => $value) {
 | 
				
			||||||
| 
						 | 
					@ -233,17 +235,22 @@ class Main
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * set cache
 | 
					     * set cache
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function setCache(Cache\Cache $cache) : void
 | 
					    public function setCache(Cache\Cache $cache): void
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $this->cache = $cache;
 | 
					        $this->cache = $cache;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * set connector
 | 
					     * Set a connector for a specific remote host
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param string $remoteURL The remote host (like mastodon.social or contentnation.net)
 | 
				
			||||||
 | 
					     * @param Connector\Connector $connector The connector instance
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function setConnector(Connector\Connector $connector) : void
 | 
					    public function addConnector(string $remoteURL, Connector\Connector $connector): void
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $this->connector = $connector;
 | 
					        // Normalize the host (no scheme, lowercase)
 | 
				
			||||||
 | 
					        $host = strtolower(parse_url($remoteURL, PHP_URL_HOST) ?? $remoteURL);
 | 
				
			||||||
 | 
					        $this->connectors[$host] = $connector;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
| 
						 | 
					@ -252,7 +259,7 @@ class Main
 | 
				
			||||||
     * @param int $code
 | 
					     * @param int $code
 | 
				
			||||||
     *          new response code
 | 
					     *          new response code
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function setResponseCode(int $code) : void
 | 
					    public function setResponseCode(int $code): void
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $this->responseCode = $code;
 | 
					        $this->responseCode = $code;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -270,7 +277,7 @@ class Main
 | 
				
			||||||
     *          optional parameters
 | 
					     *          optional parameters
 | 
				
			||||||
     * @return string translation
 | 
					     * @return string translation
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public static function translate(?string $lang, string $group, string $key, array $parameters = array()) : string
 | 
					    public static function translate(?string $lang, string $group, string $key, array $parameters = array()): string
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $l = new Language($lang);
 | 
					        $l = new Language($lang);
 | 
				
			||||||
        return $l->printlang($group, $key, $parameters);
 | 
					        return $l->printlang($group, $key, $parameters);
 | 
				
			||||||
| 
						 | 
					@ -281,7 +288,7 @@ class Main
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @param ?string $lang
 | 
					     * @param ?string $lang
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public static function validLanguage(?string $lang) : bool
 | 
					    public static function validLanguage(?string $lang): bool
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $language = new Language($lang);
 | 
					        $language = new Language($lang);
 | 
				
			||||||
        if ($language->getLang() === $lang) {
 | 
					        if ($language->getLang() === $lang) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,11 +6,11 @@
 | 
				
			||||||
 * @author Sascha Nitsch (grumpydeveloper)
 | 
					 * @author Sascha Nitsch (grumpydeveloper)
 | 
				
			||||||
 **/
 | 
					 **/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 namespace Federator\Connector;
 | 
					namespace Federator\Connector;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 /**
 | 
					/**
 | 
				
			||||||
  * Connector to ContentNation.net
 | 
					 * Connector to ContentNation.net
 | 
				
			||||||
  */
 | 
					 */
 | 
				
			||||||
class ContentNation implements Connector
 | 
					class ContentNation implements Connector
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
| 
						 | 
					@ -49,6 +49,15 @@ class ContentNation implements Connector
 | 
				
			||||||
        $this->main = $main;
 | 
					        $this->main = $main;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * get the host this connector is dedicated to
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @return string
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function getHost() {
 | 
				
			||||||
 | 
					        return "contentnation.net";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * get posts by given user
 | 
					     * get posts by given user
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
| 
						 | 
					@ -83,63 +92,66 @@ class ContentNation implements Connector
 | 
				
			||||||
            $userdata = $this->config['userdata']['url'];
 | 
					            $userdata = $this->config['userdata']['url'];
 | 
				
			||||||
            foreach ($activities as $activity) {
 | 
					            foreach ($activities as $activity) {
 | 
				
			||||||
                $create = new \Federator\Data\ActivityPub\Common\Create();
 | 
					                $create = new \Federator\Data\ActivityPub\Common\Create();
 | 
				
			||||||
                $create->setAActor('https://' . $host .'/' . $userId);
 | 
					                $create->setAActor('https://' . $host . '/' . $userId);
 | 
				
			||||||
                $create->setID($activity['id'])
 | 
					                $create->setID($activity['id'])
 | 
				
			||||||
                    ->setPublished($activity['timestamp'])
 | 
					                    ->setPublished($activity['timestamp'])
 | 
				
			||||||
                    ->addTo("https://www.w3.org/ns/activitystreams#Public")
 | 
					                    ->addTo("https://www.w3.org/ns/activitystreams#Public")
 | 
				
			||||||
                    ->addCC('https://' . $host . '/' . $userId . '/followers.json');
 | 
					                    ->addCC('https://' . $host . '/' . $userId . '/followers.json');
 | 
				
			||||||
                switch ($activity['type']) {
 | 
					                switch ($activity['type']) {
 | 
				
			||||||
                    case 'Article':
 | 
					                    case 'Article':
 | 
				
			||||||
                        $create->setURL('https://'.$host . '/' . $activity['language'] . '/' . $userId . '/'
 | 
					                        $create->setURL('https://' . $host . '/' . $activity['language'] . '/' . $userId . '/'
 | 
				
			||||||
                           . $activity['name']);
 | 
					                            . $activity['name']);
 | 
				
			||||||
                        $apArticle = new \Federator\Data\ActivityPub\Common\Article();
 | 
					                        $apArticle = new \Federator\Data\ActivityPub\Common\Article();
 | 
				
			||||||
                        if (array_key_exists('tags', $activity)) {
 | 
					                        if (array_key_exists('tags', $activity)) {
 | 
				
			||||||
                            foreach ($activity['tags'] as $tag) {
 | 
					                            foreach ($activity['tags'] as $tag) {
 | 
				
			||||||
                                $href = 'https://' . $host . '/' . $activity['language']
 | 
					                                $href = 'https://' . $host . '/' . $activity['language']
 | 
				
			||||||
                                  . '/search.htm?tagsearch=' . urlencode($tag);
 | 
					                                    . '/search.htm?tagsearch=' . urlencode($tag);
 | 
				
			||||||
                                $tagObj = new \Federator\Data\ActivityPub\Common\Tag();
 | 
					                                $tagObj = new \Federator\Data\ActivityPub\Common\Tag();
 | 
				
			||||||
                                $tagObj->setHref($href)
 | 
					                                $tagObj->setHref($href)
 | 
				
			||||||
                                  ->setName('#' . urlencode(str_replace(' ', '', $tag)))
 | 
					                                    ->setName('#' . urlencode(str_replace(' ', '', $tag)))
 | 
				
			||||||
                                  ->setType('Hashtag');
 | 
					                                    ->setType('Hashtag');
 | 
				
			||||||
                                $apArticle->addTag($tagObj);
 | 
					                                $apArticle->addTag($tagObj);
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                        $apArticle->setPublished($activity['published'])
 | 
					                        $apArticle->setPublished($activity['published'])
 | 
				
			||||||
                          ->setName($activity['title'])
 | 
					                            ->setName($activity['title'])
 | 
				
			||||||
                          ->setAttributedTo('https://' . $host .'/' . $activity['profilename'])
 | 
					                            ->setAttributedTo('https://' . $host . '/' . $activity['profilename'])
 | 
				
			||||||
                          ->setContent(
 | 
					                            ->setContent(
 | 
				
			||||||
                              $activity['teaser'] ??
 | 
					                                $activity['teaser'] ??
 | 
				
			||||||
                              $this->main->translate(
 | 
					                                $this->main->translate(
 | 
				
			||||||
                                  $activity['language'],
 | 
					                                    $activity['language'],
 | 
				
			||||||
                                  'article',
 | 
					                                    'article',
 | 
				
			||||||
                                  'newarticle'
 | 
					                                    'newarticle'
 | 
				
			||||||
                              )
 | 
					                                )
 | 
				
			||||||
                          )
 | 
					                            )
 | 
				
			||||||
                          ->addTo("https://www.w3.org/ns/activitystreams#Public")
 | 
					                            ->addTo("https://www.w3.org/ns/activitystreams#Public")
 | 
				
			||||||
                          ->addCC('https://' . $host . '/' . $userId . '/followers.json');
 | 
					                            ->addCC('https://' . $host . '/' . $userId . '/followers.json');
 | 
				
			||||||
                        $articleimage = $activity['imagealt'] ??
 | 
					                        $articleimage = $activity['imagealt'] ??
 | 
				
			||||||
                            $this->main->translate($activity['language'], 'article', 'image');
 | 
					                            $this->main->translate($activity['language'], 'article', 'image');
 | 
				
			||||||
                        $idurl = 'https://' . $host . '/' . $activity['language']
 | 
					                        $idurl = 'https://' . $host . '/' . $activity['language']
 | 
				
			||||||
                          . '/' . $userId . '/'. $activity['name'];
 | 
					                            . '/' . $userId . '/' . $activity['name'];
 | 
				
			||||||
                        $apArticle->setID($idurl)
 | 
					                        $apArticle->setID($idurl)
 | 
				
			||||||
                          ->setURL($idurl);
 | 
					                            ->setURL($idurl);
 | 
				
			||||||
                        $image = $activity['image'] ?? $activity['profileimg'];
 | 
					                        $image = $activity['image'] ?? $activity['profileimg'];
 | 
				
			||||||
                        $mediaType = @mime_content_type($imgpath . $activity['profile'] . '/' . $image) | 'text/plain';
 | 
					                        // $mediaType = @mime_content_type($imgpath . $activity['profile'] . '/' . $image) | 'text/plain'; // old approach, using local copy of images
 | 
				
			||||||
 | 
					                        $imgUrl = $userdata . '/' . $activity['profile'] . $image;
 | 
				
			||||||
 | 
					                        $mediaType = $this->getRemoteMimeType($imgUrl) ?? 'text/plain';
 | 
				
			||||||
                        $img = new \Federator\Data\ActivityPub\Common\Image();
 | 
					                        $img = new \Federator\Data\ActivityPub\Common\Image();
 | 
				
			||||||
                        $img->setMediaType($mediaType)
 | 
					                        $img->setMediaType($mediaType)
 | 
				
			||||||
                          ->setName($articleimage)
 | 
					                            ->setName($articleimage)
 | 
				
			||||||
                          ->setURL($userdata . '/' . $activity['profile'] . $image);
 | 
					                            ->setURL($userdata . '/' . $activity['profile'] . $image);
 | 
				
			||||||
                        $apArticle->addImage($img);
 | 
					                        $apArticle->addImage($img);
 | 
				
			||||||
                        $create->setObject($apArticle);
 | 
					                        $create->setObject(object: $apArticle);
 | 
				
			||||||
                        $posts[] = $create;
 | 
					                        $posts[] = $create;
 | 
				
			||||||
                        break; // Article
 | 
					                        break; // Article
 | 
				
			||||||
                    case 'Comment':
 | 
					                    case 'Comment':
 | 
				
			||||||
//                        echo "comment\n";
 | 
					                        $comment = new \Federator\Data\ActivityPub\Common\Activity('Comment');
 | 
				
			||||||
//                        print_r($activity);
 | 
					                        $create->setObject($comment);
 | 
				
			||||||
 | 
					                        $posts[] = $create;
 | 
				
			||||||
                        break; // Comment
 | 
					                        break; // Comment
 | 
				
			||||||
                    case 'Vote':
 | 
					                    case 'Vote':
 | 
				
			||||||
                        $url = 'https://'.$host . '/' . $activity['articlelang'] . $userId . '/'
 | 
					                        $url = 'https://' . $host . '/' . $activity['articlelang'] . $userId . '/'
 | 
				
			||||||
                           . $activity['articlename'];
 | 
					                            . $activity['articlename'];
 | 
				
			||||||
                        $url .= '/vote/' . $activity['id'];
 | 
					                        $url .= '/vote/' . $activity['id'];
 | 
				
			||||||
                        $create->setURL($url);
 | 
					                        $create->setURL($url);
 | 
				
			||||||
                        if ($activity['upvote'] === true) {
 | 
					                        if ($activity['upvote'] === true) {
 | 
				
			||||||
| 
						 | 
					@ -167,7 +179,7 @@ class ContentNation implements Connector
 | 
				
			||||||
                        $actor->setName($activity['username']);
 | 
					                        $actor->setName($activity['username']);
 | 
				
			||||||
                        $like->setActor($actor);
 | 
					                        $like->setActor($actor);
 | 
				
			||||||
                        $url = 'https://' . $host . '/' . $activity['articlelang']
 | 
					                        $url = 'https://' . $host . '/' . $activity['articlelang']
 | 
				
			||||||
                          . '/' . $userId . '/'. $activity['articlename'];
 | 
					                            . '/' . $userId . '/' . $activity['articlename'];
 | 
				
			||||||
                        if ($activity['comment'] !== '') {
 | 
					                        if ($activity['comment'] !== '') {
 | 
				
			||||||
                            $url .= '/comment/' . $activity['comment'];
 | 
					                            $url .= '/comment/' . $activity['comment'];
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
| 
						 | 
					@ -188,6 +200,18 @@ class ContentNation implements Connector
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        return $posts;
 | 
					        return $posts;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Get the MIME type of a remote file by its URL.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param string $_url The URL of the remote file.
 | 
				
			||||||
 | 
					     * @return string|false The MIME type if found, or false on failure.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function getRemoteMimeType($url)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $headers = get_headers($url, 1);
 | 
				
			||||||
 | 
					        return $headers['Content-Type'] ?? 'unknown';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * get statistics from remote system
 | 
					     * get statistics from remote system
 | 
				
			||||||
| 
						 | 
					@ -212,7 +236,7 @@ class ContentNation implements Connector
 | 
				
			||||||
        return $stats;
 | 
					        return $stats;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
   /**
 | 
					    /**
 | 
				
			||||||
     * get remote user by given name
 | 
					     * get remote user by given name
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @param string $_name user/profile name
 | 
					     * @param string $_name user/profile name
 | 
				
			||||||
| 
						 | 
					@ -220,6 +244,9 @@ class ContentNation implements Connector
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function getRemoteUserByName(string $_name)
 | 
					    public function getRemoteUserByName(string $_name)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 | 
					        if (preg_match("#^([^@]+)@([^/]+)$#", $_name, $matches) === 1) {
 | 
				
			||||||
 | 
					            $_name = $matches[1];
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        // validate name
 | 
					        // validate name
 | 
				
			||||||
        if (preg_match("/^[a-zA-Z0-9_\-]+$/", $_name) != 1) {
 | 
					        if (preg_match("/^[a-zA-Z0-9_\-]+$/", $_name) != 1) {
 | 
				
			||||||
            return false;
 | 
					            return false;
 | 
				
			||||||
| 
						 | 
					@ -271,7 +298,7 @@ class ContentNation implements Connector
 | 
				
			||||||
            return false;
 | 
					            return false;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        $r = json_decode($response, true);
 | 
					        $r = json_decode($response, true);
 | 
				
			||||||
        if ($r === false || !is_array($r) ||  !array_key_exists($_user, $r)) {
 | 
					        if ($r === false || !is_array($r) || !array_key_exists($_user, $r)) {
 | 
				
			||||||
            return false;
 | 
					            return false;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        $user = $this->getRemoteUserByName($_user);
 | 
					        $user = $this->getRemoteUserByName($_user);
 | 
				
			||||||
| 
						 | 
					@ -299,5 +326,5 @@ namespace Federator;
 | 
				
			||||||
function contentnation_load($main)
 | 
					function contentnation_load($main)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    $cn = new Connector\ContentNation($main);
 | 
					    $cn = new Connector\ContentNation($main);
 | 
				
			||||||
    $main->setConnector($cn);
 | 
					    $main->addConnector($cn->getHost(), $cn);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -18,6 +18,15 @@ class DummyConnector implements Connector
 | 
				
			||||||
    public function __construct()
 | 
					    public function __construct()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * get the host this connector is dedicated to
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @return string
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function getHost() {
 | 
				
			||||||
 | 
					        return "dummy";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * get posts by given user
 | 
					     * get posts by given user
 | 
				
			||||||
| 
						 | 
					@ -87,5 +96,5 @@ namespace Federator;
 | 
				
			||||||
function dummy_load($main)
 | 
					function dummy_load($main)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    $dummy = new Connector\DummyConnector();
 | 
					    $dummy = new Connector\DummyConnector();
 | 
				
			||||||
    $main->setConnector($dummy);
 | 
					    $main->addConnector($dummy->getHost(), $dummy);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										302
									
								
								plugins/federator/mastodon.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										302
									
								
								plugins/federator/mastodon.php
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,302 @@
 | 
				
			||||||
 | 
					<?php
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
 | 
				
			||||||
 | 
					 * SPDX-License-Identifier: GPL-3.0-or-later
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @author Sascha Nitsch (grumpydeveloper)
 | 
				
			||||||
 | 
					 **/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Federator\Connector;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Connector to Mastodon.social
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					class Mastodon implements Connector
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * config parameter
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @var array<string, mixed> $config
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private $config;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * main instance
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @var \Federator\Main $main
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private $main;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * service-URL
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @var string $service
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private $service;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * constructor
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param \Federator\Main $main
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function __construct($main)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $config = parse_ini_file($_SERVER['DOCUMENT_ROOT'] . '../mastodon.ini', true);
 | 
				
			||||||
 | 
					        if ($config !== false) {
 | 
				
			||||||
 | 
					            $this->config = $config;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        $this->service = $config['mastodon']['service-uri'];
 | 
				
			||||||
 | 
					        $this->main = $main;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * get the host this connector is dedicated to
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @return string
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function getHost() {
 | 
				
			||||||
 | 
					        return "mastodon.social";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * get posts by given user
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param string $userId user id
 | 
				
			||||||
 | 
					     * @param string $min min date
 | 
				
			||||||
 | 
					     * @param string $max max date
 | 
				
			||||||
 | 
					     * @return \Federator\Data\ActivityPub\Common\APObject[]|false
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function getRemotePostsByUser($userId, $min = null, $max = null)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $remoteURL = $this->service . '/users/' . $userId . '/outbox';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $items = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        do {
 | 
				
			||||||
 | 
					            if ($min !== '') {
 | 
				
			||||||
 | 
					                $remoteURL .= '&minTS=' . urlencode($min);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if ($max !== '') {
 | 
				
			||||||
 | 
					                $remoteURL .= '&maxTS=' . urlencode($max);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            // Fetch the current page of items (first or subsequent pages)
 | 
				
			||||||
 | 
					            [$outboxResponse, $outboxInfo] = \Federator\Main::getFromRemote($remoteURL, ['Accept: application/activity+json']);
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            if ($outboxInfo['http_code'] !== 200) {
 | 
				
			||||||
 | 
					                echo "aborting";
 | 
				
			||||||
 | 
					                return false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            $outbox = json_decode($outboxResponse, true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Extract orderedItems from the current page
 | 
				
			||||||
 | 
					            if (isset($outbox['orderedItems'])) {
 | 
				
			||||||
 | 
					                $items = array_merge($items, $outbox['orderedItems']);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Step 4: Use 'last' URL to determine pagination
 | 
				
			||||||
 | 
					            if (isset($outbox['last'])) {
 | 
				
			||||||
 | 
					                // The 'last' URL will usually have a query string that includes min_id for the next set of results
 | 
				
			||||||
 | 
					                $remoteURL = $outbox['last']; // Update to the last URL for the next page of items
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                break; // No more pages, exit pagination
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        } while (!empty($outbox['last'])); // Continue fetching until no 'last' URL
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $items = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Follow `first` page (or get orderedItems directly)
 | 
				
			||||||
 | 
					        if (isset($outbox['orderedItems'])) {
 | 
				
			||||||
 | 
					            $items = $outbox['orderedItems'];
 | 
				
			||||||
 | 
					        } elseif (isset($outbox['first'])) {
 | 
				
			||||||
 | 
					            $firstURL = is_array($outbox['first']) ? $outbox['first']['id'] : $outbox['first'];
 | 
				
			||||||
 | 
					            [$pageResponse, $pageInfo] = \Federator\Main::getFromRemote($firstURL, ['Accept: application/activity+json']);
 | 
				
			||||||
 | 
					            if ($pageInfo['http_code'] !== 200) {
 | 
				
			||||||
 | 
					                return false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            $page = json_decode($pageResponse, true);
 | 
				
			||||||
 | 
					            $items = $page['orderedItems'] ?? [];
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Convert to internal representation
 | 
				
			||||||
 | 
					        $posts = [];
 | 
				
			||||||
 | 
					        foreach ($items as $activity) {
 | 
				
			||||||
 | 
					            if (!isset($activity['type']) || $activity['type'] !== 'Create' || !isset($activity['object'])) {
 | 
				
			||||||
 | 
					                continue; // Skip non-Create activities
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            $obj = $activity['object'];
 | 
				
			||||||
 | 
					            $create = new \Federator\Data\ActivityPub\Common\Create();
 | 
				
			||||||
 | 
					            $create->setID($activity['id'])
 | 
				
			||||||
 | 
					                ->setPublished(strtotime($activity['published'] ?? $obj['published'] ?? 'now'))
 | 
				
			||||||
 | 
					                ->setAActor($activity['actor'])
 | 
				
			||||||
 | 
					                ->addTo("https://www.w3.org/ns/activitystreams#Public");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Handle main Note content
 | 
				
			||||||
 | 
					            if ($obj['type'] === 'Note') {
 | 
				
			||||||
 | 
					                $apNote = new \Federator\Data\ActivityPub\Common\Note();
 | 
				
			||||||
 | 
					                $apNote->setID($obj['id'])
 | 
				
			||||||
 | 
					                    ->setPublished(strtotime($obj['published'] ?? 'now'))
 | 
				
			||||||
 | 
					                    ->setContent($obj['content'] ?? '')
 | 
				
			||||||
 | 
					                    ->setAttributedTo($obj['attributedTo'] ?? $activity['actor'])
 | 
				
			||||||
 | 
					                    ->addTo("https://www.w3.org/ns/activitystreams#Public");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Handle attachments
 | 
				
			||||||
 | 
					                if (!empty($obj['attachment']) && is_array($obj['attachment'])) {
 | 
				
			||||||
 | 
					                    foreach ($obj['attachment'] as $media) {
 | 
				
			||||||
 | 
					                        if (!isset($media['type'], $media['url']))
 | 
				
			||||||
 | 
					                            continue;
 | 
				
			||||||
 | 
					                        $mediaObj = new \Federator\Data\ActivityPub\Common\APObject($media['type']);
 | 
				
			||||||
 | 
					                        $mediaObj->setURL($media['url']);
 | 
				
			||||||
 | 
					                        $apNote->addAttachment($mediaObj);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                $create->setObject($apNote);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            $posts[] = $create;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return $posts;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Get the MIME type of a remote file by its URL.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param string $_url The URL of the remote file.
 | 
				
			||||||
 | 
					     * @return string|false The MIME type if found, or false on failure.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function getRemoteMimeType($url)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $headers = get_headers($url, 1);
 | 
				
			||||||
 | 
					        return $headers['Content-Type'] ?? 'unknown';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * get statistics from remote system
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @return \Federator\Data\Stats|false
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function getRemoteStats()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $remoteURL = $this->service . '/api/stats';
 | 
				
			||||||
 | 
					        [$response, $info] = \Federator\Main::getFromRemote($remoteURL, []);
 | 
				
			||||||
 | 
					        if ($info['http_code'] != 200) {
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        $r = json_decode($response, true);
 | 
				
			||||||
 | 
					        if ($r === false || $r === null || !is_array($r)) {
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        $stats = new \Federator\Data\Stats();
 | 
				
			||||||
 | 
					        $stats->userCount = array_key_exists('userCount', $r) ? $r['userCount'] : 0;
 | 
				
			||||||
 | 
					        $stats->postCount = array_key_exists('pageCount', $r) ? $r['pageCount'] : 0;
 | 
				
			||||||
 | 
					        $stats->commentCount = array_key_exists('commentCount', $r) ? $r['commentCount'] : 0;
 | 
				
			||||||
 | 
					        return $stats;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * get remote user by given name
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param string $_name user/profile name
 | 
				
			||||||
 | 
					     * @return \Federator\Data\User | false
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function getRemoteUserByName(string $_name)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        // Validate username (Mastodon usernames can include @ and domain parts)
 | 
				
			||||||
 | 
					        if (preg_match("/^[a-zA-Z0-9_\-@.]+$/", $_name) !== 1) {
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Mastodon lookup API endpoint
 | 
				
			||||||
 | 
					        $remoteURL = $this->service . '/api/v1/accounts/lookup?acct=' . urlencode($_name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Set headers
 | 
				
			||||||
 | 
					        $headers = ['Accept: application/json'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Fetch data from Mastodon instance
 | 
				
			||||||
 | 
					        [$response, $info] = \Federator\Main::getFromRemote($remoteURL, $headers);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Handle HTTP errors
 | 
				
			||||||
 | 
					        if ($info['http_code'] !== 200) {
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Decode response
 | 
				
			||||||
 | 
					        $r = json_decode($response, true);
 | 
				
			||||||
 | 
					        if ($r === false || $r === null || !is_array($r)) {
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Map response to User object
 | 
				
			||||||
 | 
					        $user = new \Federator\Data\User();
 | 
				
			||||||
 | 
					        $user->externalid = (string) $r['id']; // Mastodon uses numeric IDs
 | 
				
			||||||
 | 
					        $user->iconMediaType = 'image/png'; // Mastodon doesn't explicitly return this, assume PNG
 | 
				
			||||||
 | 
					        $user->iconURL = $r['avatar'] ?? null;
 | 
				
			||||||
 | 
					        $user->imageMediaType = 'image/png';
 | 
				
			||||||
 | 
					        $user->imageURL = $r['header'] ?? null;
 | 
				
			||||||
 | 
					        $user->name = $r['display_name'] ?: $r['username'];
 | 
				
			||||||
 | 
					        $user->summary = $r['note'];
 | 
				
			||||||
 | 
					        $user->type = 'Person'; // Mastodon profiles are ActivityPub "Person" objects
 | 
				
			||||||
 | 
					        $user->registered = strtotime($r['created_at']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return $user;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * get remote user by given session
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param string $_session session id
 | 
				
			||||||
 | 
					     * @param string $_user user or profile name
 | 
				
			||||||
 | 
					     * @return \Federator\Data\User | false
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function getRemoteUserBySession(string $_session, string $_user)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        // validate $_session and $user
 | 
				
			||||||
 | 
					        if (preg_match("/^[a-z0-9]{16}$/", $_session) != 1) {
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (preg_match("/^[a-zA-Z0-9_\-]+$/", $_user) != 1) {
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        $remoteURL = $this->service . '/api/users/permissions?profile=' . urlencode($_user);
 | 
				
			||||||
 | 
					        $headers = ['Cookie: session=' . $_session, 'Accept: application/json'];
 | 
				
			||||||
 | 
					        [$response, $info] = \Federator\Main::getFromRemote($remoteURL, $headers);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if ($info['http_code'] != 200) {
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        $r = json_decode($response, true);
 | 
				
			||||||
 | 
					        if ($r === false || !is_array($r) || !array_key_exists($_user, $r)) {
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        $user = $this->getRemoteUserByName($_user);
 | 
				
			||||||
 | 
					        if ($user === false) {
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        // extend with permissions
 | 
				
			||||||
 | 
					        $user->permissions = [];
 | 
				
			||||||
 | 
					        $user->session = $_session;
 | 
				
			||||||
 | 
					        foreach ($r[$_user] as $p) {
 | 
				
			||||||
 | 
					            $user->permissions[] = $p;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return $user;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Federator;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Function to initialize plugin
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param  \Federator\Main $main main instance
 | 
				
			||||||
 | 
					 * @return void
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					function mastodon_load($main)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    $mast = new Connector\Mastodon($main);
 | 
				
			||||||
 | 
					    $main->addConnector($mast->getHost(), $mast);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -53,6 +53,15 @@ class RedisCache implements Cache
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * get the host this connector is dedicated to
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @return string
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function getHost() {
 | 
				
			||||||
 | 
					        return "redis";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * connect to redis
 | 
					     * connect to redis
 | 
				
			||||||
     * @return void
 | 
					     * @return void
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		
		Reference in a new issue