forked from grumpydevelop/federator
		
	Compare commits
	
		
			No commits in common. "mastodon-support" and "develop" have entirely different histories.
		
	
	
		
			mastodon-s
			...
			develop
		
	
		
					 71 changed files with 302 additions and 5449 deletions
				
			
		
							
								
								
									
										16
									
								
								.gitattributes
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										16
									
								
								.gitattributes
									
										
									
									
										vendored
									
									
								
							| 
						 | 
					@ -1,16 +0,0 @@
 | 
				
			||||||
# Set the default behavior, in case people don't have core.autocrlf set.
 | 
					 | 
				
			||||||
* text=auto
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Explicitly declare text files you want to always be normalized and converted
 | 
					 | 
				
			||||||
# to native line endings on checkout.
 | 
					 | 
				
			||||||
*.c text
 | 
					 | 
				
			||||||
*.h text
 | 
					 | 
				
			||||||
*.php text
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Declare files that will always have LF line endings on checkout.
 | 
					 | 
				
			||||||
*.sln text eol=lf
 | 
					 | 
				
			||||||
*.php text eol.lf
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Denote all files that are truly binary and should not be modified.
 | 
					 | 
				
			||||||
*.png binary
 | 
					 | 
				
			||||||
*.jpg binary
 | 
					 | 
				
			||||||
							
								
								
									
										1
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							| 
						 | 
					@ -5,4 +5,3 @@ php-docs
 | 
				
			||||||
.phpdoc
 | 
					.phpdoc
 | 
				
			||||||
phpdoc
 | 
					phpdoc
 | 
				
			||||||
html
 | 
					html
 | 
				
			||||||
/cache
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -359,7 +359,6 @@ return [
 | 
				
			||||||
    'directory_list' => [
 | 
					    'directory_list' => [
 | 
				
			||||||
        'vendor/phan/phan/src/Phan',
 | 
					        'vendor/phan/phan/src/Phan',
 | 
				
			||||||
        'vendor/smarty/smarty/src',
 | 
					        'vendor/smarty/smarty/src',
 | 
				
			||||||
        'vendor/resque/php-resque/lib',
 | 
					 | 
				
			||||||
        'php/',
 | 
					        'php/',
 | 
				
			||||||
        'plugins',
 | 
					        'plugins',
 | 
				
			||||||
        'htdocs',
 | 
					        'htdocs',
 | 
				
			||||||
| 
						 | 
					@ -368,7 +367,5 @@ return [
 | 
				
			||||||
    // A list of individual files to include in analysis
 | 
					    // A list of individual files to include in analysis
 | 
				
			||||||
    // with a path relative to the root directory of the
 | 
					    // with a path relative to the root directory of the
 | 
				
			||||||
    // project.
 | 
					    // project.
 | 
				
			||||||
    'file_list' => [
 | 
					    'file_list' => [],
 | 
				
			||||||
        'phan-stubs.php',
 | 
					 | 
				
			||||||
    ],
 | 
					 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -35,7 +35,7 @@ If the include redis cache is enabled,
 | 
				
			||||||
- install redis
 | 
					- install redis
 | 
				
			||||||
- create a users.acl for redis with the content:
 | 
					- create a users.acl for redis with the content:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    user federator on ~u_* +get +set  ~s_* +get +setex ~m_* +get +setex ~publickey_* +get +setex >redis*change*password
 | 
					    user federator on ~u_* +get +set  ~s_* +get +setex ~m_* +get +setex >redis*change*password
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- change password in the rediscache.ini to match your given password.
 | 
					- change password in the rediscache.ini to match your given password.
 | 
				
			||||||
- install the redis plugin from pecl if not provided via your distro
 | 
					- install the redis plugin from pecl if not provided via your distro
 | 
				
			||||||
| 
						 | 
					@ -52,11 +52,9 @@ To configure an apache server, add the following rewrite rules:
 | 
				
			||||||
      RewriteCond expr "%{HTTP:content-type} -strcmatch '*application/activity+json*'"
 | 
					      RewriteCond expr "%{HTTP:content-type} -strcmatch '*application/activity+json*'"
 | 
				
			||||||
      RewriteRule ^@(.*)$ /federator.php?_call=fedusers/$1 [L,END]
 | 
					      RewriteRule ^@(.*)$ /federator.php?_call=fedusers/$1 [L,END]
 | 
				
			||||||
      RewriteRule ^users/(.*)$ /federator.php?_call=fedusers/$1 [L,END]
 | 
					      RewriteRule ^users/(.*)$ /federator.php?_call=fedusers/$1 [L,END]
 | 
				
			||||||
      RewriteRule ^inbox[/]?$ /federator.php?_call=fedusers/inbox [L,END]
 | 
					      RewriteRule ^api/federator/(.+)$ federator.php?_call=$1 [L,END]
 | 
				
			||||||
      RewriteRule ^api/federator/(.+)$ /federator.php?_call=$1 [L,END]
 | 
					 | 
				
			||||||
      RewriteRule ^(\.well-known/.*)$ /federator.php?_call=$1 [L,END]
 | 
					      RewriteRule ^(\.well-known/.*)$ /federator.php?_call=$1 [L,END]
 | 
				
			||||||
      RewriteRule ^(nodeinfo/2\.[01])$ /federator.php?_call=$1 [L,END]
 | 
					      RewriteRule ^(nodeinfo/2\.[01])$ /federator.php?_call=$1 [L,END]
 | 
				
			||||||
      RewriteRule ^([a-zA-Z0-9_-]+.*)$ /federator.php?_call=fedusers/$1 [L,END]
 | 
					 | 
				
			||||||
    </Directory>
 | 
					    </Directory>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
change your document root for the domain you want to use (or default one if using localhost) to the directory you installed it, with the /htdocs at the end. A user should only be able to open that file, not the other data.
 | 
					change your document root for the domain you want to use (or default one if using localhost) to the directory you installed it, with the /htdocs at the end. A user should only be able to open that file, not the other data.
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,8 +3,7 @@
 | 
				
			||||||
    "description": "A federation service",
 | 
					    "description": "A federation service",
 | 
				
			||||||
    "type": "project",
 | 
					    "type": "project",
 | 
				
			||||||
    "require": {
 | 
					    "require": {
 | 
				
			||||||
        "smarty/smarty": "^5.3",
 | 
					        "smarty/smarty": "^5.3"
 | 
				
			||||||
        "resque/php-resque": "^1.3.6"
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "license": "GPL-3.0-or-later",
 | 
					    "license": "GPL-3.0-or-later",
 | 
				
			||||||
    "authors": [
 | 
					    "authors": [
 | 
				
			||||||
| 
						 | 
					@ -15,10 +14,5 @@
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
    "require-dev": {
 | 
					    "require-dev": {
 | 
				
			||||||
        "phan/phan": "^5.4"
 | 
					        "phan/phan": "^5.4"
 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "autoload": {
 | 
					 | 
				
			||||||
        "psr-4": {
 | 
					 | 
				
			||||||
            "Federator\\": "php/federator/"
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										13
									
								
								config.ini
									
										
									
									
									
								
							
							
						
						
									
										13
									
								
								config.ini
									
										
									
									
									
								
							| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
[generic]
 | 
					[generic]
 | 
				
			||||||
externaldomain = 'contentnation.net'
 | 
					externaldomain = 'your.fqdn'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[database]
 | 
					[database]
 | 
				
			||||||
host = '127.0.0.1'
 | 
					host = '127.0.0.1'
 | 
				
			||||||
| 
						 | 
					@ -8,18 +8,13 @@ password = '*change*me*'
 | 
				
			||||||
database = 'federator'
 | 
					database = 'federator'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[templates]
 | 
					[templates]
 | 
				
			||||||
path = 'templates/federator/'
 | 
					path = '../templates/federator/'
 | 
				
			||||||
compiledir = 'cache'
 | 
					compiledir = '../cache'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[plugins]
 | 
					[plugins]
 | 
				
			||||||
rediscache = 'rediscache.php'
 | 
					rediscache = 'rediscache.php'
 | 
				
			||||||
# dummy = 'dummyconnector.php'
 | 
					dummy = 'dummyconnector.php'
 | 
				
			||||||
contentnation = 'contentnation.php'
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
[maintenance]
 | 
					[maintenance]
 | 
				
			||||||
username = 'federatoradmin'
 | 
					username = 'federatoradmin'
 | 
				
			||||||
password = '*change*me*as*well'
 | 
					password = '*change*me*as*well'
 | 
				
			||||||
 | 
					 | 
				
			||||||
[keys]
 | 
					 | 
				
			||||||
federatorPrivateKeyPath = 'federator.key'
 | 
					 | 
				
			||||||
federatorPublicKeyPath = 'federator.pub'
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,10 +0,0 @@
 | 
				
			||||||
[contentnation]
 | 
					 | 
				
			||||||
service-uri = http://local.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'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[keys]
 | 
					 | 
				
			||||||
headerSenderName = 'contentnation'
 | 
					 | 
				
			||||||
publicKeyPath = 'contentnation.pub'
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,8 +0,0 @@
 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    "activitypub": {
 | 
					 | 
				
			||||||
        "article": [
 | 
					 | 
				
			||||||
            "localhost",
 | 
					 | 
				
			||||||
            "writefreely.org"
 | 
					 | 
				
			||||||
        ]
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -14,7 +14,6 @@ date_default_timezone_set("Europe/Berlin");
 | 
				
			||||||
spl_autoload_register(static function (string $className) {
 | 
					spl_autoload_register(static function (string $className) {
 | 
				
			||||||
    include '../php/' . str_replace("\\", "/", strtolower($className)) . '.php';
 | 
					    include '../php/' . str_replace("\\", "/", strtolower($className)) . '.php';
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
define('PROJECT_ROOT', dirname(__DIR__, 1));
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// main instance
 | 
					/// main instance
 | 
				
			||||||
$contentnation = new \Federator\Api();
 | 
					$contentnation = new \Federator\Api();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,113 +0,0 @@
 | 
				
			||||||
<!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 overflow-y-auto">
 | 
					 | 
				
			||||||
                <label class="block font-medium">API target link</label>
 | 
					 | 
				
			||||||
                <input type="text" class="target-link-input w-full p-2 border rounded-md mb-2"
 | 
					 | 
				
			||||||
                    placeholder="Enter target link" value="users/grumpydevelop/outbox?page=0">
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <label class="block font-medium">Request type</label>
 | 
					 | 
				
			||||||
                <input type="text" class="request-type-input w-full p-2 border rounded-md mb-2"
 | 
					 | 
				
			||||||
                    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-wrap">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 } : {}),
 | 
					 | 
				
			||||||
                "HTTP_HOST": "localhost",
 | 
					 | 
				
			||||||
            };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            fetch("http://localhost/" + 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 = "users/grumpydevelop@contentnation.net/outbox?page=0";
 | 
					 | 
				
			||||||
            requestBox.querySelector(".request-type-input").value = "GET";
 | 
					 | 
				
			||||||
            requestBox.querySelector(".session-input").value = "";
 | 
					 | 
				
			||||||
            requestBox.querySelector(".profile-input").value = "";
 | 
					 | 
				
			||||||
            requestBox.querySelector(".response").textContent = "Waiting for response";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            requestBox.querySelector(".send-btn").addEventListener("click", function () {
 | 
					 | 
				
			||||||
                sendRequest(this);
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            container.appendChild(requestBox);
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    </script>
 | 
					 | 
				
			||||||
</body>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
</html>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,10 +0,0 @@
 | 
				
			||||||
<?php
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
if (!function_exists('getallheaders')) {
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * @return array<string, string>
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    function getallheaders(): array {
 | 
					 | 
				
			||||||
        return [];
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -6,7 +6,7 @@
 | 
				
			||||||
 * @author Sascha Nitsch (grumpydeveloper)
 | 
					 * @author Sascha Nitsch (grumpydeveloper)
 | 
				
			||||||
 **/
 | 
					 **/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Federator;
 | 
					 namespace Federator;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * main API class
 | 
					 * main API class
 | 
				
			||||||
| 
						 | 
					@ -47,7 +47,7 @@ class Api extends Main
 | 
				
			||||||
    public function __construct()
 | 
					    public function __construct()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $this->contentType = "application/json";
 | 
					        $this->contentType = "application/json";
 | 
				
			||||||
        parent::__construct();
 | 
					        Main::__construct();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
| 
						 | 
					@ -69,12 +69,11 @@ 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();
 | 
				
			||||||
 | 
					 | 
				
			||||||
        $retval = "";
 | 
					        $retval = "";
 | 
				
			||||||
        $handler = null;
 | 
					        $handler = null;
 | 
				
			||||||
        if ($this->connector === null) {
 | 
					        if ($this->connector === null) {
 | 
				
			||||||
| 
						 | 
					@ -101,49 +100,12 @@ class Api extends Main
 | 
				
			||||||
                break;
 | 
					                break;
 | 
				
			||||||
            case 'fedusers':
 | 
					            case 'fedusers':
 | 
				
			||||||
                $handler = new Api\FedUsers($this);
 | 
					                $handler = new Api\FedUsers($this);
 | 
				
			||||||
                $this->setContentType("application/activity+json");
 | 
					 | 
				
			||||||
                break;
 | 
					                break;
 | 
				
			||||||
            case 'v1':
 | 
					            case 'v1':
 | 
				
			||||||
                switch ($this->paths[1]) {
 | 
					                switch ($this->paths[1]) {
 | 
				
			||||||
                    case 'dummy':
 | 
					                    case 'dummy':
 | 
				
			||||||
                        $handler = new Api\V1\Dummy($this);
 | 
					                        $handler = new Api\V1\Dummy($this);
 | 
				
			||||||
                        break;
 | 
					                        break;
 | 
				
			||||||
                    case 'newcontent':
 | 
					 | 
				
			||||||
                        $handler = new Api\V1\NewContent($this);
 | 
					 | 
				
			||||||
                        break;
 | 
					 | 
				
			||||||
                    /* case 'sendFollow': { // hacky implementation for testing purposes
 | 
					 | 
				
			||||||
                        $username = $this->paths[2];
 | 
					 | 
				
			||||||
                        $domain = $this->config['generic']['externaldomain'];
 | 
					 | 
				
			||||||
                        $response = \Federator\DIO\Followers::sendFollowRequest(
 | 
					 | 
				
			||||||
                            $this->dbh,
 | 
					 | 
				
			||||||
                            $this->connector,
 | 
					 | 
				
			||||||
                            $this->cache,
 | 
					 | 
				
			||||||
                            $username,
 | 
					 | 
				
			||||||
                            "admin@mastodon.local",
 | 
					 | 
				
			||||||
                            $domain
 | 
					 | 
				
			||||||
                        );
 | 
					 | 
				
			||||||
                        header("Content-type: " . $this->contentType);
 | 
					 | 
				
			||||||
                        header("Access-Control-Allow-Origin: *");
 | 
					 | 
				
			||||||
                        header("Cache-Control: no-cache, no-store, must-revalidate");
 | 
					 | 
				
			||||||
                        header("Pragma: no-cache");
 | 
					 | 
				
			||||||
                        header("Expires: 0");
 | 
					 | 
				
			||||||
                        if (is_string($response)) {
 | 
					 | 
				
			||||||
                            $this->setResponseCode(200);
 | 
					 | 
				
			||||||
                            $retval = json_encode(array(
 | 
					 | 
				
			||||||
                                "status" => "ok",
 | 
					 | 
				
			||||||
                                "message" => $response
 | 
					 | 
				
			||||||
                            ));
 | 
					 | 
				
			||||||
                        } else {
 | 
					 | 
				
			||||||
                            $this->setResponseCode(500);
 | 
					 | 
				
			||||||
                            $retval = json_encode(array(
 | 
					 | 
				
			||||||
                                "status" => "error",
 | 
					 | 
				
			||||||
                                "message" => "Failed to send follow request"
 | 
					 | 
				
			||||||
                            ));
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                        http_response_code($this->responseCode);
 | 
					 | 
				
			||||||
                        echo $retval;
 | 
					 | 
				
			||||||
                        return;
 | 
					 | 
				
			||||||
                    } */
 | 
					 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                break;
 | 
					                break;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
| 
						 | 
					@ -206,7 +168,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) {
 | 
				
			||||||
| 
						 | 
					@ -229,96 +191,6 @@ class Api extends Main
 | 
				
			||||||
        throw new $exception($message);
 | 
					        throw new $exception($message);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * check if the headers include a valid signature
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param string[] $headers the headers
 | 
					 | 
				
			||||||
     * @throws Exceptions\PermissionDenied
 | 
					 | 
				
			||||||
     * @return string|Exceptions\PermissionDenied
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function checkSignature($headers)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        if (isset($headers['X-Sender'])) {
 | 
					 | 
				
			||||||
            try {
 | 
					 | 
				
			||||||
                return $this->connector->checkSignature($headers);
 | 
					 | 
				
			||||||
            } catch (Exceptions\PermissionDenied $e) {
 | 
					 | 
				
			||||||
                http_response_code(500);
 | 
					 | 
				
			||||||
                throw $e;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $signatureHeader = $headers['Signature'] ?? null;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (!isset($signatureHeader)) {
 | 
					 | 
				
			||||||
            throw new Exceptions\PermissionDenied("Missing Signature header");
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Parse Signature header
 | 
					 | 
				
			||||||
        preg_match_all('/(\w+)=["\']?([^"\',]+)["\']?/', $signatureHeader, $matches);
 | 
					 | 
				
			||||||
        $signatureParts = array_combine($matches[1], $matches[2]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $signature = base64_decode($signatureParts['signature']);
 | 
					 | 
				
			||||||
        $signedHeaders = explode(' ', $signatureParts['headers']);
 | 
					 | 
				
			||||||
        $keyId = $signatureParts['keyId'];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $publicKeyPem = $this->cache->getPublicKey($keyId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (!isset($publicKeyPem) || $publicKeyPem === false) {
 | 
					 | 
				
			||||||
            // Fetch public key from `keyId` (usually actor URL + #main-key)
 | 
					 | 
				
			||||||
            [$publicKeyData, $info] = \Federator\Main::getFromRemote($keyId, ['Accept: application/activity+json']);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if ($info['http_code'] != 200) {
 | 
					 | 
				
			||||||
                throw new Exceptions\PermissionDenied("Failed to fetch public key from keyId: $keyId");
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            $actor = json_decode($publicKeyData, true);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (!is_array($actor) || !isset($actor['id'])) {
 | 
					 | 
				
			||||||
                throw new Exceptions\PermissionDenied("Invalid actor data");
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            $publicKeyPem = $actor['publicKey']['publicKeyPem'] ?? null;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (!isset($publicKeyPem) || $publicKeyPem === false) {
 | 
					 | 
				
			||||||
                http_response_code(500);
 | 
					 | 
				
			||||||
                throw new Exceptions\PermissionDenied("Public key couldn't be determined");
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            // Cache the public key for 1 hour
 | 
					 | 
				
			||||||
            $this->cache->savePublicKey($keyId, $publicKeyPem);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Reconstruct the signed string
 | 
					 | 
				
			||||||
        $signedString = '';
 | 
					 | 
				
			||||||
        foreach ($signedHeaders as $header) {
 | 
					 | 
				
			||||||
            if ($header === '(request-target)') {
 | 
					 | 
				
			||||||
                $method = strtolower($_SERVER['REQUEST_METHOD']);
 | 
					 | 
				
			||||||
                $path = $_SERVER['REQUEST_URI'];
 | 
					 | 
				
			||||||
                $headerValue = "$method $path";
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                $headerValue = $headers[ucwords($header, '-')] ?? '';
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            $signedString .= strtolower($header) . ": " . $headerValue . "\n";
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $signedString = rtrim($signedString);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Verify the signature
 | 
					 | 
				
			||||||
        $pubkeyRes = openssl_pkey_get_public($publicKeyPem);
 | 
					 | 
				
			||||||
        $verified = false;
 | 
					 | 
				
			||||||
        if ($pubkeyRes instanceof \OpenSSLAsymmetricKey && is_string($signature)) {
 | 
					 | 
				
			||||||
            $verified = openssl_verify($signedString, $signature, $pubkeyRes, OPENSSL_ALGO_SHA256);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if ($verified != 1) {
 | 
					 | 
				
			||||||
            http_response_code(500);
 | 
					 | 
				
			||||||
            throw new Exceptions\PermissionDenied("Signature verification failed");
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Signature is valid!
 | 
					 | 
				
			||||||
        return "Signature verified.";
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * remove unwanted elements from html input
 | 
					     * remove unwanted elements from html input
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
| 
						 | 
					@ -326,7 +198,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);
 | 
				
			||||||
| 
						 | 
					@ -340,7 +212,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);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -356,13 +228,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;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,7 +16,7 @@ class FedUsers implements APIInterface
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * main instance
 | 
					     * main instance
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @var \Federator\Api $main
 | 
					     * @var \Federator\Main $main
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    private $main;
 | 
					    private $main;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -49,34 +49,24 @@ class FedUsers implements APIInterface
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $method = $_SERVER["REQUEST_METHOD"];
 | 
					        $method = $_SERVER["REQUEST_METHOD"];
 | 
				
			||||||
        $handler = null;
 | 
					        $handler = null;
 | 
				
			||||||
        $_username = $paths[1];
 | 
					 | 
				
			||||||
        switch (sizeof($paths)) {
 | 
					        switch (sizeof($paths)) {
 | 
				
			||||||
            case 2:
 | 
					            case 2:
 | 
				
			||||||
                if ($method === 'GET') {
 | 
					                if ($method === 'GET') {
 | 
				
			||||||
                    // /users/username or /@username or /username
 | 
					                    // /users/username or /@username
 | 
				
			||||||
                    return $this->returnUserProfile($_username);
 | 
					                    return $this->returnUserProfile($paths[1]);
 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    switch ($paths[1]) {
 | 
					 | 
				
			||||||
                        case 'inbox':
 | 
					 | 
				
			||||||
                            $_username = NULL;
 | 
					 | 
				
			||||||
                            $handler = new FedUsers\Inbox($this->main);
 | 
					 | 
				
			||||||
                            break;
 | 
					 | 
				
			||||||
                        default:
 | 
					 | 
				
			||||||
                            break;
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                break;
 | 
					                break;
 | 
				
			||||||
            case 3:
 | 
					            case 3:
 | 
				
			||||||
                // /users/username/(inbox|outbox|following|followers)
 | 
					                // /users/username/(inbox|outbox|following|followers)
 | 
				
			||||||
                switch ($paths[2]) {
 | 
					                switch ($paths[2]) {
 | 
				
			||||||
                    case 'following':
 | 
					                    case 'following':
 | 
				
			||||||
                        $handler = new FedUsers\Following($this->main);
 | 
					                        // $handler = new FedUsers\Following();
 | 
				
			||||||
                        break;
 | 
					                        break;
 | 
				
			||||||
                    case 'followers':
 | 
					                    case 'followers':
 | 
				
			||||||
                        $handler = new FedUsers\Followers($this->main);
 | 
					                        // $handler = new FedUsers\Followers();
 | 
				
			||||||
                        break;
 | 
					                        break;
 | 
				
			||||||
                    case 'inbox':
 | 
					                    case 'inbox':
 | 
				
			||||||
                        $handler = new FedUsers\Inbox($this->main);
 | 
					                        // $handler = new FedUsers\Inbox();
 | 
				
			||||||
                        break;
 | 
					                        break;
 | 
				
			||||||
                    case 'outbox':
 | 
					                    case 'outbox':
 | 
				
			||||||
                        $handler = new FedUsers\Outbox($this->main);
 | 
					                        $handler = new FedUsers\Outbox($this->main);
 | 
				
			||||||
| 
						 | 
					@ -92,10 +82,10 @@ class FedUsers implements APIInterface
 | 
				
			||||||
            $ret = false;
 | 
					            $ret = false;
 | 
				
			||||||
            switch ($method) {
 | 
					            switch ($method) {
 | 
				
			||||||
                case 'GET':
 | 
					                case 'GET':
 | 
				
			||||||
                    $ret = $handler->get($_username);
 | 
					                    $ret = $handler->get($paths[1]);
 | 
				
			||||||
                    break;
 | 
					                    break;
 | 
				
			||||||
                case 'POST':
 | 
					                case 'POST':
 | 
				
			||||||
                    $ret = $handler->post($_username);
 | 
					                    $ret = $handler->post($paths[1]);
 | 
				
			||||||
                    break;
 | 
					                    break;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            if ($ret !== false) {
 | 
					            if ($ret !== false) {
 | 
				
			||||||
| 
						 | 
					@ -125,24 +115,18 @@ class FedUsers implements APIInterface
 | 
				
			||||||
        if ($user === false || $user->id === null) {
 | 
					        if ($user === false || $user->id === null) {
 | 
				
			||||||
            throw new \Federator\Exceptions\FileNotFound();
 | 
					            throw new \Federator\Exceptions\FileNotFound();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        $config = $this->main->getConfig();
 | 
					 | 
				
			||||||
        $domain = $config['generic']['externaldomain'];
 | 
					 | 
				
			||||||
        $jsonKey = json_encode($user->publicKey);
 | 
					 | 
				
			||||||
        if (!is_string($jsonKey)) {
 | 
					 | 
				
			||||||
            throw new \Federator\Exceptions\FileNotFound();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $data = [
 | 
					        $data = [
 | 
				
			||||||
            'iconMediaType' => $user->iconMediaType,
 | 
					            'iconMediaType' => $user->iconMediaType,
 | 
				
			||||||
            'iconURL' => $user->iconURL,
 | 
					            'iconURL' => $user->iconURL,
 | 
				
			||||||
            'imageMediaType' => $user->imageMediaType,
 | 
					            'imageMediaType' => $user->imageMediaType,
 | 
				
			||||||
            'imageURL' => $user->imageURL,
 | 
					            'imageURL' => $user->imageURL,
 | 
				
			||||||
            'fqdn' => $domain,
 | 
					            'fqdn' => $_SERVER['SERVER_NAME'],
 | 
				
			||||||
            'name' => $user->name,
 | 
					            'name' => $user->name,
 | 
				
			||||||
            'username' => $user->id,
 | 
					            'username' => $user->id,
 | 
				
			||||||
            'publickey' => trim($jsonKey, '"'),
 | 
					            'publickey' => str_replace("\n", "\\n", $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' => ucfirst($user->type) // capitalized user type
 | 
					            'type' => $user->type
 | 
				
			||||||
        ];
 | 
					        ];
 | 
				
			||||||
        $this->response = $this->main->renderTemplate('user.json', $data);
 | 
					        $this->response = $this->main->renderTemplate('user.json', $data);
 | 
				
			||||||
        return true;
 | 
					        return true;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,122 +0,0 @@
 | 
				
			||||||
<?php
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
 | 
					 | 
				
			||||||
 * SPDX-License-Identifier: GPL-3.0-or-later
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * @author Yannis Vogel (vogeldevelopment)
 | 
					 | 
				
			||||||
 **/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace Federator\Api\FedUsers;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * handle activitypub followers requests
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
class Followers implements \Federator\Api\FedUsers\FedUsersInterface
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * main instance
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @var \Federator\Api $main
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    private $main;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * constructor
 | 
					 | 
				
			||||||
     * @param \Federator\Api $main main instance
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function __construct($main)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        $this->main = $main;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * handle get call
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param string|null $_user user to fetch followers for
 | 
					 | 
				
			||||||
     * @return string|false response
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function get($_user)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        if (!isset($_user)) {
 | 
					 | 
				
			||||||
            return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $dbh = $this->main->getDatabase();
 | 
					 | 
				
			||||||
        $cache = $this->main->getCache();
 | 
					 | 
				
			||||||
        $connector = $this->main->getConnector();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // get user
 | 
					 | 
				
			||||||
        $user = \Federator\DIO\User::getUserByName(
 | 
					 | 
				
			||||||
            $dbh,
 | 
					 | 
				
			||||||
            $_user,
 | 
					 | 
				
			||||||
            $connector,
 | 
					 | 
				
			||||||
            $cache
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        if ($user->id === null) {
 | 
					 | 
				
			||||||
            return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $followers = new \Federator\Data\ActivityPub\Common\Followers();
 | 
					 | 
				
			||||||
        $followerItems = \Federator\DIO\Followers::getFollowersByUser($dbh, $user->id, $connector, $cache);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $config = $this->main->getConfig();
 | 
					 | 
				
			||||||
        $domain = $config['generic']['externaldomain'];
 | 
					 | 
				
			||||||
        $baseUrl = 'https://' . $domain . '/' . $_user . '/followers';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $pageSize = 10;
 | 
					 | 
				
			||||||
        $page = $this->main->extractFromURI("page", "");
 | 
					 | 
				
			||||||
        $id = $baseUrl;
 | 
					 | 
				
			||||||
        $items = [];
 | 
					 | 
				
			||||||
        $totalItems = count($followerItems);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if ($page !== "") {
 | 
					 | 
				
			||||||
            $pageNum = max(0, (int) $page);
 | 
					 | 
				
			||||||
            $offset = (int)($pageNum * $pageSize);
 | 
					 | 
				
			||||||
            $pagedItems = array_slice($followerItems, $offset, $pageSize);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            foreach ($pagedItems as $follower) {
 | 
					 | 
				
			||||||
                $items[] = $follower->actorURL;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            $followers->setItems($items);
 | 
					 | 
				
			||||||
            $id .= '?page=' . urlencode($page);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $followers->setID($id);
 | 
					 | 
				
			||||||
        $followers->setPartOf($baseUrl);
 | 
					 | 
				
			||||||
        $followers->setTotalItems($totalItems);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Pagination navigation
 | 
					 | 
				
			||||||
        $lastPage = max(0, ceil($totalItems / $pageSize) - 1);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if ($page === "" || $followers->getCount() == 0) {
 | 
					 | 
				
			||||||
            $followers->setFirst($baseUrl . '?page=0');
 | 
					 | 
				
			||||||
            $followers->setLast($baseUrl . '?page=' . $lastPage);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if ($page !== "") {
 | 
					 | 
				
			||||||
            $pageNum = max(0, (int) $page);
 | 
					 | 
				
			||||||
            if ($pageNum < $lastPage) {
 | 
					 | 
				
			||||||
                $followers->setNext($baseUrl . '?page=' . ($pageNum + 1));
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            if ($pageNum > 0) {
 | 
					 | 
				
			||||||
                $followers->setPrev($baseUrl . '?page=' . ($pageNum - 1));
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            $followers->setFirst($baseUrl . '?page=0');
 | 
					 | 
				
			||||||
            $followers->setLast($baseUrl . '?page=' . $lastPage);
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            $followers->setType('OrderedCollection');
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $obj = $followers->toObject();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return json_encode($obj, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * handle post call
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param string|null $_user user to add data to outbox @unused-param
 | 
					 | 
				
			||||||
     * @return string|false response
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function post($_user)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        return false;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,122 +0,0 @@
 | 
				
			||||||
<?php
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
 | 
					 | 
				
			||||||
 * SPDX-License-Identifier: GPL-3.0-or-later
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * @author Yannis Vogel (vogeldevelopment)
 | 
					 | 
				
			||||||
 **/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace Federator\Api\FedUsers;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * handle activitypub following requests
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
class Following implements \Federator\Api\FedUsers\FedUsersInterface
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * main instance
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @var \Federator\Api $main
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    private $main;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * constructor
 | 
					 | 
				
			||||||
     * @param \Federator\Api $main main instance
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function __construct($main)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        $this->main = $main;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * handle get call
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param string|null $_user user to fetch followers for
 | 
					 | 
				
			||||||
     * @return string|false response
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function get($_user)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        if (!isset($_user)) {
 | 
					 | 
				
			||||||
            return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $dbh = $this->main->getDatabase();
 | 
					 | 
				
			||||||
        $cache = $this->main->getCache();
 | 
					 | 
				
			||||||
        $connector = $this->main->getConnector();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // get user
 | 
					 | 
				
			||||||
        $user = \Federator\DIO\User::getUserByName(
 | 
					 | 
				
			||||||
            $dbh,
 | 
					 | 
				
			||||||
            $_user,
 | 
					 | 
				
			||||||
            $connector,
 | 
					 | 
				
			||||||
            $cache
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        if ($user->id === null) {
 | 
					 | 
				
			||||||
            return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $following = new \Federator\Data\ActivityPub\Common\Following();
 | 
					 | 
				
			||||||
        $followingItems = \Federator\DIO\Followers::getFollowingForUser($dbh, $user->id, $connector, $cache);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $config = $this->main->getConfig();
 | 
					 | 
				
			||||||
        $domain = $config['generic']['externaldomain'];
 | 
					 | 
				
			||||||
        $baseUrl = 'https://' . $domain . '/users/' . $_user . '/following';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $pageSize = 10;
 | 
					 | 
				
			||||||
        $page = $this->main->extractFromURI("page", "");
 | 
					 | 
				
			||||||
        $id = $baseUrl;
 | 
					 | 
				
			||||||
        $items = [];
 | 
					 | 
				
			||||||
        $totalItems = count($followingItems);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if ($page !== "") {
 | 
					 | 
				
			||||||
            $pageNum = max(0, (int) $page);
 | 
					 | 
				
			||||||
            $offset = (int) ($pageNum * $pageSize);
 | 
					 | 
				
			||||||
            $pagedItems = array_slice($followingItems, $offset, $pageSize);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            foreach ($pagedItems as $followed) {
 | 
					 | 
				
			||||||
                $items[] = $followed->actorURL;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            $following->setItems($items);
 | 
					 | 
				
			||||||
            $id .= '?page=' . urlencode($page);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $following->setID($id);
 | 
					 | 
				
			||||||
        $following->setPartOf($baseUrl);
 | 
					 | 
				
			||||||
        $following->setTotalItems($totalItems);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Pagination navigation
 | 
					 | 
				
			||||||
        $lastPage = max(0, ceil($totalItems / $pageSize) - 1);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if ($page === "" || $following->getCount() == 0) {
 | 
					 | 
				
			||||||
            $following->setFirst($baseUrl . '?page=0');
 | 
					 | 
				
			||||||
            $following->setLast($baseUrl . '?page=' . $lastPage);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if ($page !== "") {
 | 
					 | 
				
			||||||
            $pageNum = max(0, (int) $page);
 | 
					 | 
				
			||||||
            if ($pageNum < $lastPage) {
 | 
					 | 
				
			||||||
                $following->setNext($baseUrl . '?page=' . ($pageNum + 1));
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            if ($pageNum > 0) {
 | 
					 | 
				
			||||||
                $following->setPrev($baseUrl . '?page=' . ($pageNum - 1));
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            $following->setFirst($baseUrl . '?page=0');
 | 
					 | 
				
			||||||
            $following->setLast($baseUrl . '?page=' . $lastPage);
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            $following->setType('OrderedCollection');
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $obj = $following->toObject();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return json_encode($obj, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * handle post call
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param string|null $_user user to add data to outbox @unused-param
 | 
					 | 
				
			||||||
     * @return string|false response
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function post($_user)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        return false;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,435 +0,0 @@
 | 
				
			||||||
<?php
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
 | 
					 | 
				
			||||||
 * SPDX-License-Identifier: GPL-3.0-or-later
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * @author Yannis Vogel (vogeldevelopment)
 | 
					 | 
				
			||||||
 **/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace Federator\Api\FedUsers;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * handle activitypub inbox requests
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * main instance
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @var \Federator\Api $main
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    private $main;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * constructor
 | 
					 | 
				
			||||||
     * @param \Federator\Api $main api main instance
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function __construct($main)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        $this->main = $main;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * handle get call
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param string|null $_user user to fetch inbox for @unused-param
 | 
					 | 
				
			||||||
     * @return string|false response
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function get($_user)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        return false;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * handle post call
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param string|null $_user user to add data to inbox
 | 
					 | 
				
			||||||
     * @return string|false response
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function post($_user)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        $_rawInput = file_get_contents('php://input');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $allHeaders = getallheaders();
 | 
					 | 
				
			||||||
        try {
 | 
					 | 
				
			||||||
            $this->main->checkSignature($allHeaders);
 | 
					 | 
				
			||||||
        } catch (\Federator\Exceptions\PermissionDenied $e) {
 | 
					 | 
				
			||||||
            throw new \Federator\Exceptions\Unauthorized("Inbox::post Signature check failed: " . $e->getMessage());
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $activity = is_string($_rawInput) ? json_decode($_rawInput, true) : null;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $dbh = $this->main->getDatabase();
 | 
					 | 
				
			||||||
        $cache = $this->main->getCache();
 | 
					 | 
				
			||||||
        $connector = $this->main->getConnector();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $config = $this->main->getConfig();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (!is_array($activity)) {
 | 
					 | 
				
			||||||
            throw new \Federator\Exceptions\ServerError("Inbox::post Input wasn't of type array");
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $inboxActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($activity);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if ($inboxActivity === false) {
 | 
					 | 
				
			||||||
            throw new \Federator\Exceptions\ServerError("Inbox::post couldn't create inboxActivity");
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $user = $inboxActivity->getAActor(); // url of the sender https://contentnation.net/username
 | 
					 | 
				
			||||||
        $username = basename((string) (parse_url($user, PHP_URL_PATH) ?? ''));
 | 
					 | 
				
			||||||
        $domain = parse_url($user, PHP_URL_HOST);
 | 
					 | 
				
			||||||
        $userId = $username . '@' . $domain;
 | 
					 | 
				
			||||||
        $user = \Federator\DIO\FedUser::getUserByName(
 | 
					 | 
				
			||||||
            $dbh,
 | 
					 | 
				
			||||||
            $userId,
 | 
					 | 
				
			||||||
            $cache
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        if ($user === null || $user->id === null) {
 | 
					 | 
				
			||||||
            error_log("Inbox::post couldn't find user: $userId");
 | 
					 | 
				
			||||||
            throw new \Federator\Exceptions\ServerError("Inbox::post couldn't find user: $userId");
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $users = [];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $receivers = array_merge($inboxActivity->getTo(), $inboxActivity->getCC());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // For Undo, the object may hold the proper to/cc
 | 
					 | 
				
			||||||
        if ($inboxActivity->getType() === 'Undo') {
 | 
					 | 
				
			||||||
            $object = $inboxActivity->getObject();
 | 
					 | 
				
			||||||
            if ($object !== null && is_object($object)) {
 | 
					 | 
				
			||||||
                $receivers = array_merge($object->getTo(), $object->getCC());
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Filter out the public address and keep only actual URLs
 | 
					 | 
				
			||||||
        $receivers = array_filter($receivers, static function (mixed $receiver): bool {
 | 
					 | 
				
			||||||
            return is_string($receiver)
 | 
					 | 
				
			||||||
                && $receiver !== 'https://www.w3.org/ns/activitystreams#Public'
 | 
					 | 
				
			||||||
                && (filter_var($receiver, FILTER_VALIDATE_URL) !== false);
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (isset($_user)) {
 | 
					 | 
				
			||||||
            $receivers[] = $dbh->real_escape_string($_user); // Add the target user to the receivers list
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Special handling for Follow and Undo follow activities
 | 
					 | 
				
			||||||
        if (strtolower($inboxActivity->getType()) === 'follow') {
 | 
					 | 
				
			||||||
            // For Follow, the object should hold the target
 | 
					 | 
				
			||||||
            $object = $inboxActivity->getObject();
 | 
					 | 
				
			||||||
            if ($object !== null && is_string($object)) {
 | 
					 | 
				
			||||||
                $receivers[] = $object;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        } elseif (strtolower($inboxActivity->getType()) === 'undo') {
 | 
					 | 
				
			||||||
            $object = $inboxActivity->getObject();
 | 
					 | 
				
			||||||
            if ($object !== null && is_object($object)) {
 | 
					 | 
				
			||||||
                // For Undo, the objects object should hold the target
 | 
					 | 
				
			||||||
                if (strtolower($object->getType()) === 'follow') {
 | 
					 | 
				
			||||||
                    $objObject = $object->getObject();
 | 
					 | 
				
			||||||
                    if ($objObject !== null && is_string($objObject)) {
 | 
					 | 
				
			||||||
                        $receivers[] = $objObject;
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $ourDomain = $config['generic']['externaldomain'];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        foreach ($receivers as $receiver) {
 | 
					 | 
				
			||||||
            if ($receiver === '' || !is_string($receiver)) {
 | 
					 | 
				
			||||||
                continue;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (str_ends_with($receiver, '/followers')) {
 | 
					 | 
				
			||||||
                $actor = $inboxActivity->getAActor();
 | 
					 | 
				
			||||||
                if ($actor === null || !is_string($actor)) {
 | 
					 | 
				
			||||||
                    error_log("Inbox::post no actor found");
 | 
					 | 
				
			||||||
                    continue;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                // Extract username from the actor URL
 | 
					 | 
				
			||||||
                $username = basename((string) (parse_url($actor, PHP_URL_PATH) ?? ''));
 | 
					 | 
				
			||||||
                $domain = parse_url($actor, PHP_URL_HOST);
 | 
					 | 
				
			||||||
                if ($username === null || $domain === null) {
 | 
					 | 
				
			||||||
                    error_log("Inbox::post no username or domain found for recipient: $receiver");
 | 
					 | 
				
			||||||
                    continue;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                try {
 | 
					 | 
				
			||||||
                    $followers = \Federator\DIO\Followers::getFollowersByFedUser($dbh, $connector, $cache, $username . '@' . $domain);
 | 
					 | 
				
			||||||
                } catch (\Throwable $e) {
 | 
					 | 
				
			||||||
                    error_log("Inbox::post get followers for user: " . $username . '@' . $domain . ". Exception: " . $e->getMessage());
 | 
					 | 
				
			||||||
                    continue;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if (is_array($followers)) {
 | 
					 | 
				
			||||||
                    $users = array_merge($users, array_column($followers, 'id'));
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                // check if receiver is an actor url from our domain
 | 
					 | 
				
			||||||
                if (!str_contains($receiver, $ourDomain) && $receiver !== $_user) {
 | 
					 | 
				
			||||||
                    continue;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                if ($receiver !== $_user) {
 | 
					 | 
				
			||||||
                    $receiverName = basename((string) (parse_url($receiver, PHP_URL_PATH) ?? ''));
 | 
					 | 
				
			||||||
                    $ourDomain = parse_url($receiver, PHP_URL_HOST);
 | 
					 | 
				
			||||||
                    if ($receiverName === null || $ourDomain === null) {
 | 
					 | 
				
			||||||
                        error_log("Inbox::post no receiverName or domain found for receiver: " . $receiver);
 | 
					 | 
				
			||||||
                        continue;
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    $receiver = $receiverName;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                try {
 | 
					 | 
				
			||||||
                    $localUser = \Federator\DIO\User::getUserByName(
 | 
					 | 
				
			||||||
                        $dbh,
 | 
					 | 
				
			||||||
                        $receiver,
 | 
					 | 
				
			||||||
                        $connector,
 | 
					 | 
				
			||||||
                        $cache
 | 
					 | 
				
			||||||
                    );
 | 
					 | 
				
			||||||
                } catch (\Throwable $e) {
 | 
					 | 
				
			||||||
                    error_log("Inbox::post get user by name: " . $receiver . ". Exception: " . $e->getMessage());
 | 
					 | 
				
			||||||
                    continue;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                if ($localUser === null || $localUser->id === null) {
 | 
					 | 
				
			||||||
                    error_log("Inbox::post couldn't find user: $receiver");
 | 
					 | 
				
			||||||
                    continue;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                $users[] = $localUser->id;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $users = array_unique($users); // remove duplicates
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (empty($users)) { // todo remove after proper implementation, debugging for now
 | 
					 | 
				
			||||||
            $rootDir = PROJECT_ROOT . '/';
 | 
					 | 
				
			||||||
            // Save the raw input and parsed JSON to a file for inspection
 | 
					 | 
				
			||||||
            file_put_contents(
 | 
					 | 
				
			||||||
                $rootDir . 'logs/inbox.log',
 | 
					 | 
				
			||||||
                date('Y-m-d H:i:s') . ": ==== POST Inbox Activity ====\n" . json_encode($inboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n",
 | 
					 | 
				
			||||||
                FILE_APPEND
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        foreach ($users as $receiver) {
 | 
					 | 
				
			||||||
            if (!isset($receiver)) {
 | 
					 | 
				
			||||||
                continue;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            $token = \Resque::enqueue('inbox', 'Federator\\Jobs\\InboxJob', [
 | 
					 | 
				
			||||||
                'user' => $user->id,
 | 
					 | 
				
			||||||
                'recipientId' => $receiver,
 | 
					 | 
				
			||||||
                'activity' => $inboxActivity->toObject(),
 | 
					 | 
				
			||||||
            ]);
 | 
					 | 
				
			||||||
            error_log("Inbox::post enqueued job for user: $user->id with token: $token");
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if (empty($users)) {
 | 
					 | 
				
			||||||
            $type = strtolower($inboxActivity->getType());
 | 
					 | 
				
			||||||
            if ($type === 'undo' || $type === 'delete') {
 | 
					 | 
				
			||||||
                $token = \Resque::enqueue('inbox', 'Federator\\Jobs\\InboxJob', [
 | 
					 | 
				
			||||||
                    'user' => $user->id,
 | 
					 | 
				
			||||||
                    'recipientId' => "",
 | 
					 | 
				
			||||||
                    'activity' => $inboxActivity->toObject(),
 | 
					 | 
				
			||||||
                ]);
 | 
					 | 
				
			||||||
                error_log("Inbox::post enqueued job for user: $user->id with token: $token");
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                error_log("Inbox::post no users found for activity, doing nothing: " . json_encode($inboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $connector->sendActivity($user, $inboxActivity);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return "success";
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * handle post call for specific user
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param \mysqli $dbh database handle
 | 
					 | 
				
			||||||
     * @param \Federator\Connector\Connector $connector connector to use
 | 
					 | 
				
			||||||
     * @param \Federator\Cache\Cache|null $cache optional caching service
 | 
					 | 
				
			||||||
     * @param string $_user user that triggered the post
 | 
					 | 
				
			||||||
     * @param string $_recipientId recipient of the post
 | 
					 | 
				
			||||||
     * @param \Federator\Data\ActivityPub\Common\Activity $inboxActivity the activity that we received
 | 
					 | 
				
			||||||
     * @return boolean response
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public static function postForUser($dbh, $connector, $cache, $_user, $_recipientId, $inboxActivity)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        if (!isset($_user)) {
 | 
					 | 
				
			||||||
            error_log("Inbox::postForUser no user given");
 | 
					 | 
				
			||||||
            return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // get sender
 | 
					 | 
				
			||||||
        $user = \Federator\DIO\FedUser::getUserByName(
 | 
					 | 
				
			||||||
            $dbh,
 | 
					 | 
				
			||||||
            $_user,
 | 
					 | 
				
			||||||
            $cache
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        if ($user === null || $user->id === null) {
 | 
					 | 
				
			||||||
            error_log("Inbox::postForUser couldn't find user: $_user");
 | 
					 | 
				
			||||||
            return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $type = strtolower($inboxActivity->getType());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if ($_recipientId === '') {
 | 
					 | 
				
			||||||
            if ($type === 'undo' || $type === 'delete') {
 | 
					 | 
				
			||||||
                switch ($type) {
 | 
					 | 
				
			||||||
                    case 'delete':
 | 
					 | 
				
			||||||
                        // Delete Note/Post
 | 
					 | 
				
			||||||
                        $object = $inboxActivity->getObject();
 | 
					 | 
				
			||||||
                        if (is_string($object)) {
 | 
					 | 
				
			||||||
                            \Federator\DIO\Posts::deletePost($dbh, $object);
 | 
					 | 
				
			||||||
                        } elseif (is_object($object)) {
 | 
					 | 
				
			||||||
                            $objectId = $object->getID();
 | 
					 | 
				
			||||||
                            \Federator\DIO\Posts::deletePost($dbh, $objectId);
 | 
					 | 
				
			||||||
                        } else {
 | 
					 | 
				
			||||||
                            error_log("Inbox::postForUser Error in Delete Post for user $user->id, object is not a string or object");
 | 
					 | 
				
			||||||
                            error_log(" object of type " . gettype($object));
 | 
					 | 
				
			||||||
                            return false;
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                        break;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    case 'undo':
 | 
					 | 
				
			||||||
                        $object = $inboxActivity->getObject();
 | 
					 | 
				
			||||||
                        if (is_object($object)) {
 | 
					 | 
				
			||||||
                            switch (strtolower($object->getType())) {
 | 
					 | 
				
			||||||
                                case 'like':
 | 
					 | 
				
			||||||
                                case 'dislike':
 | 
					 | 
				
			||||||
                                    // Undo Like/Dislike (remove like/dislike)
 | 
					 | 
				
			||||||
                                    $targetId = $object->getID();
 | 
					 | 
				
			||||||
                                    // \Federator\DIO\Votes::removeVote($dbh, $user->id, $targetId, 'dislike');
 | 
					 | 
				
			||||||
                                    \Federator\DIO\Posts::deletePost($dbh, $targetId);
 | 
					 | 
				
			||||||
                                    break;
 | 
					 | 
				
			||||||
                                case 'note':
 | 
					 | 
				
			||||||
                                case 'article':
 | 
					 | 
				
			||||||
                                    // Undo Note (remove note)
 | 
					 | 
				
			||||||
                                    $noteId = $object->getID();
 | 
					 | 
				
			||||||
                                    \Federator\DIO\Posts::deletePost($dbh, $noteId);
 | 
					 | 
				
			||||||
                                    break;
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                        break;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    default:
 | 
					 | 
				
			||||||
                        error_log("Inbox::postForUser Unhandled activity type $type for user $user->id");
 | 
					 | 
				
			||||||
                        break;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                return true;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $atPos = strpos($_recipientId, '@');
 | 
					 | 
				
			||||||
        if ($atPos !== false) {
 | 
					 | 
				
			||||||
            $_recipientId = substr($_recipientId, 0, $atPos);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // get recipient
 | 
					 | 
				
			||||||
        $recipient = \Federator\DIO\User::getUserByName(
 | 
					 | 
				
			||||||
            $dbh,
 | 
					 | 
				
			||||||
            $_recipientId,
 | 
					 | 
				
			||||||
            $connector,
 | 
					 | 
				
			||||||
            $cache
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        if ($recipient === null || $recipient->id === null) {
 | 
					 | 
				
			||||||
            error_log("Inbox::postForUser couldn't find recipient: $_recipientId");
 | 
					 | 
				
			||||||
            return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $rootDir = PROJECT_ROOT . '/';
 | 
					 | 
				
			||||||
        // Save the raw input and parsed JSON to a file for inspection
 | 
					 | 
				
			||||||
        file_put_contents(
 | 
					 | 
				
			||||||
            $rootDir . 'logs/inbox_' . $recipient->id . '.log',
 | 
					 | 
				
			||||||
            date('Y-m-d H:i:s') . ": ==== POST " . $recipient->id . " Inbox Activity ====\n" . json_encode($inboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n",
 | 
					 | 
				
			||||||
            FILE_APPEND
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        switch ($type) {
 | 
					 | 
				
			||||||
            case 'follow':
 | 
					 | 
				
			||||||
                $success = \Federator\DIO\Followers::addExternalFollow($dbh, $inboxActivity->getID(), $user->id, $recipient->id);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if ($success === false) {
 | 
					 | 
				
			||||||
                    error_log("Inbox::postForUser Failed to add follower for user $user->id");
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                break;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            case 'delete':
 | 
					 | 
				
			||||||
                // Delete Note/Post
 | 
					 | 
				
			||||||
                $object = $inboxActivity->getObject();
 | 
					 | 
				
			||||||
                if (is_string($object)) {
 | 
					 | 
				
			||||||
                    \Federator\DIO\Posts::deletePost($dbh, $object);
 | 
					 | 
				
			||||||
                } elseif (is_object($object)) {
 | 
					 | 
				
			||||||
                    $objectId = $object->getID();
 | 
					 | 
				
			||||||
                    \Federator\DIO\Posts::deletePost($dbh, $objectId);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                break;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            case 'undo':
 | 
					 | 
				
			||||||
                $object = $inboxActivity->getObject();
 | 
					 | 
				
			||||||
                if (is_object($object)) {
 | 
					 | 
				
			||||||
                    switch (strtolower($object->getType())) {
 | 
					 | 
				
			||||||
                        case 'follow':
 | 
					 | 
				
			||||||
                            $success = false;
 | 
					 | 
				
			||||||
                            if ($object instanceof \Federator\Data\ActivityPub\Common\Activity) {
 | 
					 | 
				
			||||||
                                $actor = $object->getAActor();
 | 
					 | 
				
			||||||
                                if ($actor !== '') {
 | 
					 | 
				
			||||||
                                    $success = \Federator\DIO\Followers::removeFollow($dbh, $user->id, $recipient->id);
 | 
					 | 
				
			||||||
                                }
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                            if ($success === false) {
 | 
					 | 
				
			||||||
                                error_log("Inbox::postForUser Failed to remove follower for user $user->id");
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                            break;
 | 
					 | 
				
			||||||
                        case 'like':
 | 
					 | 
				
			||||||
                        case 'dislike':
 | 
					 | 
				
			||||||
                            // Undo Like/Dislike (remove like/dislike)
 | 
					 | 
				
			||||||
                            $targetId = $object->getID();
 | 
					 | 
				
			||||||
                            // \Federator\DIO\Votes::removeVote($dbh, $user->id, $targetId, 'dislike');
 | 
					 | 
				
			||||||
                            \Federator\DIO\Posts::deletePost($dbh, $targetId);
 | 
					 | 
				
			||||||
                            break;
 | 
					 | 
				
			||||||
                        case 'note':
 | 
					 | 
				
			||||||
                            // Undo Note (remove note)
 | 
					 | 
				
			||||||
                            $noteId = $object->getID();
 | 
					 | 
				
			||||||
                            \Federator\DIO\Posts::deletePost($dbh, $noteId);
 | 
					 | 
				
			||||||
                            break;
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                break;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            case 'like':
 | 
					 | 
				
			||||||
            case 'dislike':
 | 
					 | 
				
			||||||
                // Add Like/Dislike
 | 
					 | 
				
			||||||
                $targetId = $inboxActivity->getObject();
 | 
					 | 
				
			||||||
                if (is_string($targetId)) {
 | 
					 | 
				
			||||||
                    // \Federator\DIO\Votes::addVote($dbh, $user->id, $targetId, 'dislike');
 | 
					 | 
				
			||||||
                    \Federator\DIO\Posts::savePost($dbh, $user->id, $inboxActivity);
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    error_log("Inbox::postForUser Error in Add Like/Dislike for user $user->id, targetId is not a string");
 | 
					 | 
				
			||||||
                    return false;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                break;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            case 'create':
 | 
					 | 
				
			||||||
            case 'update':
 | 
					 | 
				
			||||||
                $object = $inboxActivity->getObject();
 | 
					 | 
				
			||||||
                if (is_object($object)) {
 | 
					 | 
				
			||||||
                    switch (strtolower($object->getType())) {
 | 
					 | 
				
			||||||
                        case 'note':
 | 
					 | 
				
			||||||
                            \Federator\DIO\Posts::savePost($dbh, $user->id, $inboxActivity);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            break;
 | 
					 | 
				
			||||||
                        case 'article':
 | 
					 | 
				
			||||||
                            \Federator\DIO\Posts::savePost($dbh, $user->id, $inboxActivity);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            break;
 | 
					 | 
				
			||||||
                        default:
 | 
					 | 
				
			||||||
                            \Federator\DIO\Posts::savePost($dbh, $user->id, $inboxActivity);
 | 
					 | 
				
			||||||
                            break;
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                break;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            default:
 | 
					 | 
				
			||||||
                error_log("Inbox::postForUser Unhandled activity type $type for user $user->id");
 | 
					 | 
				
			||||||
                break;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return true;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -16,13 +16,13 @@ class Outbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * main instance
 | 
					     * main instance
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @var \Federator\Api $main
 | 
					     * @var \Federator\Main $main
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    private $main;
 | 
					    private $main;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * constructor
 | 
					     * constructor
 | 
				
			||||||
     * @param \Federator\Api $main main instance
 | 
					     * @param \Federator\Main $main main instance
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function __construct($main)
 | 
					    public function __construct($main)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
| 
						 | 
					@ -32,14 +32,11 @@ class Outbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * handle get call
 | 
					     * handle get call
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @param string|null $_user user to fetch outbox for
 | 
					     * @param string $_user user to fetch outbox for
 | 
				
			||||||
     * @return string|false response
 | 
					     * @return string|false response
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function get($_user)
 | 
					    public function get($_user)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        if (!isset($_user)) {
 | 
					 | 
				
			||||||
            return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $dbh = $this->main->getDatabase();
 | 
					        $dbh = $this->main->getDatabase();
 | 
				
			||||||
        $cache = $this->main->getCache();
 | 
					        $cache = $this->main->getCache();
 | 
				
			||||||
        $connector = $this->main->getConnector();
 | 
					        $connector = $this->main->getConnector();
 | 
				
			||||||
| 
						 | 
					@ -53,45 +50,42 @@ 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 = intval($this->main->extractFromURI('min', '0'), 10);
 | 
					        $min = $this->main->extractFromURI("min", "");
 | 
				
			||||||
        $max = intval($this->main->extractFromURI('max', '0'), 10);
 | 
					        $max = $this->main->extractFromURI("max", "");
 | 
				
			||||||
        $page = $this->main->extractFromURI("page", "");
 | 
					        $page = $this->main->extractFromURI("page", "");
 | 
				
			||||||
        if ($page !== "") {
 | 
					        if ($page !== "") {
 | 
				
			||||||
            $items = \Federator\DIO\Posts::getPostsByUser($dbh, $user->id, $connector, $cache, $min, $max, 20);
 | 
					            $items = \Federator\DIO\Posts::getPostsByUser($dbh, $user->id, $connector, $cache, $min, $max);
 | 
				
			||||||
            $outbox->setItems($items);
 | 
					            $outbox->setItems($items);
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            $tmpitems = \Federator\DIO\Posts::getPostsByUser($dbh, $user->id, $connector, $cache, $min, $max, 99999);
 | 
					 | 
				
			||||||
            $outbox->setTotalItems(sizeof($tmpitems));
 | 
					 | 
				
			||||||
            $items = [];
 | 
					            $items = [];
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        $config = $this->main->getConfig();
 | 
					        $host = $_SERVER['SERVER_NAME'];
 | 
				
			||||||
        $domain = $config['generic']['externaldomain'];
 | 
					        $id = 'https://' . $host .'/' . $_user . '/outbox';
 | 
				
			||||||
        $id = 'https://' . $domain . '/' . $_user . '/outbox';
 | 
					 | 
				
			||||||
        $outbox->setPartOf($id);
 | 
					        $outbox->setPartOf($id);
 | 
				
			||||||
        $outbox->setID($id);
 | 
					        $outbox->setID($id);
 | 
				
			||||||
        if ($page === '') {
 | 
					        if ($page !== '') {
 | 
				
			||||||
            $outbox->setType('OrderedCollection');
 | 
					            $id .= '?page=' . urlencode($page);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if ($page === '' || $outbox->getCount() == 0) {
 | 
					        if ($page === '' || $outbox->count() == 0) {
 | 
				
			||||||
            $outbox->setFirst($id . '?page=true');
 | 
					            $outbox->setFirst($id);
 | 
				
			||||||
 | 
					            $outbox->setLast($id . '&min=0');
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if (sizeof($items) > 0) {
 | 
					        if (sizeof($items)>0) {
 | 
				
			||||||
            $oldestTS = $items[0]->getPublished();
 | 
					            $newestId = $items[0]->getPublished();
 | 
				
			||||||
            $newestTS = $items[sizeof($items) - 1]->getPublished();
 | 
					            $oldestId = $items[sizeof($items)-1]->getPublished();
 | 
				
			||||||
            $outbox->setNext($id . '?page=true&max=' . $newestTS);
 | 
					            $outbox->setNext($id . '&max=' . $newestId);
 | 
				
			||||||
            $outbox->setPrev($id . '?page=true&min=' . $oldestTS);
 | 
					            $outbox->setPrev($id . '&min=' . $oldestId);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        $obj = $outbox->toObject();
 | 
					        $obj = $outbox->toObject();
 | 
				
			||||||
        return json_encode($obj, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
 | 
					        return json_encode($obj);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * handle post call
 | 
					     * handle post call
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @param string|null $_user user to add data to outbox @unused-param
 | 
					     * @param string $_user user to add data to outbox @unused-param
 | 
				
			||||||
     * @return string|false response
 | 
					     * @return string|false response
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function post($_user)
 | 
					    public function post($_user)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -20,7 +20,11 @@ class Dummy implements \Federator\Api\APIInterface
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    private $main;
 | 
					    private $main;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /** @var array<string, string> $message internal message to output */
 | 
					    /**
 | 
				
			||||||
 | 
					     * internal message to output
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @var Array<string, mixed> $message
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
    private $message = [];
 | 
					    private $message = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
| 
						 | 
					@ -37,10 +41,10 @@ class Dummy implements \Federator\Api\APIInterface
 | 
				
			||||||
     * run given url path
 | 
					     * run given url path
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @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
 | 
				
			||||||
     * @return bool true on success
 | 
					     * @return bool true on success
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function exec($paths, $user): bool
 | 
					    public function exec($paths, $user) : 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) {
 | 
				
			||||||
| 
						 | 
					@ -51,23 +55,16 @@ class Dummy implements \Federator\Api\APIInterface
 | 
				
			||||||
            case 'GET':
 | 
					            case 'GET':
 | 
				
			||||||
                switch (sizeof($paths)) {
 | 
					                switch (sizeof($paths)) {
 | 
				
			||||||
                    case 3:
 | 
					                    case 3:
 | 
				
			||||||
                        switch ($paths[2]) {
 | 
					                        if ($paths[2] === 'moo') {
 | 
				
			||||||
                            case 'moo':
 | 
					                            return $this->getDummy();
 | 
				
			||||||
                                return $this->getDummy();
 | 
					 | 
				
			||||||
                            default:
 | 
					 | 
				
			||||||
                                break;
 | 
					 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                        break;
 | 
					 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                break;
 | 
					                break;
 | 
				
			||||||
            case 'POST':
 | 
					            case 'POST':
 | 
				
			||||||
                switch (sizeof($paths)) {
 | 
					                switch (sizeof($paths)) {
 | 
				
			||||||
                    case 3:
 | 
					                    case 3:
 | 
				
			||||||
                        switch ($paths[2]) {
 | 
					                        if ($paths[2] === 'moo') {
 | 
				
			||||||
                            case 'moo':
 | 
					                            return $this->postDummy();
 | 
				
			||||||
                                return $this->postDummy();
 | 
					 | 
				
			||||||
                            default:
 | 
					 | 
				
			||||||
                                break;
 | 
					 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                        break;
 | 
					                        break;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,560 +0,0 @@
 | 
				
			||||||
<?php
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
 | 
					 | 
				
			||||||
 * SPDX-License-Identifier: GPL-3.0-or-later
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * @author Yannis Vogel (vogeldevelopment)
 | 
					 | 
				
			||||||
 **/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace Federator\Api\V1;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * Called from our application to inform us about new content (f.e. new posts on contentnation.net)
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
class NewContent implements \Federator\Api\APIInterface
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * main instance
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @var \Federator\Api $main
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    private $main;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * response from sub-calls
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @var string $response
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    private $response;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * constructor
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param \Federator\Main $main main instance
 | 
					 | 
				
			||||||
     * @return void
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function __construct($main)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        $this->main = $main;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * run given url path
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param array<string> $paths path array split by /
 | 
					 | 
				
			||||||
     * @param \Federator\Data\User|false $user user who is calling us @unused-param
 | 
					 | 
				
			||||||
     * @return bool true on success
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function exec($paths, $user)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        $method = $_SERVER["REQUEST_METHOD"];
 | 
					 | 
				
			||||||
        $_username = $paths[2];
 | 
					 | 
				
			||||||
        if ($method === 'GET') { // unsupported
 | 
					 | 
				
			||||||
            throw new \Federator\Exceptions\InvalidArgument("GET not supported");
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        switch (sizeof($paths)) {
 | 
					 | 
				
			||||||
            case 3:
 | 
					 | 
				
			||||||
                $ret = $this->post($_username);
 | 
					 | 
				
			||||||
                break;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (isset($ret) && $ret !== false) {
 | 
					 | 
				
			||||||
            $this->response = $ret;
 | 
					 | 
				
			||||||
            return true;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $this->main->setResponseCode(404);
 | 
					 | 
				
			||||||
        return false;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * handle post call
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param string|null $_user optional user that triggered the post
 | 
					 | 
				
			||||||
     * @return string|false response
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function post($_user)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        $_rawInput = file_get_contents('php://input');
 | 
					 | 
				
			||||||
        $allHeaders = getallheaders();
 | 
					 | 
				
			||||||
        try {
 | 
					 | 
				
			||||||
            $this->main->checkSignature($allHeaders);
 | 
					 | 
				
			||||||
        } catch (\Federator\Exceptions\PermissionDenied $e) {
 | 
					 | 
				
			||||||
            error_log("NewContent::post Signature check failed: " . $e->getMessage());
 | 
					 | 
				
			||||||
            http_response_code(401);
 | 
					 | 
				
			||||||
            return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $input = is_string($_rawInput) ? json_decode($_rawInput, true) : null;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $dbh = $this->main->getDatabase();
 | 
					 | 
				
			||||||
        $cache = $this->main->getCache();
 | 
					 | 
				
			||||||
        $connector = $this->main->getConnector();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $config = $this->main->getConfig();
 | 
					 | 
				
			||||||
        $domain = $config['generic']['externaldomain'];
 | 
					 | 
				
			||||||
        if (!is_array($input)) {
 | 
					 | 
				
			||||||
            error_log("NewContent::post Input wasn't of type array");
 | 
					 | 
				
			||||||
            return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $articleId = "";
 | 
					 | 
				
			||||||
        if (isset($allHeaders['X-Sender'])) {
 | 
					 | 
				
			||||||
            $newActivity = $connector->jsonToActivity($input, $articleId);
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            error_log("NewContent::post No X-Sender header found");
 | 
					 | 
				
			||||||
            return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if ($newActivity === false) {
 | 
					 | 
				
			||||||
            error_log("NewContent::post couldn't create newActivity");
 | 
					 | 
				
			||||||
            return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if (!isset($_user)) {
 | 
					 | 
				
			||||||
            $user = $newActivity->getAActor(); // url of the sender https://contentnation.net/username
 | 
					 | 
				
			||||||
            $posterName = str_replace(
 | 
					 | 
				
			||||||
                $domain,
 | 
					 | 
				
			||||||
                '',
 | 
					 | 
				
			||||||
                $user
 | 
					 | 
				
			||||||
            ); // retrieve only the last part of the url
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            $posterName = $dbh->real_escape_string($_user);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $users = [];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $receivers = array_merge($newActivity->getTo(), $newActivity->getCC());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // For Undo, the object may hold the proper to/cc
 | 
					 | 
				
			||||||
        if ($newActivity->getType() === 'Undo') {
 | 
					 | 
				
			||||||
            $object = $newActivity->getObject();
 | 
					 | 
				
			||||||
            if ($object !== null && is_object($object)) {
 | 
					 | 
				
			||||||
                $receivers = array_merge($object->getTo(), $object->getCC());
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Filter out the public address and keep only actual URLs
 | 
					 | 
				
			||||||
        $receivers = array_filter($receivers, static function (mixed $receiver): bool {
 | 
					 | 
				
			||||||
            return is_string($receiver)
 | 
					 | 
				
			||||||
                && $receiver !== 'https://www.w3.org/ns/activitystreams#Public'
 | 
					 | 
				
			||||||
                && (filter_var($receiver, FILTER_VALIDATE_URL) !== false);
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        foreach ($receivers as $receiver) {
 | 
					 | 
				
			||||||
            if ($receiver === '' || !is_string($receiver)) {
 | 
					 | 
				
			||||||
                continue;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (str_ends_with($receiver, '/followers')) {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if ($posterName === null) {
 | 
					 | 
				
			||||||
                    error_log("NewContent::post no username found");
 | 
					 | 
				
			||||||
                    continue;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                try {
 | 
					 | 
				
			||||||
                    $followers = \Federator\DIO\Followers::getFollowersByUser($dbh, $posterName, $connector, $cache);
 | 
					 | 
				
			||||||
                } catch (\Throwable $e) {
 | 
					 | 
				
			||||||
                    error_log("NewContent::post get followers for user: " . $posterName . ". Exception: " . $e->getMessage());
 | 
					 | 
				
			||||||
                    continue;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if (is_array($followers)) {
 | 
					 | 
				
			||||||
                    $users = array_merge($users, array_column($followers, 'id'));
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                // check if receiver is an actor url and not from our domain
 | 
					 | 
				
			||||||
                if (str_contains($receiver, $domain)) {
 | 
					 | 
				
			||||||
                    continue;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                $receiverName = basename((string) (parse_url($receiver, PHP_URL_PATH) ?? ''));
 | 
					 | 
				
			||||||
                $domain = parse_url($receiver, PHP_URL_HOST);
 | 
					 | 
				
			||||||
                if ($receiverName === null || $domain === null) {
 | 
					 | 
				
			||||||
                    if ($receiver === $posterName) {
 | 
					 | 
				
			||||||
                        continue;
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    error_log("NewContent::post no receiverName or domain found for receiver: " . $receiver);
 | 
					 | 
				
			||||||
                    continue;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                $receiver = $receiverName . '@' . $domain;
 | 
					 | 
				
			||||||
                try {
 | 
					 | 
				
			||||||
                    $user = \Federator\DIO\FedUser::getUserByName(
 | 
					 | 
				
			||||||
                        $dbh,
 | 
					 | 
				
			||||||
                        $receiver,
 | 
					 | 
				
			||||||
                        $cache
 | 
					 | 
				
			||||||
                    );
 | 
					 | 
				
			||||||
                } catch (\Throwable $e) {
 | 
					 | 
				
			||||||
                    error_log("NewContent::post get user by name: " . $receiver . ". Exception: " . $e->getMessage());
 | 
					 | 
				
			||||||
                    continue;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                if ($user === null || $user->id === null) {
 | 
					 | 
				
			||||||
                    error_log("NewContent::post couldn't find user: $receiver");
 | 
					 | 
				
			||||||
                    continue;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                $users[] = $user->id;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $users = array_unique($users); // remove duplicates
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (empty($users)) { // todo remove after proper implementation, debugging for now
 | 
					 | 
				
			||||||
            $rootDir = PROJECT_ROOT . '/';
 | 
					 | 
				
			||||||
            // Save the raw input and parsed JSON to a file for inspection
 | 
					 | 
				
			||||||
            file_put_contents(
 | 
					 | 
				
			||||||
                $rootDir . 'logs/newContent.log',
 | 
					 | 
				
			||||||
                date('Y-m-d H:i:s') . ": ==== POST NewContent Activity ====\n" . json_encode($newActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n",
 | 
					 | 
				
			||||||
                FILE_APPEND
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        foreach ($users as $receiver) {
 | 
					 | 
				
			||||||
            if (!isset($receiver)) {
 | 
					 | 
				
			||||||
                continue;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            $token = \Resque::enqueue('inbox', 'Federator\\Jobs\\NewContentJob', [
 | 
					 | 
				
			||||||
                'user' => $posterName,
 | 
					 | 
				
			||||||
                'recipientId' => $receiver,
 | 
					 | 
				
			||||||
                'activity' => $newActivity->toObject(),
 | 
					 | 
				
			||||||
                'articleId' => $articleId,
 | 
					 | 
				
			||||||
            ]);
 | 
					 | 
				
			||||||
            error_log("Inbox::post enqueued job for receiver: $receiver with token: $token");
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return json_encode($newActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * handle post call for specific user
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param \mysqli $dbh @unused-param
 | 
					 | 
				
			||||||
     *          database handle
 | 
					 | 
				
			||||||
     * @param \Federator\Connector\Connector $connector
 | 
					 | 
				
			||||||
     *          connector to fetch use with
 | 
					 | 
				
			||||||
     * @param \Federator\Cache\Cache|null $cache
 | 
					 | 
				
			||||||
     *          optional caching service
 | 
					 | 
				
			||||||
     * @param string $host host url of our server (e.g. https://federator.com)
 | 
					 | 
				
			||||||
     * @param string $_user user that triggered the post
 | 
					 | 
				
			||||||
     * @param string $_recipientId recipient of the post
 | 
					 | 
				
			||||||
     * @param \Federator\Data\ActivityPub\Common\Activity $newActivity the activity that we received
 | 
					 | 
				
			||||||
     * @param string $articleId the original id of the article (if applicable)
 | 
					 | 
				
			||||||
     *                           (used to identify the article in the remote system)
 | 
					 | 
				
			||||||
     * @return boolean response
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public static function postForUser($dbh, $connector, $cache, $host, $_user, $_recipientId, $newActivity, $articleId)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        if (!isset($_user)) {
 | 
					 | 
				
			||||||
            error_log("NewContent::postForUser no user given");
 | 
					 | 
				
			||||||
            return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // get sender
 | 
					 | 
				
			||||||
        $user = \Federator\DIO\User::getUserByName(
 | 
					 | 
				
			||||||
            $dbh,
 | 
					 | 
				
			||||||
            $_user,
 | 
					 | 
				
			||||||
            $connector,
 | 
					 | 
				
			||||||
            $cache
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        if ($user === null || $user->id === null) {
 | 
					 | 
				
			||||||
            error_log("NewContent::postForUser couldn't find user: $_user");
 | 
					 | 
				
			||||||
            return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // get recipient
 | 
					 | 
				
			||||||
        $recipient = \Federator\DIO\FedUser::getUserByName(
 | 
					 | 
				
			||||||
            $dbh,
 | 
					 | 
				
			||||||
            $_recipientId,
 | 
					 | 
				
			||||||
            $cache
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        if ($recipient === null || $recipient->id === null) {
 | 
					 | 
				
			||||||
            error_log("NewContent::postForUser couldn't find user: $_recipientId");
 | 
					 | 
				
			||||||
            return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $rootDir = PROJECT_ROOT . '/';
 | 
					 | 
				
			||||||
        // Save the raw input and parsed JSON to a file for inspection
 | 
					 | 
				
			||||||
        file_put_contents(
 | 
					 | 
				
			||||||
            $rootDir . 'logs/newcontent_' . $recipient->id . '.log',
 | 
					 | 
				
			||||||
            date('Y-m-d H:i:s') . ": ==== POST " . $recipient->id . " NewContent Activity ====\n" . json_encode($newActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n",
 | 
					 | 
				
			||||||
            FILE_APPEND
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $type = strtolower($newActivity->getType());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        switch ($type) {
 | 
					 | 
				
			||||||
            case 'follow':
 | 
					 | 
				
			||||||
                // $success = false;
 | 
					 | 
				
			||||||
                $actor = $newActivity->getAActor();
 | 
					 | 
				
			||||||
                if ($actor !== '') {
 | 
					 | 
				
			||||||
                    // $followerUsername = basename((string) (parse_url($actor, PHP_URL_PATH) ?? ''));
 | 
					 | 
				
			||||||
                    // $followerDomain = parse_url($actor, PHP_URL_HOST);
 | 
					 | 
				
			||||||
                    $newIdUrl = \Federator\DIO\Followers::generateNewFollowId($dbh, $host);
 | 
					 | 
				
			||||||
                    $newActivity->setID($newIdUrl);
 | 
					 | 
				
			||||||
                    /* if (is_string($followerDomain)) {
 | 
					 | 
				
			||||||
                        $followerId = "{$followerUsername}@{$followerDomain}";
 | 
					 | 
				
			||||||
                        $success = \Federator\DIO\Followers::sendFollowRequest($dbh, $connector, $cache, $user->id, $followerId, $followerDomain);
 | 
					 | 
				
			||||||
                    } */
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                /* if ($success === false) {
 | 
					 | 
				
			||||||
                    error_log("NewContent::postForUser Failed to add follower for user $user->id");
 | 
					 | 
				
			||||||
                } */
 | 
					 | 
				
			||||||
                break;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            case 'delete':
 | 
					 | 
				
			||||||
                // Delete Note/Post
 | 
					 | 
				
			||||||
                $object = $newActivity->getObject();
 | 
					 | 
				
			||||||
                if (is_string($object)) {
 | 
					 | 
				
			||||||
                    \Federator\DIO\Posts::deletePost($dbh, $object);
 | 
					 | 
				
			||||||
                } elseif (is_object($object)) {
 | 
					 | 
				
			||||||
                    $objectId = $object->getID();
 | 
					 | 
				
			||||||
                    \Federator\DIO\Posts::deletePost($dbh, $objectId);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                break;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            case 'undo':
 | 
					 | 
				
			||||||
                $object = $newActivity->getObject();
 | 
					 | 
				
			||||||
                if (is_object($object)) {
 | 
					 | 
				
			||||||
                    switch (strtolower($object->getType())) {
 | 
					 | 
				
			||||||
                        case 'follow':
 | 
					 | 
				
			||||||
                            $success = false;
 | 
					 | 
				
			||||||
                            if ($object instanceof \Federator\Data\ActivityPub\Common\Activity) {
 | 
					 | 
				
			||||||
                                $actor = $object->getAActor();
 | 
					 | 
				
			||||||
                                if ($actor !== '') {
 | 
					 | 
				
			||||||
                                    $followerUsername = basename((string) (parse_url($actor, PHP_URL_PATH) ?? ''));
 | 
					 | 
				
			||||||
                                    $followerDomain = parse_url($actor, PHP_URL_HOST);
 | 
					 | 
				
			||||||
                                    if (is_string($followerDomain)) {
 | 
					 | 
				
			||||||
                                        $followerId = "{$followerUsername}@{$followerDomain}";
 | 
					 | 
				
			||||||
                                        $removedId = \Federator\DIO\Followers::removeFollow($dbh, $followerId, $user->id);
 | 
					 | 
				
			||||||
                                        if ($removedId !== false) {
 | 
					 | 
				
			||||||
                                            $object->setID($removedId);
 | 
					 | 
				
			||||||
                                            $newActivity->setObject($object);
 | 
					 | 
				
			||||||
                                            $success = true;
 | 
					 | 
				
			||||||
                                        } else {
 | 
					 | 
				
			||||||
                                            error_log("NewContent::postForUser Failed to remove follow for user $user->id");
 | 
					 | 
				
			||||||
                                        }
 | 
					 | 
				
			||||||
                                    }
 | 
					 | 
				
			||||||
                                }
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                            if ($success === false) {
 | 
					 | 
				
			||||||
                                error_log("NewContent::postForUser Failed to remove follower for user $user->id");
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                            break;
 | 
					 | 
				
			||||||
                        case 'like':
 | 
					 | 
				
			||||||
                        case 'dislike':
 | 
					 | 
				
			||||||
                            if (method_exists($object, 'getObject')) {
 | 
					 | 
				
			||||||
                                $targetId = $object->getObject();
 | 
					 | 
				
			||||||
                                if (is_string($targetId)) {
 | 
					 | 
				
			||||||
                                    // \Federator\DIO\Votes::removeVote($dbh, $user->id, $targetId);
 | 
					 | 
				
			||||||
                                    \Federator\DIO\Posts::deletePost($dbh, $targetId);
 | 
					 | 
				
			||||||
                                } else {
 | 
					 | 
				
			||||||
                                    error_log("NewContent::postForUser Error in Undo Like/Dislike for user $user->id, targetId is not a string");
 | 
					 | 
				
			||||||
                                }
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                            break;
 | 
					 | 
				
			||||||
                        case 'note':
 | 
					 | 
				
			||||||
                            // Undo Note (remove note)
 | 
					 | 
				
			||||||
                            $noteId = $object->getID();
 | 
					 | 
				
			||||||
                            \Federator\DIO\Posts::deletePost($dbh, $noteId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            break;
 | 
					 | 
				
			||||||
                        case 'article':
 | 
					 | 
				
			||||||
                            $articleId = $object->getID();
 | 
					 | 
				
			||||||
                            \Federator\DIO\Posts::deletePost($dbh, $articleId);
 | 
					 | 
				
			||||||
                            // also remove latest saved article-update
 | 
					 | 
				
			||||||
                            \Federator\DIO\Posts::deletePost($dbh, $articleId . '#update');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            // Undo Article (remove article)
 | 
					 | 
				
			||||||
                            $idPart = strrchr($recipient->id, '@');
 | 
					 | 
				
			||||||
                            if ($idPart === false) {
 | 
					 | 
				
			||||||
                                error_log("NewContent::postForUser Error in Undo Article. $recipient->id, recipient ID is not valid");
 | 
					 | 
				
			||||||
                                return false;
 | 
					 | 
				
			||||||
                            } else {
 | 
					 | 
				
			||||||
                                $targetUrl = ltrim($idPart, '@');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                                if ($object instanceof \Federator\Data\ActivityPub\Common\Article) {
 | 
					 | 
				
			||||||
                                    $object = \Federator\DIO\Article::conditionalConvertToNote($object, $targetUrl);
 | 
					 | 
				
			||||||
                                    $newActivity->setObject($object);
 | 
					 | 
				
			||||||
                                } else {
 | 
					 | 
				
			||||||
                                    error_log("NewContent::postForUser Error in Undo Article for recipient $recipient->id, object is not an Article");
 | 
					 | 
				
			||||||
                                }
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            break;
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                } else if (is_string($object)) {
 | 
					 | 
				
			||||||
                    \Federator\DIO\Posts::deletePost($dbh, $object);
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    error_log("NewContent::postForUser Error in Undo for recipient $recipient->id, object is not a string or object");
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                break;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            case 'like':
 | 
					 | 
				
			||||||
            case 'dislike':
 | 
					 | 
				
			||||||
                // Add Like/Dislike
 | 
					 | 
				
			||||||
                $targetId = $newActivity->getObject();
 | 
					 | 
				
			||||||
                if (is_string($targetId)) {
 | 
					 | 
				
			||||||
                    // \Federator\DIO\Votes::addVote($dbh, $user->id, $targetId, 'like');
 | 
					 | 
				
			||||||
                    \Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity, $articleId);
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    error_log("NewContent::postForUser Error in Add Like/Dislike for recipient $recipient->id, targetId is not a string");
 | 
					 | 
				
			||||||
                    return false;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                break;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            case 'create':
 | 
					 | 
				
			||||||
            case 'update':
 | 
					 | 
				
			||||||
                $object = $newActivity->getObject();
 | 
					 | 
				
			||||||
                if (is_object($object)) {
 | 
					 | 
				
			||||||
                    switch (strtolower($object->getType())) {
 | 
					 | 
				
			||||||
                        case 'note':
 | 
					 | 
				
			||||||
                            \Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity, $articleId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            break;
 | 
					 | 
				
			||||||
                        case 'article':
 | 
					 | 
				
			||||||
                            \Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity, $articleId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            $idPart = strrchr($recipient->id, '@');
 | 
					 | 
				
			||||||
                            if ($idPart === false) {
 | 
					 | 
				
			||||||
                                error_log("NewContent::postForUser Error in Create/Update Article. $recipient->id, recipient ID is not valid");
 | 
					 | 
				
			||||||
                                return false;
 | 
					 | 
				
			||||||
                            } else {
 | 
					 | 
				
			||||||
                                $targetUrl = ltrim($idPart, '@');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                                if ($object instanceof \Federator\Data\ActivityPub\Common\Article) {
 | 
					 | 
				
			||||||
                                    $object = \Federator\DIO\Article::conditionalConvertToNote($object, $targetUrl);
 | 
					 | 
				
			||||||
                                    $newActivity->setObject($object);
 | 
					 | 
				
			||||||
                                } else {
 | 
					 | 
				
			||||||
                                    error_log("NewContent::postForUser Error in Create/Update Article for recipient $recipient->id, object is not an Article");
 | 
					 | 
				
			||||||
                                }
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            break;
 | 
					 | 
				
			||||||
                        default:
 | 
					 | 
				
			||||||
                            \Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity, $articleId);
 | 
					 | 
				
			||||||
                            break;
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                // Post Note
 | 
					 | 
				
			||||||
                break;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            default:
 | 
					 | 
				
			||||||
                error_log("NewContent::postForUser Unhandled activity type $type for user $user->id");
 | 
					 | 
				
			||||||
                break;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        try {
 | 
					 | 
				
			||||||
            $response = self::sendActivity($dbh, $host, $user, $recipient, $newActivity);
 | 
					 | 
				
			||||||
        } catch (\Exception $e) {
 | 
					 | 
				
			||||||
            error_log("NewContent::postForUser Failed to send activity: " . $e->getMessage());
 | 
					 | 
				
			||||||
            return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if (empty($response)) {
 | 
					 | 
				
			||||||
            error_log("NewContent::postForUser Sent activity to $recipient->id");
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            error_log("NewContent::postForUser Sent activity to $recipient->id with response: " . json_encode($response, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return true;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * send activity to federated server
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param \mysqli $dbh database handle
 | 
					 | 
				
			||||||
     * @param string $host host url of our server (e.g. federator)
 | 
					 | 
				
			||||||
     * @param \Federator\Data\User $sender source user
 | 
					 | 
				
			||||||
     * @param \Federator\Data\FedUser $target federated target user
 | 
					 | 
				
			||||||
     * @param \Federator\Data\ActivityPub\Common\Activity $activity activity to send
 | 
					 | 
				
			||||||
     * @return string|true the generated follow ID on success, false on failure
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public static function sendActivity($dbh, $host, $sender, $target, $activity)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        if ($dbh === false) {
 | 
					 | 
				
			||||||
            throw new \Federator\Exceptions\ServerError("NewContent::sendActivity Failed to get database handle");
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $inboxUrl = $target->inboxURL;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $json = json_encode($activity, JSON_UNESCAPED_SLASHES);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if ($json === false) {
 | 
					 | 
				
			||||||
            throw new \Exception('Failed to encode JSON: ' . json_last_error_msg());
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $digest = 'SHA-256=' . base64_encode(hash('sha256', $json, true));
 | 
					 | 
				
			||||||
        $date = gmdate('D, d M Y H:i:s') . ' GMT';
 | 
					 | 
				
			||||||
        $parsed = parse_url($inboxUrl);
 | 
					 | 
				
			||||||
        if ($parsed === false) {
 | 
					 | 
				
			||||||
            throw new \Exception('Failed to parse URL: ' . $inboxUrl);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (!isset($parsed['host']) || !isset($parsed['path'])) {
 | 
					 | 
				
			||||||
            throw new \Exception('Invalid inbox URL: missing host or path');
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $extHost = $parsed['host'];
 | 
					 | 
				
			||||||
        $path = $parsed['path'];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Build the signature string
 | 
					 | 
				
			||||||
        $signatureString = "(request-target): post {$path}\n" .
 | 
					 | 
				
			||||||
            "host: {$extHost}\n" .
 | 
					 | 
				
			||||||
            "date: {$date}\n" .
 | 
					 | 
				
			||||||
            "digest: {$digest}";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Get rsa private key
 | 
					 | 
				
			||||||
        $privateKey = \Federator\DIO\User::getrsaprivate($dbh, $sender->id); // OR from DB
 | 
					 | 
				
			||||||
        if ($privateKey === false) {
 | 
					 | 
				
			||||||
            throw new \Exception('Failed to get private key');
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $pkeyId = openssl_pkey_get_private($privateKey);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if ($pkeyId === false) {
 | 
					 | 
				
			||||||
            throw new \Exception('Invalid private key');
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        openssl_sign($signatureString, $signature, $pkeyId, OPENSSL_ALGO_SHA256);
 | 
					 | 
				
			||||||
        $signature_b64 = base64_encode($signature);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Build keyId (public key ID from your actor object)
 | 
					 | 
				
			||||||
        $keyId = $host . '/' . $sender->id . '#main-key';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $signatureHeader = 'keyId="' . $keyId . '",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="' . $signature_b64 . '"';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $ch = curl_init($inboxUrl);
 | 
					 | 
				
			||||||
        if ($ch === false) {
 | 
					 | 
				
			||||||
            throw new \Exception('Failed to initialize cURL');
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $headers = [
 | 
					 | 
				
			||||||
            'Host: ' . $extHost,
 | 
					 | 
				
			||||||
            'Date: ' . $date,
 | 
					 | 
				
			||||||
            'Digest: ' . $digest,
 | 
					 | 
				
			||||||
            'Content-Type: application/activity+json',
 | 
					 | 
				
			||||||
            'Signature: ' . $signatureHeader,
 | 
					 | 
				
			||||||
            'Accept: application/activity+json',
 | 
					 | 
				
			||||||
        ];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
 | 
					 | 
				
			||||||
        curl_setopt($ch, CURLOPT_POST, true);
 | 
					 | 
				
			||||||
        curl_setopt($ch, CURLOPT_POSTFIELDS, $json);
 | 
					 | 
				
			||||||
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
 | 
					 | 
				
			||||||
        $response = curl_exec($ch);
 | 
					 | 
				
			||||||
        curl_close($ch);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if ($response === false) {
 | 
					 | 
				
			||||||
            throw new \Exception("Failed to send activity: " . curl_error($ch));
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            $httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
 | 
					 | 
				
			||||||
            if ($httpcode != 200 && $httpcode != 202) {
 | 
					 | 
				
			||||||
                throw new \Exception("Unexpected HTTP code $httpcode: $response");
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return $response;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * get internal represenation as json string
 | 
					 | 
				
			||||||
     * @return string json string or html
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function toJson()
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        return $this->response;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -44,10 +44,8 @@ class WellKnown implements APIInterface
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    private function hostMeta()
 | 
					    private function hostMeta()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $config = $this->main->getConfig();
 | 
					 | 
				
			||||||
        $domain = $config['generic']['externaldomain'];
 | 
					 | 
				
			||||||
        $data = [
 | 
					        $data = [
 | 
				
			||||||
          'fqdn' => $domain
 | 
					          'fqdn' => $_SERVER['SERVER_NAME']
 | 
				
			||||||
        ];
 | 
					        ];
 | 
				
			||||||
        $this->response = $this->main->renderTemplate('host-meta.xml', $data);
 | 
					        $this->response = $this->main->renderTemplate('host-meta.xml', $data);
 | 
				
			||||||
        return true;
 | 
					        return true;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -45,10 +45,8 @@ class NodeInfo
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function exec($paths)
 | 
					    public function exec($paths)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $config = $this->main->getConfig();
 | 
					 | 
				
			||||||
        $domain = $config['generic']['externaldomain'];
 | 
					 | 
				
			||||||
        $data = [
 | 
					        $data = [
 | 
				
			||||||
            'fqdn' => $domain
 | 
					            'fqdn' => $_SERVER['SERVER_NAME']
 | 
				
			||||||
        ];
 | 
					        ];
 | 
				
			||||||
        $template = null;
 | 
					        $template = null;
 | 
				
			||||||
        if (sizeof($paths) == 2 && $paths[0] === '.well-known' && $paths[1] === 'nodeinfo') {
 | 
					        if (sizeof($paths) == 2 && $paths[0] === '.well-known' && $paths[1] === 'nodeinfo') {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -51,7 +51,6 @@ class WebFinger
 | 
				
			||||||
        if (preg_match("/^acct:([^@]+)@(.*)$/", $_resource, $matches) != 1 || $matches[2] !== $domain) {
 | 
					        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],
 | 
				
			||||||
| 
						 | 
					@ -59,7 +58,6 @@ class WebFinger
 | 
				
			||||||
            $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 = [
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										57
									
								
								php/federator/cache/cache.php
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										57
									
								
								php/federator/cache/cache.php
									
										
									
									
										vendored
									
									
								
							| 
						 | 
					@ -13,35 +13,14 @@ namespace Federator\Cache;
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
interface Cache extends \Federator\Connector\Connector
 | 
					interface Cache extends \Federator\Connector\Connector
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * save remote followers of user
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param string $user user name
 | 
					 | 
				
			||||||
     * @param \Federator\Data\FedUser[]|false $followers user followers
 | 
					 | 
				
			||||||
     * @return void
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function saveRemoteFollowersOfUser($user, $followers);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * save remote following for user
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param string $user user name
 | 
					 | 
				
			||||||
     * @param \Federator\Data\FedUser[]|false $following user following
 | 
					 | 
				
			||||||
     * @return void
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function saveRemoteFollowingForUser($user, $following);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * save remote posts by user
 | 
					     * save remote posts by user
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @param string $user user name
 | 
					     * @param string $user user name
 | 
				
			||||||
     * @param int $min min timestamp
 | 
					 | 
				
			||||||
     * @param int $max max timestamp
 | 
					 | 
				
			||||||
     * @param int $limit limit results
 | 
					 | 
				
			||||||
     * @param \Federator\Data\ActivityPub\Common\APObject[]|false $posts user posts
 | 
					     * @param \Federator\Data\ActivityPub\Common\APObject[]|false $posts user posts
 | 
				
			||||||
     * @return void
 | 
					     * @return void
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function saveRemotePostsByUser($user, $min, $max, $limit, $posts);
 | 
					    public function saveRemotePostsByUser($user, $posts);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * save remote stats
 | 
					     * save remote stats
 | 
				
			||||||
| 
						 | 
					@ -60,15 +39,6 @@ interface Cache extends \Federator\Connector\Connector
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function saveRemoteUserByName($_name, $user);
 | 
					    public function saveRemoteUserByName($_name, $user);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * save remote federation user by given name
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param string $_name user/profile name
 | 
					 | 
				
			||||||
     * @param \Federator\Data\FedUser $user user data
 | 
					 | 
				
			||||||
     * @return void
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function saveRemoteFedUserByName(string $_name, \Federator\Data\FedUser $user);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * save remote user by given session
 | 
					     * save remote user by given session
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
| 
						 | 
					@ -78,29 +48,4 @@ interface Cache extends \Federator\Connector\Connector
 | 
				
			||||||
     * @return void
 | 
					     * @return void
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function saveRemoteUserBySession($_session, $_user, $user);
 | 
					    public function saveRemoteUserBySession($_session, $_user, $user);
 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Save the public key for a given keyId
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param string $keyId The keyId (e.g., actor URL + #main-key)
 | 
					 | 
				
			||||||
     * @param string $publicKeyPem The public key PEM to cache
 | 
					 | 
				
			||||||
     * @return void
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function savePublicKey(string $keyId, string $publicKeyPem);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * get remote federation user by given name
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param string $_name user/profile name
 | 
					 | 
				
			||||||
     * @return \Federator\Data\FedUser | false
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function getRemoteFedUserByName(string $_name);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Retrieve the public key for a given keyId
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param string $keyId The keyId (e.g., actor URL + #main-key)
 | 
					 | 
				
			||||||
     * @return string|false The cached public key PEM or false if not found
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function getPublicKey(string $keyId);
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,35 +13,16 @@ namespace Federator\Connector;
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
interface Connector
 | 
					interface Connector
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * get followers of given user
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param string $id user id
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
     * @return \Federator\Data\FedUser[]|false
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function getRemoteFollowersOfUser($id);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * get following of given user
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param string $id user id
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
     * @return \Federator\Data\FedUser[]|false
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function getRemoteFollowingForUser($id);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * get posts by given user
 | 
					     * get posts by given user
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @param string $id user id
 | 
					     * @param string $id user id
 | 
				
			||||||
     * @param int $min min value
 | 
					     * @param string $minId min ID
 | 
				
			||||||
     * @param int $max max value
 | 
					     * @param string $maxId max ID
 | 
				
			||||||
     * @param int $limit maximum number of results
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
     * @return \Federator\Data\ActivityPub\Common\Activity[]|false
 | 
					     * @return \Federator\Data\ActivityPub\Common\APObject[]|false
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function getRemotePostsByUser($id, $min, $max, $limit);
 | 
					    public function getRemotePostsByUser($id, $minId, $maxId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * get remote user by given name
 | 
					     * get remote user by given name
 | 
				
			||||||
| 
						 | 
					@ -66,32 +47,4 @@ interface Connector
 | 
				
			||||||
     * @return \Federator\Data\Stats|false
 | 
					     * @return \Federator\Data\Stats|false
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function getRemoteStats();
 | 
					    public function getRemoteStats();
 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Convert jsonData to Activity format
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param array<string, mixed> $jsonData the json data from our platfrom
 | 
					 | 
				
			||||||
     * @param string $articleId the original id of the article (if applicable)
 | 
					 | 
				
			||||||
     *                           (used to identify the article in the remote system)
 | 
					 | 
				
			||||||
     * @return \Federator\Data\ActivityPub\Common\Activity|false
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function jsonToActivity(array $jsonData, &$articleId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * send target-friendly json from ActivityPub activity
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param \Federator\Data\FedUser $sender the user of the sender
 | 
					 | 
				
			||||||
     * @param \Federator\Data\ActivityPub\Common\Activity $activity the activity
 | 
					 | 
				
			||||||
     * @return boolean did we successfully send the activity?
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function sendActivity($sender, $activity);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * check if the headers include a valid signature
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param string[] $headers the headers
 | 
					 | 
				
			||||||
     * @throws \Federator\Exceptions\PermissionDenied
 | 
					 | 
				
			||||||
     * @return string|\Federator\Exceptions\PermissionDenied
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function checkSignature($headers);
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,41 +0,0 @@
 | 
				
			||||||
<?php
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
 | 
					 | 
				
			||||||
 * SPDX-License-Identifier: GPL-3.0-or-later
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * @author Yannis Vogel (vogeldevelopment)
 | 
					 | 
				
			||||||
 **/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace Federator\Data\ActivityPub\Common;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Undo extends Activity
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    public function __construct()
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        parent::__construct('Undo');
 | 
					 | 
				
			||||||
        parent::addContext('https://www.w3.org/ns/activitystreams');
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * convert internal state to php array
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @return array<string,mixed>
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function toObject()
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        $return = parent::toObject();
 | 
					 | 
				
			||||||
        $return['type'] = 'Undo';
 | 
					 | 
				
			||||||
        return $return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
   /**
 | 
					 | 
				
			||||||
     * create object from json
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param array<string,mixed> $json input json
 | 
					 | 
				
			||||||
     * @return bool true on success
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function fromJson($json)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        return parent::fromJson($json);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,18 +0,0 @@
 | 
				
			||||||
<?php
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
 | 
					 | 
				
			||||||
 * SPDX-License-Identifier: GPL-3.0-or-later
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * @author Yannis Vogel (vogeldevelopment)
 | 
					 | 
				
			||||||
 **/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace Federator\Data\ActivityPub\Common;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Accept extends Activity
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    public function __construct()
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        parent::__construct('Accept');
 | 
					 | 
				
			||||||
        parent::addContext('https://www.w3.org/ns/activitystreams');
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -47,7 +47,7 @@ class Activity extends APObject
 | 
				
			||||||
        return $this;
 | 
					        return $this;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function getAActor(): string
 | 
					    public function getAActor() : string
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        return $this->aactor;
 | 
					        return $this->aactor;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -74,7 +74,6 @@ class Activity extends APObject
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        if (array_key_exists('actor', $json)) {
 | 
					        if (array_key_exists('actor', $json)) {
 | 
				
			||||||
            $this->actor = $json['actor'];
 | 
					            $this->actor = $json['actor'];
 | 
				
			||||||
            $this->aactor = $json['actor'];
 | 
					 | 
				
			||||||
            unset($json['actor']);
 | 
					            unset($json['actor']);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if (!parent::fromJson($json)) {
 | 
					        if (!parent::fromJson($json)) {
 | 
				
			||||||
| 
						 | 
					@ -103,7 +102,7 @@ class Activity extends APObject
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * get Child Object
 | 
					     * get Child Object
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @return APObject|string|null
 | 
					     * @return APObject|null
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function getObject()
 | 
					    public function getObject()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,41 +0,0 @@
 | 
				
			||||||
<?php
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
 | 
					 | 
				
			||||||
 * SPDX-License-Identifier: GPL-3.0-or-later
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * @author Yannis Vogel (vogeldevelopment)
 | 
					 | 
				
			||||||
 **/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace Federator\Data\ActivityPub\Common;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Announce extends Activity
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    public function __construct()
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        parent::__construct('Announce');
 | 
					 | 
				
			||||||
        parent::addContext('https://www.w3.org/ns/activitystreams');
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * convert internal state to php array
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @return array<string,mixed>
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function toObject()
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        $return = parent::toObject();
 | 
					 | 
				
			||||||
        $return['type'] = 'Announce';
 | 
					 | 
				
			||||||
        return $return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
   /**
 | 
					 | 
				
			||||||
     * create object from json
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param array<string,mixed> $json input json
 | 
					 | 
				
			||||||
     * @return bool true on success
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function fromJson($json)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        return parent::fromJson($json);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -27,7 +27,7 @@ class APObject implements \JsonSerializable
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * child object
 | 
					     * child object
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @var APObject|string|null $object
 | 
					     * @var APObject|null $object
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    private $object = null;
 | 
					    private $object = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -347,7 +347,7 @@ class APObject implements \JsonSerializable
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * set child object
 | 
					     * set child object
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @param APObject|string $object
 | 
					     * @param APObject $object
 | 
				
			||||||
     * @return void
 | 
					     * @return void
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function setObject($object)
 | 
					    public function setObject($object)
 | 
				
			||||||
| 
						 | 
					@ -358,7 +358,7 @@ class APObject implements \JsonSerializable
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * get child object
 | 
					     * get child object
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @return APObject|string|null child object
 | 
					     * @return APObject|null child object
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function getObject()
 | 
					    public function getObject()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
| 
						 | 
					@ -376,17 +376,6 @@ class APObject implements \JsonSerializable
 | 
				
			||||||
        $this->summary = $summary;
 | 
					        $this->summary = $summary;
 | 
				
			||||||
        return $this;
 | 
					        return $this;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * get summary
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @return string summary
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function getSummary()
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        return $this->summary;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * set type
 | 
					     * set type
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
| 
						 | 
					@ -470,16 +459,6 @@ class APObject implements \JsonSerializable
 | 
				
			||||||
        return $this;
 | 
					        return $this;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * get name
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @return string name
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function getName() : string
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        return $this->name;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * add Image
 | 
					     * add Image
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
| 
						 | 
					@ -668,7 +647,7 @@ class APObject implements \JsonSerializable
 | 
				
			||||||
        if (array_key_exists('duration', $json)) {
 | 
					        if (array_key_exists('duration', $json)) {
 | 
				
			||||||
            try {
 | 
					            try {
 | 
				
			||||||
                $this->duration = new \DateInterval($json['duration']);
 | 
					                $this->duration = new \DateInterval($json['duration']);
 | 
				
			||||||
            } catch (\Throwable $unused_e) {
 | 
					            } catch (\Exception $unused_e) {
 | 
				
			||||||
                error_log("error parsing duration ". $json['duration']);
 | 
					                error_log("error parsing duration ". $json['duration']);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
| 
						 | 
					@ -771,8 +750,8 @@ class APObject implements \JsonSerializable
 | 
				
			||||||
        if (array_key_exists('mediaType', $json)) {
 | 
					        if (array_key_exists('mediaType', $json)) {
 | 
				
			||||||
            $this->mediaType = $json['mediaType'];
 | 
					            $this->mediaType = $json['mediaType'];
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if (array_key_exists('object', $json)) { // some actPub servers send strings in the object field
 | 
					        if (array_key_exists('object', $json)) {
 | 
				
			||||||
            $this->object = is_array($json['object']) ? \Federator\Data\ActivityPub\Factory::newFromJson($json['object'], "") : $json['object'];
 | 
					            $this->object = \Federator\Data\ActivityPub\Factory::newFromJson($json['object'], "");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if (array_key_exists('sensitive', $json)) {
 | 
					        if (array_key_exists('sensitive', $json)) {
 | 
				
			||||||
            $this->sensitive = $json['sensitive'];
 | 
					            $this->sensitive = $json['sensitive'];
 | 
				
			||||||
| 
						 | 
					@ -794,7 +773,7 @@ class APObject implements \JsonSerializable
 | 
				
			||||||
     * {@inheritDoc}
 | 
					     * {@inheritDoc}
 | 
				
			||||||
     * @see JsonSerializable::jsonSerialize()
 | 
					     * @see JsonSerializable::jsonSerialize()
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function jsonSerialize(): mixed
 | 
					    public function jsonSerialize()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        return $this->toObject();
 | 
					        return $this->toObject();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -896,7 +875,7 @@ class APObject implements \JsonSerializable
 | 
				
			||||||
            $return['tag'] = $tags;
 | 
					            $return['tag'] = $tags;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if ($this->updated > 0) {
 | 
					        if ($this->updated > 0) {
 | 
				
			||||||
            $return['updated'] = gmdate("Y-m-d\TH:i:s\Z", $this->updated);
 | 
					            $return['updated'] = gmdate("Y-m-d\TH:i:S\Z", $this->updated);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if ($this->url !== '') {
 | 
					        if ($this->url !== '') {
 | 
				
			||||||
            $return['url'] = $this->url;
 | 
					            $return['url'] = $this->url;
 | 
				
			||||||
| 
						 | 
					@ -911,7 +890,7 @@ class APObject implements \JsonSerializable
 | 
				
			||||||
            $return['mediaType'] = $this->mediaType;
 | 
					            $return['mediaType'] = $this->mediaType;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if ($this->object !== null) {
 | 
					        if ($this->object !== null) {
 | 
				
			||||||
            $return['object'] = is_string($this->object) ? $this->object : $this->object->toObject();
 | 
					            $return['object'] = $this->object->toObject();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if ($this->atomURI !== '') {
 | 
					        if ($this->atomURI !== '') {
 | 
				
			||||||
            $return['atomUri'] = $this->atomURI;
 | 
					            $return['atomUri'] = $this->atomURI;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -48,48 +48,20 @@ class Collection extends APObject
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function fromJson($json)
 | 
					    public function fromJson($json)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $success = parent::fromJson($json);
 | 
					        return parent::fromJson($json);
 | 
				
			||||||
        if (!$success) {
 | 
					 | 
				
			||||||
            return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if (array_key_exists('totalItems', $json)) {
 | 
					 | 
				
			||||||
            $this->totalItems = $json['totalItems'];
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if (array_key_exists('first', $json)) {
 | 
					 | 
				
			||||||
            $this->first = $json['first'];
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if (array_key_exists('last', $json)) {
 | 
					 | 
				
			||||||
            $this->last = $json['last'];
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return true;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    public function count() : int
 | 
				
			||||||
     * set total items
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param int $totalItems total items
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function setTotalItems(int $totalItems): void
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        $this->totalItems = $totalItems;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public function getTotalItems(): int
 | 
					 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        return $this->totalItems;
 | 
					        return $this->totalItems;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function getFirst(): string
 | 
					    public function setFirst(string $url) : void
 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        return $this->first;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public function setFirst(string $url): void
 | 
					 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $this->first = $url;
 | 
					        $this->first = $url;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function setLast(string $url): void
 | 
					    public function setLast(string $url) : void
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $this->last = $url;
 | 
					        $this->last = $url;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -26,9 +26,7 @@ class Create extends Activity
 | 
				
			||||||
        $return = parent::toObject();
 | 
					        $return = parent::toObject();
 | 
				
			||||||
        $return['type'] = 'Create';
 | 
					        $return['type'] = 'Create';
 | 
				
			||||||
        // overwrite id from url
 | 
					        // overwrite id from url
 | 
				
			||||||
        if ($this->getURL() !== '') {
 | 
					        $return['id'] = $this->getURL();
 | 
				
			||||||
            $return['id'] = $this->getURL();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return $return;
 | 
					        return $return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,36 +0,0 @@
 | 
				
			||||||
<?php
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
 | 
					 | 
				
			||||||
 * SPDX-License-Identifier: GPL-3.0-or-later
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * @author Yannis Vogel (vogeldevelopment)
 | 
					 | 
				
			||||||
 **/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace Federator\Data\ActivityPub\Common;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Delete extends Activity
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    public function __construct()
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        parent::__construct('Delete');
 | 
					 | 
				
			||||||
        parent::addContext('https://www.w3.org/ns/activitystreams');
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * create from json/array
 | 
					 | 
				
			||||||
     * @param mixed $json
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function fromJson($json): bool
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        return parent::fromJson($json);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * convert internal state to php array
 | 
					 | 
				
			||||||
     * @return array<string,mixed>
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function toObject()
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        $return = parent::toObject();
 | 
					 | 
				
			||||||
        return $return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,18 +0,0 @@
 | 
				
			||||||
<?php
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
 | 
					 | 
				
			||||||
 * SPDX-License-Identifier: GPL-3.0-or-later
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * @author Yannis Vogel (vogeldevelopment)
 | 
					 | 
				
			||||||
 **/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace Federator\Data\ActivityPub\Common;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Dislike extends Activity
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    public function __construct()
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        parent::__construct('Dislike');
 | 
					 | 
				
			||||||
        parent::addContext('https://www.w3.org/ns/activitystreams');
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,55 +0,0 @@
 | 
				
			||||||
<?php
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
 | 
					 | 
				
			||||||
 * SPDX-License-Identifier: GPL-3.0-or-later
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * @author Yannis Vogel (vogeldevelopment)
 | 
					 | 
				
			||||||
 **/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace Federator\Data\ActivityPub\Common;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Follow extends Activity
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * object overwrite
 | 
					 | 
				
			||||||
     * @var string
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    private $object = "";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public function setFObject(string $object): void
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        $this->object = $object;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public function getObject(): string
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        return $this->object;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public function __construct()
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        parent::__construct("Follow");
 | 
					 | 
				
			||||||
        parent::addContext('https://www.w3.org/ns/activitystreams');
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public function fromJson($json): bool
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        if (array_key_exists('object', $json)) {
 | 
					 | 
				
			||||||
            $this->object = $json['object'];
 | 
					 | 
				
			||||||
            unset($json['object']);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return parent::fromJson($json);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * convert internal state to php array
 | 
					 | 
				
			||||||
     * @return array<string,mixed>
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function toObject()
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        $return = parent::toObject();
 | 
					 | 
				
			||||||
        if ($this->object !== "") {
 | 
					 | 
				
			||||||
            $return['object'] = $this->object;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return $return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,53 +0,0 @@
 | 
				
			||||||
<?php
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
 | 
					 | 
				
			||||||
 * SPDX-License-Identifier: GPL-3.0-or-later
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * @author Yannis Vogel (vogeldevelopment)
 | 
					 | 
				
			||||||
 **/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace Federator\Data\ActivityPub\Common;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Followers extends OrderedCollectionPage
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    public function __construct()
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        parent::__construct();
 | 
					 | 
				
			||||||
        parent::addContext('https://www.w3.org/ns/activitystreams');
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * set items
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param string[] $items the items in the collection
 | 
					 | 
				
			||||||
     * @return void
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function setItems(&$items)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        // Optionally: type check that all $items are Activity objects
 | 
					 | 
				
			||||||
        $this->orderedItems = $items;
 | 
					 | 
				
			||||||
        $this->totalItems = sizeof($items);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * convert internal state to php array
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @return array<string,mixed>
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function toObject()
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        $return = parent::toObject();
 | 
					 | 
				
			||||||
        return $return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * create object from json
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param array<string,mixed> $json input json
 | 
					 | 
				
			||||||
     * @return bool true on success
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function fromJson($json)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        return parent::fromJson($json);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,53 +0,0 @@
 | 
				
			||||||
<?php
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
 | 
					 | 
				
			||||||
 * SPDX-License-Identifier: GPL-3.0-or-later
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * @author Yannis Vogel (vogeldevelopment)
 | 
					 | 
				
			||||||
 **/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace Federator\Data\ActivityPub\Common;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Following extends OrderedCollectionPage
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    public function __construct()
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        parent::__construct();
 | 
					 | 
				
			||||||
        parent::addContext('https://www.w3.org/ns/activitystreams');
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * set items
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param string[] $items the items in the collection
 | 
					 | 
				
			||||||
     * @return void
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function setItems(&$items)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        // Optionally: type check that all $items are Activity objects
 | 
					 | 
				
			||||||
        $this->orderedItems = $items;
 | 
					 | 
				
			||||||
        $this->totalItems = sizeof($items);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * convert internal state to php array
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @return array<string,mixed>
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function toObject()
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        $return = parent::toObject();
 | 
					 | 
				
			||||||
        return $return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * create object from json
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param array<string,mixed> $json input json
 | 
					 | 
				
			||||||
     * @return bool true on success
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function fromJson($json)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        return parent::fromJson($json);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,50 +0,0 @@
 | 
				
			||||||
<?php
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
 | 
					 | 
				
			||||||
 * SPDX-License-Identifier: GPL-3.0-or-later
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * @author Yannis Vogel (vogeldevelopment)
 | 
					 | 
				
			||||||
 **/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace Federator\Data\ActivityPub\Common;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Inbox extends OrderedCollectionPage
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    public function __construct()
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        parent::__construct();
 | 
					 | 
				
			||||||
        parent::addContext('https://www.w3.org/ns/activitystreams');
 | 
					 | 
				
			||||||
        parent::addContexts([
 | 
					 | 
				
			||||||
            "ostatus" => "http://ostatus.org#",
 | 
					 | 
				
			||||||
            "atomUri" => "ostatus:atomUri",
 | 
					 | 
				
			||||||
            "inReplyToAtomUri" => "ostatus:inReplyToAtomUri",
 | 
					 | 
				
			||||||
            "conversation" => "ostatus:conversation",
 | 
					 | 
				
			||||||
            "sensitive" => "as:sensitive",
 | 
					 | 
				
			||||||
            "toot" => "http://joinmastodon.org/ns#",
 | 
					 | 
				
			||||||
            "votersCount" => "toot:votersCount",
 | 
					 | 
				
			||||||
            "Hashtag" => "as:Hashtag"
 | 
					 | 
				
			||||||
        ]);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * convert internal state to php array
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @return array<string,mixed>
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function toObject()
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        $return = parent::toObject();
 | 
					 | 
				
			||||||
        return $return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * create object from json
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param array<string,mixed> $json input json
 | 
					 | 
				
			||||||
     * @return bool true on success
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function fromJson($json)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        return parent::fromJson($json);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,18 +0,0 @@
 | 
				
			||||||
<?php
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
 | 
					 | 
				
			||||||
 * SPDX-License-Identifier: GPL-3.0-or-later
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * @author Yannis Vogel (vogeldevelopment)
 | 
					 | 
				
			||||||
 **/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace Federator\Data\ActivityPub\Common;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Like extends Activity
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    public function __construct()
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        parent::__construct('Like');
 | 
					 | 
				
			||||||
        parent::addContext('https://www.w3.org/ns/activitystreams');
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -13,9 +13,9 @@ class OrderedCollection extends Collection
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * nested items
 | 
					     * nested items
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @var APObject[]|string[]
 | 
					     * @var APObject[]
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    protected $orderedItems = [];
 | 
					    protected $items = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function __construct()
 | 
					    public function __construct()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
| 
						 | 
					@ -32,13 +32,9 @@ class OrderedCollection extends Collection
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $return = parent::toObject();
 | 
					        $return = parent::toObject();
 | 
				
			||||||
        $return['type'] = 'OrderedCollection';
 | 
					        $return['type'] = 'OrderedCollection';
 | 
				
			||||||
        if (sizeof($this->orderedItems) > 0) {
 | 
					        if ($this->totalItems > 0) {
 | 
				
			||||||
            foreach ($this->orderedItems as $item) {
 | 
					            foreach ($this->items as $item) {
 | 
				
			||||||
                if (is_string($item)) {
 | 
					                $return['OrderedItems'][] = $item->toObject();
 | 
				
			||||||
                    $return['orderedItems'][] = $item;
 | 
					 | 
				
			||||||
                } elseif (is_object($item)) {
 | 
					 | 
				
			||||||
                    $return['orderedItems'][] = $item->toObject();
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        return $return;
 | 
					        return $return;
 | 
				
			||||||
| 
						 | 
					@ -52,63 +48,44 @@ class OrderedCollection extends Collection
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function fromJson($json)
 | 
					    public function fromJson($json)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $success = parent::fromJson($json);
 | 
					        return parent::fromJson($json);
 | 
				
			||||||
        if (!$success) {
 | 
					 | 
				
			||||||
            return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if (array_key_exists('orderedItems', $json)) {
 | 
					 | 
				
			||||||
            foreach ($json['orderedItems'] as $item) {
 | 
					 | 
				
			||||||
                $obj = \Federator\Data\ActivityPub\Factory::newActivityFromJson($item);
 | 
					 | 
				
			||||||
                if ($obj !== false) {
 | 
					 | 
				
			||||||
                    $this->orderedItems[] = $obj;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return true;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    public function append(APObject &$item) : void
 | 
				
			||||||
     * add item to collection
 | 
					 | 
				
			||||||
     * @param APObject|string $item
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function append(&$item): void
 | 
					 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $this->orderedItems[] = $item;
 | 
					        $this->items[] = $item;
 | 
				
			||||||
 | 
					        $this->totalItems = sizeof($this->items);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * get item with given index
 | 
					     * get item with given index
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @return APObject|string|false
 | 
					     * @return APObject|false
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function get(int $index)
 | 
					    public function get(int $index)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        if ($index >= 0) {
 | 
					        if ($index >= 0) {
 | 
				
			||||||
            if ($index >= sizeof($this->orderedItems)) {
 | 
					            if ($index >= $this->totalItems) {
 | 
				
			||||||
                return false;
 | 
					                return false;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            return $this->orderedItems[$index];
 | 
					            return $this->items[$index];
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            if (sizeof($this->orderedItems) + $index < 0) {
 | 
					            if ($this->totalItems+ $index < 0) {
 | 
				
			||||||
                return false;
 | 
					                return false;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            return $this->orderedItems[sizeof($this->orderedItems) + $index];
 | 
					            return $this->items[$this->totalItems + $index];
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function getCount(): int
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        return sizeof($this->orderedItems);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * set items
 | 
					     * set items
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @param APObject[]|string[] $items
 | 
					     * @param APObject[] $items
 | 
				
			||||||
     * @return void
 | 
					     * @return void
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function setItems(&$items)
 | 
					    public function setItems(&$items)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $this->orderedItems = $items;
 | 
					        $this->items = $items;
 | 
				
			||||||
 | 
					        $this->totalItems = sizeof($items);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,7 +19,6 @@ class OrderedCollectionPage extends OrderedCollection
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        parent::__construct();
 | 
					        parent::__construct();
 | 
				
			||||||
        parent::addContext('https://www.w3.org/ns/activitystreams');
 | 
					        parent::addContext('https://www.w3.org/ns/activitystreams');
 | 
				
			||||||
        $this->setType('OrderedCollectionPage');
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
| 
						 | 
					@ -39,7 +38,7 @@ class OrderedCollectionPage extends OrderedCollection
 | 
				
			||||||
        if ($this->partOf !== '') {
 | 
					        if ($this->partOf !== '') {
 | 
				
			||||||
            $return['partOf'] = $this->partOf;
 | 
					            $return['partOf'] = $this->partOf;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        $return['type'] = $this->getType();
 | 
					        $return['type'] = 'OrderedCollectionPage';
 | 
				
			||||||
        return $return;
 | 
					        return $return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -51,29 +50,7 @@ class OrderedCollectionPage extends OrderedCollection
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function fromJson($json)
 | 
					    public function fromJson($json)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $success = parent::fromJson($json);
 | 
					        return parent::fromJson($json);
 | 
				
			||||||
        if (!$success) {
 | 
					 | 
				
			||||||
            return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if (array_key_exists('next', $json)) {
 | 
					 | 
				
			||||||
            $this->next = $json['next'];
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if (array_key_exists('prev', $json)) {
 | 
					 | 
				
			||||||
            $this->prev = $json['prev'];
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if (array_key_exists('partOf', $json)) {
 | 
					 | 
				
			||||||
            $this->partOf = $json['partOf'];
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return true;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * get next url
 | 
					 | 
				
			||||||
     * @return string next URL
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function getNext()
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        return $this->next;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,7 +4,6 @@
 | 
				
			||||||
 * SPDX-License-Identifier: GPL-3.0-or-later
 | 
					 * SPDX-License-Identifier: GPL-3.0-or-later
 | 
				
			||||||
 *
 | 
					 *
 | 
				
			||||||
 * @author Sascha Nitsch (grumpydeveloper)
 | 
					 * @author Sascha Nitsch (grumpydeveloper)
 | 
				
			||||||
 * @author Yannis Vogel (vogeldevelopment)
 | 
					 | 
				
			||||||
 **/
 | 
					 **/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Federator\Data\ActivityPub\Common;
 | 
					namespace Federator\Data\ActivityPub\Common;
 | 
				
			||||||
| 
						 | 
					@ -27,17 +26,6 @@ class Outbox extends OrderedCollectionPage
 | 
				
			||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * set items
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param \Federator\Data\ActivityPub\Common\APObject[] $items the items in the collection
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function setItems(&$items)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        // Optionally: type check that all $items are Activity objects
 | 
					 | 
				
			||||||
        $this->orderedItems = $items;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * convert internal state to php array
 | 
					     * convert internal state to php array
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,18 +0,0 @@
 | 
				
			||||||
<?php
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
 | 
					 | 
				
			||||||
 * SPDX-License-Identifier: GPL-3.0-or-later
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * @author Yannis Vogel (vogeldevelopment)
 | 
					 | 
				
			||||||
 **/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace Federator\Data\ActivityPub\Common;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Reject extends Activity
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    public function __construct()
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        parent::__construct('Reject');
 | 
					 | 
				
			||||||
        parent::addContext('https://www.w3.org/ns/activitystreams');
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,16 +0,0 @@
 | 
				
			||||||
Array
 | 
					 | 
				
			||||||
(
 | 
					 | 
				
			||||||
    [type] => Document
 | 
					 | 
				
			||||||
    [mediaType] => image/jpeg
 | 
					 | 
				
			||||||
    [url] => https://noc.social/system/media_attachments/files/112/350/286/131/419/396/original/26ab9c8a4ab13f16.jpg
 | 
					 | 
				
			||||||
    [name] => Screenshot Zeitleiste aus kdenlive.
 | 
					 | 
				
			||||||
    [blurhash] => UC8|^tSwI-bu-taeRiaeu5e.aJjGsBWnR*jH
 | 
					 | 
				
			||||||
    [focalPoint] => Array
 | 
					 | 
				
			||||||
        (
 | 
					 | 
				
			||||||
            [0] => -0.01
 | 
					 | 
				
			||||||
            [1] => -0.79
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    [width] => 1333
 | 
					 | 
				
			||||||
    [height] => 651
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,45 +0,0 @@
 | 
				
			||||||
<?php
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
 | 
					 | 
				
			||||||
 * SPDX-License-Identifier: GPL-3.0-or-later
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * @author Yannis Vogel (vogeldevelopment)
 | 
					 | 
				
			||||||
 **/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace Federator\Data\ActivityPub\Common;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Update extends Activity
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    public function __construct()
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        parent::__construct('Update');
 | 
					 | 
				
			||||||
        parent::addContext('https://www.w3.org/ns/activitystreams');
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * convert internal state to php array
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @return array<string,mixed>
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function toObject()
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        $return = parent::toObject();
 | 
					 | 
				
			||||||
        $return['type'] = 'Update';
 | 
					 | 
				
			||||||
        // overwrite id from url
 | 
					 | 
				
			||||||
        if ($this->getURL() !== '') {
 | 
					 | 
				
			||||||
            $return['id'] = $this->getURL();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return $return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
   /**
 | 
					 | 
				
			||||||
     * create object from json
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param array<string,mixed> $json input json
 | 
					 | 
				
			||||||
     * @return bool true on success
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function fromJson($json)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        return parent::fromJson($json);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,27 +0,0 @@
 | 
				
			||||||
<?php
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
 | 
					 | 
				
			||||||
 * SPDX-License-Identifier: GPL-3.0-or-later
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * @author Yannis Vogel (vogeldevelopment)
 | 
					 | 
				
			||||||
 **/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace Federator\Data\ActivityPub\Common;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Vote extends APObject
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    public function __construct()
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        parent::__construct('Vote');
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * create object from json
 | 
					 | 
				
			||||||
     * @param mixed $json input
 | 
					 | 
				
			||||||
     * @return bool true on success
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function fromJson($json)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        return parent::fromJson($json);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -16,7 +16,7 @@ class Factory
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * create object tree from json
 | 
					     * create object tree from json
 | 
				
			||||||
     * @param array<string, mixed>|mixed $json input json
 | 
					     * @param array<string, mixed> $json input json
 | 
				
			||||||
     * @return Common\APObject|null object or false on error
 | 
					     * @return Common\APObject|null object or false on error
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public static function newFromJson($json, string $jsonstring)
 | 
					    public static function newFromJson($json, string $jsonstring)
 | 
				
			||||||
| 
						 | 
					@ -31,9 +31,6 @@ class Factory
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        $return = null;
 | 
					        $return = null;
 | 
				
			||||||
        switch ($json['type']) {
 | 
					        switch ($json['type']) {
 | 
				
			||||||
            case 'Announce':
 | 
					 | 
				
			||||||
                $return = new Common\Announce();
 | 
					 | 
				
			||||||
                break;
 | 
					 | 
				
			||||||
            case 'Article':
 | 
					            case 'Article':
 | 
				
			||||||
                $return = new Common\Article();
 | 
					                $return = new Common\Article();
 | 
				
			||||||
                break;
 | 
					                break;
 | 
				
			||||||
| 
						 | 
					@ -42,45 +39,21 @@ class Factory
 | 
				
			||||||
                break;
 | 
					                break;
 | 
				
			||||||
            case 'Event':
 | 
					            case 'Event':
 | 
				
			||||||
                $return = new Common\Event();
 | 
					                $return = new Common\Event();
 | 
				
			||||||
                break;*/
 | 
					                break;
 | 
				
			||||||
            case 'Follow':
 | 
					            case 'Follow':
 | 
				
			||||||
                $return = new Common\Follow();
 | 
					                $return = new Common\Follow();
 | 
				
			||||||
                break;
 | 
					                break;*/
 | 
				
			||||||
            case 'Image':
 | 
					            case 'Image':
 | 
				
			||||||
                $return = new Common\Image();
 | 
					                $return = new Common\Image();
 | 
				
			||||||
                break;
 | 
					                break;
 | 
				
			||||||
            case 'Note':
 | 
					            /*case 'Note':
 | 
				
			||||||
                $return = new Common\Note();
 | 
					                $return = new Common\Note();
 | 
				
			||||||
                break;
 | 
					                break;
 | 
				
			||||||
            case 'Outbox':
 | 
					            case 'Question':
 | 
				
			||||||
                $return = new Common\Outbox();
 | 
					                $return = new \Common\Question();
 | 
				
			||||||
                break;
 | 
					 | 
				
			||||||
            case 'Vote':
 | 
					 | 
				
			||||||
                $return = new Common\Vote();
 | 
					 | 
				
			||||||
                break;
 | 
					 | 
				
			||||||
            case 'Like':
 | 
					 | 
				
			||||||
                $return = new Common\Like();
 | 
					 | 
				
			||||||
                break;
 | 
					 | 
				
			||||||
            case 'Dislike':
 | 
					 | 
				
			||||||
                $return = new Common\Dislike();
 | 
					 | 
				
			||||||
                break;
 | 
					 | 
				
			||||||
            case 'Inbox':
 | 
					 | 
				
			||||||
                $return = new Common\Inbox();
 | 
					 | 
				
			||||||
                break;
 | 
					 | 
				
			||||||
            case 'OrderedCollection':
 | 
					 | 
				
			||||||
                $return = new Common\OrderedCollection();
 | 
					 | 
				
			||||||
                break;
 | 
					 | 
				
			||||||
            case 'OrderedCollectionPage':
 | 
					 | 
				
			||||||
                $return = new Common\OrderedCollectionPage();
 | 
					 | 
				
			||||||
                break;
 | 
					 | 
				
			||||||
            case 'Tombstone':
 | 
					 | 
				
			||||||
                $return = new Common\APObject("Tombstone");
 | 
					 | 
				
			||||||
                break;
 | 
					 | 
				
			||||||
            /*case 'Question':
 | 
					 | 
				
			||||||
                $return = new Common\Question();
 | 
					 | 
				
			||||||
                break;
 | 
					                break;
 | 
				
			||||||
            case 'Video':
 | 
					            case 'Video':
 | 
				
			||||||
                $return = new Common\Video();
 | 
					                $return = new \Common\Video();
 | 
				
			||||||
                break;*/
 | 
					                break;*/
 | 
				
			||||||
            default:
 | 
					            default:
 | 
				
			||||||
                error_log("newFromJson: unknown type: '" . $json['type'] . "' " . $jsonstring);
 | 
					                error_log("newFromJson: unknown type: '" . $json['type'] . "' " . $jsonstring);
 | 
				
			||||||
| 
						 | 
					@ -104,7 +77,9 @@ class Factory
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        //$return = false;
 | 
					        //$return = false;
 | 
				
			||||||
        switch ($json['type']) {
 | 
					        switch ($json['type']) {
 | 
				
			||||||
            case 'Accept':
 | 
					            case 'MakePhanHappy':
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					/*            case 'Accept':
 | 
				
			||||||
                $return = new Common\Accept();
 | 
					                $return = new Common\Accept();
 | 
				
			||||||
                break;
 | 
					                break;
 | 
				
			||||||
            case 'Announce':
 | 
					            case 'Announce':
 | 
				
			||||||
| 
						 | 
					@ -116,30 +91,18 @@ class Factory
 | 
				
			||||||
            case 'Delete':
 | 
					            case 'Delete':
 | 
				
			||||||
                $return = new Common\Delete();
 | 
					                $return = new Common\Delete();
 | 
				
			||||||
                break;
 | 
					                break;
 | 
				
			||||||
            case 'Like':
 | 
					 | 
				
			||||||
                $return = new Common\Like();
 | 
					 | 
				
			||||||
                break;
 | 
					 | 
				
			||||||
            case 'Dislike':
 | 
					 | 
				
			||||||
                $return = new Common\Dislike();
 | 
					 | 
				
			||||||
                break;
 | 
					 | 
				
			||||||
            case 'Follow':
 | 
					            case 'Follow':
 | 
				
			||||||
                $return = new Common\Follow();
 | 
					                $return = new Common\Follow();
 | 
				
			||||||
                break;
 | 
					                break;
 | 
				
			||||||
            case 'Reject':
 | 
					 | 
				
			||||||
                $return = new Common\Reject();
 | 
					 | 
				
			||||||
                break;
 | 
					 | 
				
			||||||
            case 'Undo':
 | 
					            case 'Undo':
 | 
				
			||||||
                $return = new Common\Undo();
 | 
					                $return = new \Common\Undo();
 | 
				
			||||||
                break;
 | 
					                break;*/
 | 
				
			||||||
            case 'Update':
 | 
					 | 
				
			||||||
                $return = new Common\Update();
 | 
					 | 
				
			||||||
                break;
 | 
					 | 
				
			||||||
            default:
 | 
					            default:
 | 
				
			||||||
                error_log("newActivityFromJson unsupported type: " . print_r($json, true));
 | 
					                error_log("newActivityFromJson " . print_r($json, true));
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if (isset($return) && $return->fromJson($json) !== null) {
 | 
					        /*if ($return !== false && $return->fromJson($json) !== null) {
 | 
				
			||||||
            return $return;
 | 
					            return $return;
 | 
				
			||||||
        }
 | 
					        }*/
 | 
				
			||||||
        return false;
 | 
					        return false;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,151 +0,0 @@
 | 
				
			||||||
<?php
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
 | 
					 | 
				
			||||||
 * SPDX-License-Identifier: GPL-3.0-or-later
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * @author Yannis Vogel (vogeldevelopment)
 | 
					 | 
				
			||||||
 **/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace Federator\Data;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * storage class for user attributes
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
class FedUser
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * user id
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @var string $id
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public $id;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * user url
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @var string $actorURL
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public $actorURL;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * user name
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @var string $name
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public $name = '';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * user public key
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @var string $publicKey
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public $publicKey;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * summary for user/profile
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @var string $summary
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public $summary = '';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * type of user (person/group)
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @var string $type
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public $type = 'Person';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * inbox URL
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @var string $inboxURL
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public $inboxURL;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * shared inbox URL
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @var string $sharedInboxURL
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public $sharedInboxURL;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * followers URL
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @var string $followersURL
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public $followersURL;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * following URL
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @var string $followingURL
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public $followingURL;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * public key ID
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @var string $publicKeyId
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public $publicKeyId;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * outbox URL
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @var string $outboxURL
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public $outboxURL;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * create new user object from json string
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param string $input input string
 | 
					 | 
				
			||||||
     * @return FedUser|false
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public static function createFromJson($input)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        $data = json_decode($input, true);
 | 
					 | 
				
			||||||
        if ($data === null) {
 | 
					 | 
				
			||||||
            return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $user = new FedUser();
 | 
					 | 
				
			||||||
        $user->id = $data['id'] ?? '';
 | 
					 | 
				
			||||||
        $user->actorURL = $data['actorURL'] ?? '';
 | 
					 | 
				
			||||||
        $user->name = $data['name'] ?? '';
 | 
					 | 
				
			||||||
        $user->publicKey = $data['publicKey'] ?? '';
 | 
					 | 
				
			||||||
        $user->summary = $data['summary'] ?? '';
 | 
					 | 
				
			||||||
        $user->type = $data['type'] ?? 'Person';
 | 
					 | 
				
			||||||
        $user->inboxURL = $data['inbox'] ?? '';
 | 
					 | 
				
			||||||
        $user->sharedInboxURL = $data['sharedInbox'] ?? '';
 | 
					 | 
				
			||||||
        $user->followersURL = $data['followers'] ?? '';
 | 
					 | 
				
			||||||
        $user->followingURL = $data['following'] ?? '';
 | 
					 | 
				
			||||||
        $user->publicKeyId = $data['publicKeyId'] ?? '';
 | 
					 | 
				
			||||||
        $user->outboxURL = $data['outbox'] ?? '';
 | 
					 | 
				
			||||||
        return $user;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * convert internal data to json string
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @return string
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function toJson()
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        $data = [
 | 
					 | 
				
			||||||
            'id' => $this->id,
 | 
					 | 
				
			||||||
            'actorURL' => $this->actorURL,
 | 
					 | 
				
			||||||
            'name' => $this->name,
 | 
					 | 
				
			||||||
            'publicKey' => $this->publicKey,
 | 
					 | 
				
			||||||
            'summary' => $this->summary,
 | 
					 | 
				
			||||||
            'type' => $this->type,
 | 
					 | 
				
			||||||
            'inbox' => $this->inboxURL,
 | 
					 | 
				
			||||||
            'sharedInbox' => $this->sharedInboxURL,
 | 
					 | 
				
			||||||
            'followers' => $this->followersURL,
 | 
					 | 
				
			||||||
            'following' => $this->followingURL,
 | 
					 | 
				
			||||||
            'publicKeyId' => $this->publicKeyId,
 | 
					 | 
				
			||||||
            'outbox' => $this->outboxURL,
 | 
					 | 
				
			||||||
        ];
 | 
					 | 
				
			||||||
        return json_encode($data) ?: '';
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,68 +0,0 @@
 | 
				
			||||||
<?php
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
 | 
					 | 
				
			||||||
 * SPDX-License-Identifier: GPL-3.0-or-later
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * @author Yannis Vogel (vogeldevelopment)
 | 
					 | 
				
			||||||
 **/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace Federator\DIO;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * IO functions related to articles
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
class Article
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Convert an Article to a Note
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param \Federator\Data\ActivityPub\Common\Article $article
 | 
					 | 
				
			||||||
     * @return \Federator\Data\ActivityPub\Common\Note
 | 
					 | 
				
			||||||
     *      The generated note
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public static function convertToNote($article)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        $note = new \Federator\Data\ActivityPub\Common\Note();
 | 
					 | 
				
			||||||
        $note->setId($article->getId())
 | 
					 | 
				
			||||||
            ->setURL($article->getURL());
 | 
					 | 
				
			||||||
        $note->setContent($article->getContent());
 | 
					 | 
				
			||||||
        $note->setSummary($article->getSummary());
 | 
					 | 
				
			||||||
        $note->setPublished($article->getPublished());
 | 
					 | 
				
			||||||
        $note->setName($article->getName());
 | 
					 | 
				
			||||||
        $note->setAttributedTo($article->getAttributedTo());
 | 
					 | 
				
			||||||
        foreach ($article->getTo() as $to) {
 | 
					 | 
				
			||||||
            $note->addTo($to);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        foreach ($article->getCc() as $cc) {
 | 
					 | 
				
			||||||
            $note->addCc($cc);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return $note;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /** Conditionally convert article to a note
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param \Federator\Data\ActivityPub\Common\Article $article
 | 
					 | 
				
			||||||
     * @param string $targetUrl
 | 
					 | 
				
			||||||
     *      The target URL for the activity (e.g. mastodon.social)
 | 
					 | 
				
			||||||
     * @return \Federator\Data\ActivityPub\Common\Note|\Federator\Data\ActivityPub\Common\Article
 | 
					 | 
				
			||||||
     *      The generated note on success, false on failure
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public static function conditionalConvertToNote($article, $targetUrl)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        $supportFile = file_get_contents(PROJECT_ROOT . '/formatsupport.json');
 | 
					 | 
				
			||||||
        if ($supportFile === false) {
 | 
					 | 
				
			||||||
            error_log("Article::conditionalConvertToNote Failed to read support file for article conversion.");
 | 
					 | 
				
			||||||
            return $article; // Fallback to original article if file read fails
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $supportlist = json_decode($supportFile, true);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (
 | 
					 | 
				
			||||||
            !isset($supportlist['activitypub']['article']) ||
 | 
					 | 
				
			||||||
            !is_array($supportlist['activitypub']['article']) ||
 | 
					 | 
				
			||||||
            !in_array($targetUrl, $supportlist['activitypub']['article'], true)
 | 
					 | 
				
			||||||
        ) {
 | 
					 | 
				
			||||||
            return self::convertToNote($article); // Articles are not supported for this target
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return $article; // Articles are supported, return as is
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,242 +0,0 @@
 | 
				
			||||||
<?php
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
 | 
					 | 
				
			||||||
 * SPDX-License-Identifier: GPL-3.0-or-later
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * @author Yannis Vogel (vogeldevelopment)
 | 
					 | 
				
			||||||
 **/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace Federator\DIO;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * IO functions related to fedUsers
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
class FedUser
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * add local user based on given user object received from remote service
 | 
					 | 
				
			||||||
     * @param \mysqli $dbh database handle
 | 
					 | 
				
			||||||
     * @param \Federator\Data\FedUser $user user object to use
 | 
					 | 
				
			||||||
     * @param string $_user user/profile name
 | 
					 | 
				
			||||||
     * @return void
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    protected static function addLocalUser($dbh, $user, $_user)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        // check if it is timed out user
 | 
					 | 
				
			||||||
        $sql = 'select unix_timestamp(`validuntil`) from fedusers where id=?';
 | 
					 | 
				
			||||||
        $stmt = $dbh->prepare($sql);
 | 
					 | 
				
			||||||
        if ($stmt === false) {
 | 
					 | 
				
			||||||
            throw new \Federator\Exceptions\ServerError("FedUser::addLocalUser Failed to prepare statement");
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $stmt->bind_param("s", $_user);
 | 
					 | 
				
			||||||
        $validuntil = 0;
 | 
					 | 
				
			||||||
        $ret = $stmt->bind_result($validuntil);
 | 
					 | 
				
			||||||
        $stmt->execute();
 | 
					 | 
				
			||||||
        if ($ret) {
 | 
					 | 
				
			||||||
            $stmt->fetch();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $stmt->close();
 | 
					 | 
				
			||||||
        if ($validuntil == 0) {
 | 
					 | 
				
			||||||
            $sql = 'insert into fedusers (id, url, name, publickey, summary, type, inboxurl, sharedinboxurl,';
 | 
					 | 
				
			||||||
            $sql .= ' followersurl, followingurl, publickeyid, outboxurl, validuntil)';
 | 
					 | 
				
			||||||
            $sql .= ' values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, now() + interval 1 day)';
 | 
					 | 
				
			||||||
            $stmt = $dbh->prepare($sql);
 | 
					 | 
				
			||||||
            if ($stmt === false) {
 | 
					 | 
				
			||||||
                throw new \Federator\Exceptions\ServerError("FedUser::addLocalUser Failed to prepare create statement");
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            $stmt->bind_param(
 | 
					 | 
				
			||||||
                "ssssssssssss",
 | 
					 | 
				
			||||||
                $_user,
 | 
					 | 
				
			||||||
                $user->actorURL,
 | 
					 | 
				
			||||||
                $user->name,
 | 
					 | 
				
			||||||
                $user->publicKey,
 | 
					 | 
				
			||||||
                $user->summary,
 | 
					 | 
				
			||||||
                $user->type,
 | 
					 | 
				
			||||||
                $user->inboxURL,
 | 
					 | 
				
			||||||
                $user->sharedInboxURL,
 | 
					 | 
				
			||||||
                $user->followersURL,
 | 
					 | 
				
			||||||
                $user->followingURL,
 | 
					 | 
				
			||||||
                $user->publicKeyId,
 | 
					 | 
				
			||||||
                $user->outboxURL
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            // update to existing user
 | 
					 | 
				
			||||||
            $sql = 'update fedusers set validuntil=now() + interval 1 day, url=?, name=?, publickey=?, summary=?,';
 | 
					 | 
				
			||||||
            $sql .= ' type=?, inboxurl=?, sharedinboxurl=?, followersurl=?, followingurl=?, publickeyid=?, outboxurl=?';
 | 
					 | 
				
			||||||
            $sql .= ' where id=?';
 | 
					 | 
				
			||||||
            $stmt = $dbh->prepare($sql);
 | 
					 | 
				
			||||||
            if ($stmt === false) {
 | 
					 | 
				
			||||||
                throw new \Federator\Exceptions\ServerError("FedUser::extendUser Failed to prepare update statement");
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            $stmt->bind_param(
 | 
					 | 
				
			||||||
                "ssssssssssss",
 | 
					 | 
				
			||||||
                $user->actorURL,
 | 
					 | 
				
			||||||
                $user->name,
 | 
					 | 
				
			||||||
                $user->publicKey,
 | 
					 | 
				
			||||||
                $user->summary,
 | 
					 | 
				
			||||||
                $user->type,
 | 
					 | 
				
			||||||
                $user->inboxURL,
 | 
					 | 
				
			||||||
                $user->sharedInboxURL,
 | 
					 | 
				
			||||||
                $user->followersURL,
 | 
					 | 
				
			||||||
                $user->followingURL,
 | 
					 | 
				
			||||||
                $user->publicKeyId,
 | 
					 | 
				
			||||||
                $user->outboxURL,
 | 
					 | 
				
			||||||
                $_user
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        try {
 | 
					 | 
				
			||||||
            $stmt->execute();
 | 
					 | 
				
			||||||
            $stmt->close();
 | 
					 | 
				
			||||||
            $user->id = $_user;
 | 
					 | 
				
			||||||
        } catch (\mysqli_sql_exception $e) {
 | 
					 | 
				
			||||||
            error_log($sql);
 | 
					 | 
				
			||||||
            error_log(print_r($user, true));
 | 
					 | 
				
			||||||
            error_log($e->getMessage());
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * extend the given user with internal data
 | 
					 | 
				
			||||||
     * @param \mysqli $dbh database  handle
 | 
					 | 
				
			||||||
     * @param \Federator\Data\FedUser $user user to extend
 | 
					 | 
				
			||||||
     * @param string $_user user/profile name
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    protected static function extendUser(\mysqli $dbh, \Federator\Data\FedUser $user, $_user): void
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        $sql = 'select id,unix_timestamp(`validuntil`) from fedusers where id=?';
 | 
					 | 
				
			||||||
        $stmt = $dbh->prepare($sql);
 | 
					 | 
				
			||||||
        if ($stmt === false) {
 | 
					 | 
				
			||||||
            throw new \Federator\Exceptions\ServerError("FedUser::extendUser Failed to prepare statement");
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $stmt->bind_param("s", $_user);
 | 
					 | 
				
			||||||
        $validuntil = 0;
 | 
					 | 
				
			||||||
        $ret = $stmt->bind_result($user->id, $validuntil);
 | 
					 | 
				
			||||||
        $stmt->execute();
 | 
					 | 
				
			||||||
        if ($ret) {
 | 
					 | 
				
			||||||
            $stmt->fetch();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $stmt->close();
 | 
					 | 
				
			||||||
        // if a new user, create own database entry with additionally needed info
 | 
					 | 
				
			||||||
        if ($user->id === null || $validuntil < time()) {
 | 
					 | 
				
			||||||
            self::addLocalUser($dbh, $user, $_user);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // no further processing for now
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * get user by name
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param \mysqli $dbh
 | 
					 | 
				
			||||||
     *          database handle
 | 
					 | 
				
			||||||
     * @param string $_name
 | 
					 | 
				
			||||||
     *          user name
 | 
					 | 
				
			||||||
     * @param \Federator\Cache\Cache|null $cache
 | 
					 | 
				
			||||||
     *          optional caching service
 | 
					 | 
				
			||||||
     * @return \Federator\Data\FedUser
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public static function getUserByName($dbh, $_name, $cache)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        $user = false;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // ask cache
 | 
					 | 
				
			||||||
        if ($cache !== null) {
 | 
					 | 
				
			||||||
            $user = $cache->getRemoteFedUserByName($_name);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if ($user !== false) {
 | 
					 | 
				
			||||||
            return $user;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        // check our db
 | 
					 | 
				
			||||||
        $sql = 'select `id`, `url`, `name`, `publickey`, `summary`, `type`, `inboxurl`, `sharedinboxurl`, `followersurl`,';
 | 
					 | 
				
			||||||
        $sql .= ' `followingurl`, `publickeyid`, `outboxurl`';
 | 
					 | 
				
			||||||
        $sql .= ' from fedusers where `id`=? and `validuntil`>=now()';
 | 
					 | 
				
			||||||
        $stmt = $dbh->prepare($sql);
 | 
					 | 
				
			||||||
        if ($stmt === false) {
 | 
					 | 
				
			||||||
            throw new \Federator\Exceptions\ServerError("FedUser::getUserByName Failed to prepare statement");
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $stmt->bind_param("s", $_name);
 | 
					 | 
				
			||||||
        $user = new \Federator\Data\FedUser();
 | 
					 | 
				
			||||||
        $ret = $stmt->bind_result(
 | 
					 | 
				
			||||||
            $user->id,
 | 
					 | 
				
			||||||
            $user->actorURL,
 | 
					 | 
				
			||||||
            $user->name,
 | 
					 | 
				
			||||||
            $user->publicKey,
 | 
					 | 
				
			||||||
            $user->summary,
 | 
					 | 
				
			||||||
            $user->type,
 | 
					 | 
				
			||||||
            $user->inboxURL,
 | 
					 | 
				
			||||||
            $user->sharedInboxURL,
 | 
					 | 
				
			||||||
            $user->followersURL,
 | 
					 | 
				
			||||||
            $user->followingURL,
 | 
					 | 
				
			||||||
            $user->publicKeyId,
 | 
					 | 
				
			||||||
            $user->outboxURL
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        $stmt->execute();
 | 
					 | 
				
			||||||
        if ($ret) {
 | 
					 | 
				
			||||||
            $stmt->fetch();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $stmt->close();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if ($user->id === null) {
 | 
					 | 
				
			||||||
            // check if its a federated user with username@domain.ending
 | 
					 | 
				
			||||||
            if (preg_match("/^([^@]+)@(.*)$/", $_name, $matches) == 1) {
 | 
					 | 
				
			||||||
                // make webfinger request
 | 
					 | 
				
			||||||
                $remoteURL = 'https://' . $matches[2] . '/.well-known/webfinger?resource=acct:' . urlencode($_name);
 | 
					 | 
				
			||||||
                $headers = ['Accept: application/activity+json'];
 | 
					 | 
				
			||||||
                [$response, $info] = \Federator\Main::getFromRemote($remoteURL, $headers);
 | 
					 | 
				
			||||||
                if ($info['http_code'] != 200) {
 | 
					 | 
				
			||||||
                    throw new \Federator\Exceptions\ServerError("FedUser::getUserByName Failed to fetch webfinger for " . $_name);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                $r = json_decode($response, true);
 | 
					 | 
				
			||||||
                if ($r === false || $r === null || !is_array($r)) {
 | 
					 | 
				
			||||||
                    throw new \Federator\Exceptions\ServerError("FedUser::getUserByName Failed to decode webfinger for " . $_name);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                // get the webwinger user url and fetch the user
 | 
					 | 
				
			||||||
                if (isset($r['links'])) {
 | 
					 | 
				
			||||||
                    foreach ($r['links'] as $link) {
 | 
					 | 
				
			||||||
                        if (isset($link['rel']) && $link['rel'] === 'self') {
 | 
					 | 
				
			||||||
                            $remoteURL = $link['href'];
 | 
					 | 
				
			||||||
                            break;
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                if (!isset($remoteURL)) {
 | 
					 | 
				
			||||||
                    throw new \Federator\Exceptions\ServerError("FedUser::getUserByName Failed to find self link in webfinger for " . $_name);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                // fetch the user
 | 
					 | 
				
			||||||
                $headers = ['Accept: application/activity+json'];
 | 
					 | 
				
			||||||
                [$response, $info] = \Federator\Main::getFromRemote($remoteURL, $headers);
 | 
					 | 
				
			||||||
                if ($info['http_code'] != 200) {
 | 
					 | 
				
			||||||
                    throw new \Federator\Exceptions\ServerError("FedUser::getUserByName Failed to fetch user from remoteUrl for " . $_name);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                $r = json_decode($response, true);
 | 
					 | 
				
			||||||
                if ($r === false || $r === null || !is_array($r)) {
 | 
					 | 
				
			||||||
                    throw new \Federator\Exceptions\ServerError("FedUser::getUserByName Failed to decode user for " . $_name);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                $r['publicKeyId'] = $r['publicKey']['id'];
 | 
					 | 
				
			||||||
                $r['publicKey'] = $r['publicKey']['publicKeyPem'];
 | 
					 | 
				
			||||||
                if (isset($r['endpoints'])) {
 | 
					 | 
				
			||||||
                    if (isset($r['endpoints']['sharedInbox'])) {
 | 
					 | 
				
			||||||
                        $r['sharedInbox'] = $r['endpoints']['sharedInbox'];
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                $r['actorURL'] = $remoteURL;
 | 
					 | 
				
			||||||
                $data = json_encode($r);
 | 
					 | 
				
			||||||
                if ($data === false) {
 | 
					 | 
				
			||||||
                    throw new \Federator\Exceptions\ServerError("FedUser::getUserByName Failed to encode userdata " . $_name);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                $user = \Federator\Data\FedUser::createFromJson($data);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if ($cache !== null && $user !== false) {
 | 
					 | 
				
			||||||
            if ($user->id !== null && $user->actorURL !== null) {
 | 
					 | 
				
			||||||
                self::addLocalUser($dbh, $user, $_name);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            $cache->saveRemoteFedUserByName($_name, $user);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if ($user === false) {
 | 
					 | 
				
			||||||
            throw new \Federator\Exceptions\ServerError("FedUser::getUserByName User not found");
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return $user;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,525 +0,0 @@
 | 
				
			||||||
<?php
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
 | 
					 | 
				
			||||||
 * SPDX-License-Identifier: GPL-3.0-or-later
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * @author Yannis Vogel (vogeldevelopment)
 | 
					 | 
				
			||||||
 **/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace Federator\DIO;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * IO functions related to followers
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
class Followers
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * get followers of user
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param \mysqli $dbh
 | 
					 | 
				
			||||||
     *          database handle
 | 
					 | 
				
			||||||
     * @param string $id
 | 
					 | 
				
			||||||
     *          user id
 | 
					 | 
				
			||||||
     * @param \Federator\Connector\Connector $connector
 | 
					 | 
				
			||||||
     *          connector to fetch use with
 | 
					 | 
				
			||||||
     * @param \Federator\Cache\Cache|null $cache
 | 
					 | 
				
			||||||
     *          optional caching service
 | 
					 | 
				
			||||||
     * @return \Federator\Data\FedUser[]
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public static function getFollowersByUser($dbh, $id, $connector, $cache)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        // ask cache
 | 
					 | 
				
			||||||
        if ($cache !== null) {
 | 
					 | 
				
			||||||
            $followers = $cache->getRemoteFollowersOfUser($id);
 | 
					 | 
				
			||||||
            if ($followers !== false) {
 | 
					 | 
				
			||||||
                return $followers;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $followers = [];
 | 
					 | 
				
			||||||
        $sql = 'select source_user from follows where target_user = ?';
 | 
					 | 
				
			||||||
        $stmt = $dbh->prepare($sql);
 | 
					 | 
				
			||||||
        if ($stmt === false) {
 | 
					 | 
				
			||||||
            throw new \Federator\Exceptions\ServerError("Followers::getFollowersByUser Failed to prepare statement");
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $stmt->bind_param("s", $id);
 | 
					 | 
				
			||||||
        $stmt->execute();
 | 
					 | 
				
			||||||
        $followerIds = [];
 | 
					 | 
				
			||||||
        $stmt->bind_result($sourceUser);
 | 
					 | 
				
			||||||
        while ($stmt->fetch()) {
 | 
					 | 
				
			||||||
            $followerIds[] = $sourceUser;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $stmt->close();
 | 
					 | 
				
			||||||
        foreach ($followerIds as $followerId) {
 | 
					 | 
				
			||||||
            try {
 | 
					 | 
				
			||||||
                $user = \Federator\DIO\FedUser::getUserByName(
 | 
					 | 
				
			||||||
                    $dbh,
 | 
					 | 
				
			||||||
                    $followerId,
 | 
					 | 
				
			||||||
                    $cache,
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
            } catch (\Throwable $e) {
 | 
					 | 
				
			||||||
                error_log("Followers::getFollowersByUser Exception: " . $e->getMessage());
 | 
					 | 
				
			||||||
                continue; // Skip this user if an exception occurs
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            if ($user !== false && $user->id !== null) {
 | 
					 | 
				
			||||||
                $followers[] = $user;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if ($followers === []) {
 | 
					 | 
				
			||||||
            // ask connector for user-id
 | 
					 | 
				
			||||||
            $followers = $connector->getRemoteFollowersOfUser($id);
 | 
					 | 
				
			||||||
            if ($followers === false) {
 | 
					 | 
				
			||||||
                $followers = [];
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        // save followers to cache
 | 
					 | 
				
			||||||
        if ($cache !== null) {
 | 
					 | 
				
			||||||
            $cache->saveRemoteFollowersOfUser($id, $followers);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return $followers;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * get following for user - who does the user follow
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param \mysqli $dbh
 | 
					 | 
				
			||||||
     *          database handle
 | 
					 | 
				
			||||||
     * @param string $id
 | 
					 | 
				
			||||||
     *          user id
 | 
					 | 
				
			||||||
     * @param \Federator\Connector\Connector $connector
 | 
					 | 
				
			||||||
     *          connector to fetch use with
 | 
					 | 
				
			||||||
     * @param \Federator\Cache\Cache|null $cache
 | 
					 | 
				
			||||||
     *          optional caching service
 | 
					 | 
				
			||||||
     * @return \Federator\Data\FedUser[]
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public static function getFollowingForUser($dbh, $id, $connector, $cache)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        // ask cache
 | 
					 | 
				
			||||||
        if ($cache !== null) {
 | 
					 | 
				
			||||||
            $following = $cache->getRemoteFollowingForUser($id);
 | 
					 | 
				
			||||||
            if ($following !== false) {
 | 
					 | 
				
			||||||
                return $following;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $following = [];
 | 
					 | 
				
			||||||
        $sql = 'select target_user from follows where source_user = ?';
 | 
					 | 
				
			||||||
        $stmt = $dbh->prepare($sql);
 | 
					 | 
				
			||||||
        if ($stmt === false) {
 | 
					 | 
				
			||||||
            throw new \Federator\Exceptions\ServerError("Followers::getFollowingForUser Failed to prepare statement");
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $stmt->bind_param("s", $id);
 | 
					 | 
				
			||||||
        $stmt->execute();
 | 
					 | 
				
			||||||
        $followingIds = [];
 | 
					 | 
				
			||||||
        $stmt->bind_result($sourceUser);
 | 
					 | 
				
			||||||
        while ($stmt->fetch()) {
 | 
					 | 
				
			||||||
            $followingIds[] = $sourceUser;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $stmt->close();
 | 
					 | 
				
			||||||
        foreach ($followingIds as $followingId) {
 | 
					 | 
				
			||||||
            try {
 | 
					 | 
				
			||||||
                $user = \Federator\DIO\FedUser::getUserByName(
 | 
					 | 
				
			||||||
                    $dbh,
 | 
					 | 
				
			||||||
                    $followingId,
 | 
					 | 
				
			||||||
                    $cache,
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
            } catch (\Throwable $e) {
 | 
					 | 
				
			||||||
                error_log("Followers::getFollowingForUser Exception: " . $e->getMessage());
 | 
					 | 
				
			||||||
                continue; // Skip this user if an exception occurs
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            if ($user !== false && $user->id !== null) {
 | 
					 | 
				
			||||||
                $following[] = $user;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if ($following === []) {
 | 
					 | 
				
			||||||
            // ask connector for user-id
 | 
					 | 
				
			||||||
            $following = $connector->getRemoteFollowingForUser($id);
 | 
					 | 
				
			||||||
            if ($following === false) {
 | 
					 | 
				
			||||||
                $following = [];
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        // save posts to DB
 | 
					 | 
				
			||||||
        if ($cache !== null) {
 | 
					 | 
				
			||||||
            $cache->saveRemoteFollowingForUser($id, $following);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return $following;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * get followers of federated external user (e.g. mastodon)
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param \mysqli $dbh
 | 
					 | 
				
			||||||
     *          database handle
 | 
					 | 
				
			||||||
     * @param \Federator\Connector\Connector $connector
 | 
					 | 
				
			||||||
     *          connector to fetch use with
 | 
					 | 
				
			||||||
     * @param \Federator\Cache\Cache|null $cache
 | 
					 | 
				
			||||||
     *          optional caching service
 | 
					 | 
				
			||||||
     * @param string $id
 | 
					 | 
				
			||||||
     *          user id
 | 
					 | 
				
			||||||
     * @return \Federator\Data\User[]
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public static function getFollowersByFedUser($dbh, $connector, $cache, $id)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        $followers = [];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $sql = 'select source_user from follows where target_user = ?';
 | 
					 | 
				
			||||||
        $stmt = $dbh->prepare($sql);
 | 
					 | 
				
			||||||
        if ($stmt === false) {
 | 
					 | 
				
			||||||
            throw new \Federator\Exceptions\ServerError("Followers::getFollowersByFedUser Failed to prepare statement");
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $stmt->bind_param("s", $id);
 | 
					 | 
				
			||||||
        $stmt->execute();
 | 
					 | 
				
			||||||
        $followerIds = [];
 | 
					 | 
				
			||||||
        $stmt->bind_result($sourceUser);
 | 
					 | 
				
			||||||
        while ($stmt->fetch()) {
 | 
					 | 
				
			||||||
            $followerIds[] = $sourceUser;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        foreach ($followerIds as $followerId) {
 | 
					 | 
				
			||||||
            try {
 | 
					 | 
				
			||||||
                $user = \Federator\DIO\User::getUserByName(
 | 
					 | 
				
			||||||
                    $dbh,
 | 
					 | 
				
			||||||
                    $followerId,
 | 
					 | 
				
			||||||
                    $connector,
 | 
					 | 
				
			||||||
                    $cache
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
            } catch (\Throwable $e) {
 | 
					 | 
				
			||||||
                error_log("Followers::getFollowersByFedUser Exception: " . $e->getMessage());
 | 
					 | 
				
			||||||
                continue; // Skip this user if an exception occurs
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            if ($user !== false && $user->id !== null) {
 | 
					 | 
				
			||||||
                $followers[] = $user;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return $followers;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * send follow request
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param \mysqli $dbh database handle
 | 
					 | 
				
			||||||
     * @param \Federator\Connector\Connector $connector connector to use
 | 
					 | 
				
			||||||
     * @param \Federator\Cache\Cache|null $cache optional caching service
 | 
					 | 
				
			||||||
     * @param string $_user source user
 | 
					 | 
				
			||||||
     * @param string $_targetUser target user id
 | 
					 | 
				
			||||||
     * @param string $host the host for generating the follow ID
 | 
					 | 
				
			||||||
     * @return string|false the generated follow ID on success, false on failure
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public static function sendFollowRequest($dbh, $connector, $cache, $_user, $_targetUser, $host)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        if ($dbh === false) {
 | 
					 | 
				
			||||||
            throw new \Federator\Exceptions\ServerError("Followers::sendFollowRequest Failed to get database handle");
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $user = \Federator\DIO\User::getUserByName(
 | 
					 | 
				
			||||||
            $dbh,
 | 
					 | 
				
			||||||
            $_user,
 | 
					 | 
				
			||||||
            $connector,
 | 
					 | 
				
			||||||
            $cache
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        if ($user === false || $user->id === null) {
 | 
					 | 
				
			||||||
            throw new \Federator\Exceptions\FileNotFound();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $fedUser = \Federator\DIO\FedUser::getUserByName(
 | 
					 | 
				
			||||||
            $dbh,
 | 
					 | 
				
			||||||
            $_targetUser,
 | 
					 | 
				
			||||||
            $cache
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        if ($fedUser === false || $fedUser->actorURL === null) {
 | 
					 | 
				
			||||||
            throw new \Federator\Exceptions\FileNotFound();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $sourceUser = $user->id;
 | 
					 | 
				
			||||||
        $idUrl = self::addFollow($dbh, $sourceUser, $fedUser->id, $host);
 | 
					 | 
				
			||||||
        if ($idUrl === false) {
 | 
					 | 
				
			||||||
            return false; // Failed to add follow
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $followObj = new \Federator\Data\ActivityPub\Common\Follow();
 | 
					 | 
				
			||||||
        $sourceUserUrl = 'https://' . $host . '/' . $sourceUser;
 | 
					 | 
				
			||||||
        $followObj->setFObject($fedUser->actorURL);
 | 
					 | 
				
			||||||
        $followObj->setAActor($sourceUserUrl);
 | 
					 | 
				
			||||||
        $followObj->setID($idUrl);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Send the Follow activity
 | 
					 | 
				
			||||||
        $inboxUrl = $fedUser->inboxURL;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $json = json_encode($followObj, JSON_UNESCAPED_SLASHES);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if ($json === false) {
 | 
					 | 
				
			||||||
            self::removeFollow($dbh, $sourceUser, $fedUser->id);
 | 
					 | 
				
			||||||
            throw new \Exception('Failed to encode JSON: ' . json_last_error_msg());
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $digest = 'SHA-256=' . base64_encode(hash('sha256', $json, true));
 | 
					 | 
				
			||||||
        $date = gmdate('D, d M Y H:i:s') . ' GMT';
 | 
					 | 
				
			||||||
        $parsed = parse_url($inboxUrl);
 | 
					 | 
				
			||||||
        if ($parsed === false) {
 | 
					 | 
				
			||||||
            self::removeFollow($dbh, $sourceUser, $fedUser->id);
 | 
					 | 
				
			||||||
            throw new \Exception('Failed to parse URL: ' . $inboxUrl);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (!isset($parsed['host']) || !isset($parsed['path'])) {
 | 
					 | 
				
			||||||
            self::removeFollow($dbh, $sourceUser, $fedUser->id);
 | 
					 | 
				
			||||||
            throw new \Exception('Invalid inbox URL: missing host or path');
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $extHost = $parsed['host'];
 | 
					 | 
				
			||||||
        $path = $parsed['path'];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Build the signature string
 | 
					 | 
				
			||||||
        $signatureString = "(request-target): post {$path}\n" .
 | 
					 | 
				
			||||||
            "host: {$extHost}\n" .
 | 
					 | 
				
			||||||
            "date: {$date}\n" .
 | 
					 | 
				
			||||||
            "digest: {$digest}";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Get rsa private key
 | 
					 | 
				
			||||||
        $privateKey = \Federator\DIO\User::getrsaprivate($dbh, $user->id); // OR from DB
 | 
					 | 
				
			||||||
        if ($privateKey === false) {
 | 
					 | 
				
			||||||
            self::removeFollow($dbh, $sourceUser, $fedUser->id);
 | 
					 | 
				
			||||||
            throw new \Exception('Failed to get private key');
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $pkeyId = openssl_pkey_get_private($privateKey);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if ($pkeyId === false) {
 | 
					 | 
				
			||||||
            self::removeFollow($dbh, $sourceUser, $fedUser->id);
 | 
					 | 
				
			||||||
            throw new \Exception('Invalid private key');
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        openssl_sign($signatureString, $signature, $pkeyId, OPENSSL_ALGO_SHA256);
 | 
					 | 
				
			||||||
        $signature_b64 = base64_encode($signature);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Build keyId (public key ID from your actor object)
 | 
					 | 
				
			||||||
        $keyId = 'https://' . $host . '/' . $user->id . '#main-key';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $signatureHeader = 'keyId="' . $keyId . '",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="' . $signature_b64 . '"';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $ch = curl_init($inboxUrl);
 | 
					 | 
				
			||||||
        if ($ch === false) {
 | 
					 | 
				
			||||||
            self::removeFollow($dbh, $sourceUser, $fedUser->id);
 | 
					 | 
				
			||||||
            throw new \Exception('Failed to initialize cURL');
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $headers = [
 | 
					 | 
				
			||||||
            'Host: ' . $extHost,
 | 
					 | 
				
			||||||
            'Date: ' . $date,
 | 
					 | 
				
			||||||
            'Digest: ' . $digest,
 | 
					 | 
				
			||||||
            'Content-Type: application/activity+json',
 | 
					 | 
				
			||||||
            'Signature: ' . $signatureHeader,
 | 
					 | 
				
			||||||
            'Accept: application/activity+json',
 | 
					 | 
				
			||||||
        ];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
 | 
					 | 
				
			||||||
        curl_setopt($ch, CURLOPT_POST, true);
 | 
					 | 
				
			||||||
        curl_setopt($ch, CURLOPT_POSTFIELDS, $json);
 | 
					 | 
				
			||||||
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
 | 
					 | 
				
			||||||
        $response = curl_exec($ch);
 | 
					 | 
				
			||||||
        curl_close($ch);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Log the response for debugging if needed
 | 
					 | 
				
			||||||
        if ($response === false) {
 | 
					 | 
				
			||||||
            self::removeFollow($dbh, $sourceUser, $fedUser->id);
 | 
					 | 
				
			||||||
            throw new \Exception("Failed to send Follow activity: " . curl_error($ch));
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            $httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
 | 
					 | 
				
			||||||
            if ($httpcode != 200 && $httpcode != 202) {
 | 
					 | 
				
			||||||
                self::removeFollow($dbh, $sourceUser, $fedUser->id);
 | 
					 | 
				
			||||||
                throw new \Exception("Unexpected HTTP code $httpcode: $response");
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return $idUrl;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * add follow
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param \mysqli $dbh database handle
 | 
					 | 
				
			||||||
     * @param string $sourceUser source user id
 | 
					 | 
				
			||||||
     * @param string $targetUserId target user id
 | 
					 | 
				
			||||||
     * @param string $host the host for generating the follow ID
 | 
					 | 
				
			||||||
     * @return string|false the generated follow ID on success, false on failure
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public static function addFollow($dbh, $sourceUser, $targetUserId, $host)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        // Check if we already follow this user
 | 
					 | 
				
			||||||
        $sql = 'select id from follows where source_user = ? and target_user = ?';
 | 
					 | 
				
			||||||
        $stmt = $dbh->prepare($sql);
 | 
					 | 
				
			||||||
        if ($stmt === false) {
 | 
					 | 
				
			||||||
            throw new \Federator\Exceptions\ServerError("Followers::addFollow Failed to prepare statement");
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $stmt->bind_param("ss", $sourceUser, $targetUserId);
 | 
					 | 
				
			||||||
        $foundId = 0;
 | 
					 | 
				
			||||||
        $ret = $stmt->bind_result($foundId);
 | 
					 | 
				
			||||||
        $stmt->execute();
 | 
					 | 
				
			||||||
        if ($ret) {
 | 
					 | 
				
			||||||
            $stmt->fetch();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $stmt->close();
 | 
					 | 
				
			||||||
        if ($foundId != 0) {
 | 
					 | 
				
			||||||
            return false; // Already following this user
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Generate a unique ID for the follow relationship
 | 
					 | 
				
			||||||
        do {
 | 
					 | 
				
			||||||
            $id = bin2hex(openssl_random_pseudo_bytes(16));
 | 
					 | 
				
			||||||
            $idurl = 'https://' . $host . '/' . $sourceUser . '/' . $id;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            // Check if the generated ID is unique
 | 
					 | 
				
			||||||
            $sql = 'select id from follows where id = ?';
 | 
					 | 
				
			||||||
            $stmt = $dbh->prepare($sql);
 | 
					 | 
				
			||||||
            if ($stmt === false) {
 | 
					 | 
				
			||||||
                throw new \Federator\Exceptions\ServerError("Followers::addFollow Failed to prepare id-check statement");
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            $stmt->bind_param("s", $idurl);
 | 
					 | 
				
			||||||
            $foundId = 0;
 | 
					 | 
				
			||||||
            $ret = $stmt->bind_result($foundId);
 | 
					 | 
				
			||||||
            $stmt->execute();
 | 
					 | 
				
			||||||
            if ($ret) {
 | 
					 | 
				
			||||||
                $stmt->fetch();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            $stmt->close();
 | 
					 | 
				
			||||||
        } while ($foundId > 0);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Add follow with created_at timestamp
 | 
					 | 
				
			||||||
        $sql = 'insert into follows (id, source_user, target_user, created_at) values (?, ?, ?, NOW())';
 | 
					 | 
				
			||||||
        $stmt = $dbh->prepare($sql);
 | 
					 | 
				
			||||||
        if ($stmt === false) {
 | 
					 | 
				
			||||||
            throw new \Federator\Exceptions\ServerError("Followers::addFollow Failed to prepare insert statement");
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $stmt->bind_param("sss", $idurl, $sourceUser, $targetUserId);
 | 
					 | 
				
			||||||
        $stmt->execute();
 | 
					 | 
				
			||||||
        $stmt->close();
 | 
					 | 
				
			||||||
        return $idurl; // Return the generated follow ID
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * add follow
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param \mysqli $dbh database handle
 | 
					 | 
				
			||||||
     * @param string $followId the follow ID to use (should be an external url)
 | 
					 | 
				
			||||||
     * @param string $sourceUserId source user id
 | 
					 | 
				
			||||||
     * @param string $targetUserId target user id
 | 
					 | 
				
			||||||
     * @return boolean true on success, false on failure
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public static function addExternalFollow($dbh, $followId, $sourceUserId, $targetUserId)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        // Check if we already follow this user
 | 
					 | 
				
			||||||
        $sql = 'select id from follows where source_user = ? and target_user = ?';
 | 
					 | 
				
			||||||
        $stmt = $dbh->prepare($sql);
 | 
					 | 
				
			||||||
        if ($stmt === false) {
 | 
					 | 
				
			||||||
            throw new \Federator\Exceptions\ServerError("Followers::addExternalFollow Failed to prepare statement");
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $stmt->bind_param("ss", $sourceUserId, $targetUserId);
 | 
					 | 
				
			||||||
        $foundId = 0;
 | 
					 | 
				
			||||||
        $ret = $stmt->bind_result($foundId);
 | 
					 | 
				
			||||||
        $stmt->execute();
 | 
					 | 
				
			||||||
        if ($ret) {
 | 
					 | 
				
			||||||
            $stmt->fetch();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $stmt->close();
 | 
					 | 
				
			||||||
        if ($foundId != 0) {
 | 
					 | 
				
			||||||
            return false; // Already following this user
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Add follow with created_at timestamp
 | 
					 | 
				
			||||||
        $sql = 'insert into follows (id, source_user, target_user, created_at) values (?, ?, ?, NOW())';
 | 
					 | 
				
			||||||
        $stmt = $dbh->prepare($sql);
 | 
					 | 
				
			||||||
        if ($stmt === false) {
 | 
					 | 
				
			||||||
            throw new \Federator\Exceptions\ServerError("Followers::addExternalFollow Failed to prepare insert statement");
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $stmt->bind_param("sss", $followId, $sourceUserId, $targetUserId);
 | 
					 | 
				
			||||||
        $stmt->execute();
 | 
					 | 
				
			||||||
        $stmt->close();
 | 
					 | 
				
			||||||
        return true;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * generate new follow id
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param \mysqli $dbh database handle
 | 
					 | 
				
			||||||
     * @param string $hostUrl the host URL (e.g. federator URL)
 | 
					 | 
				
			||||||
     * @return string the new follow id
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public static function generateNewFollowId($dbh, $hostUrl)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        // Generate a new unique follow ID
 | 
					 | 
				
			||||||
        do {
 | 
					 | 
				
			||||||
            $newId = bin2hex(openssl_random_pseudo_bytes(16));
 | 
					 | 
				
			||||||
            $newIdUrl = $hostUrl . '/' . $newId;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            // Check if the generated ID is unique
 | 
					 | 
				
			||||||
            $sql = 'select id from follows where id = ?';
 | 
					 | 
				
			||||||
            $stmt = $dbh->prepare($sql);
 | 
					 | 
				
			||||||
            if ($stmt === false) {
 | 
					 | 
				
			||||||
                throw new \Federator\Exceptions\ServerError("Followers::generateNewFollowId Failed to prepare id-check statement");
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            $stmt->bind_param("s", $newIdUrl);
 | 
					 | 
				
			||||||
            $foundId = 0;
 | 
					 | 
				
			||||||
            $ret = $stmt->bind_result($foundId);
 | 
					 | 
				
			||||||
            $stmt->execute();
 | 
					 | 
				
			||||||
            if ($ret) {
 | 
					 | 
				
			||||||
                $stmt->fetch();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            $stmt->close();
 | 
					 | 
				
			||||||
        } while ($foundId > 0);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return $newIdUrl;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * remove follow
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param \mysqli $dbh database handle
 | 
					 | 
				
			||||||
     * @param string $sourceUser source user id
 | 
					 | 
				
			||||||
     * @param string $targetUserId target user id
 | 
					 | 
				
			||||||
     * @return string|false removed followId on success, false on failure
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public static function removeFollow($dbh, $sourceUser, $targetUserId)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        // Combine retrieval and removal in one query using MySQL's RETURNING (if supported)
 | 
					 | 
				
			||||||
        $sql = 'delete from follows where source_user = ? and target_user = ? RETURNING id';
 | 
					 | 
				
			||||||
        $stmt = $dbh->prepare($sql);
 | 
					 | 
				
			||||||
        if ($stmt !== false) {
 | 
					 | 
				
			||||||
            $stmt->bind_param("ss", $sourceUser, $targetUserId);
 | 
					 | 
				
			||||||
            if ($stmt->execute()) {
 | 
					 | 
				
			||||||
                $stmt->bind_result($followId);
 | 
					 | 
				
			||||||
                if ($stmt->fetch() === true) {
 | 
					 | 
				
			||||||
                    $stmt->close();
 | 
					 | 
				
			||||||
                    if (!empty($followId)) {
 | 
					 | 
				
			||||||
                        return $followId;
 | 
					 | 
				
			||||||
                    } else {
 | 
					 | 
				
			||||||
                        return false;
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            $stmt->close();
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            // Fallback for MySQL versions that do not support RETURNING
 | 
					 | 
				
			||||||
            // First, fetch the id of the follow to be removed
 | 
					 | 
				
			||||||
            $sql = 'select id from follows where source_user = ? and target_user = ?';
 | 
					 | 
				
			||||||
            $stmt = $dbh->prepare($sql);
 | 
					 | 
				
			||||||
            if ($stmt === false) {
 | 
					 | 
				
			||||||
                throw new \Federator\Exceptions\ServerError("Followers::removeFollow Failed to prepare select statement");
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            $stmt->bind_param("ss", $sourceUser, $targetUserId);
 | 
					 | 
				
			||||||
            $stmt->execute();
 | 
					 | 
				
			||||||
            $stmt->bind_result($followId);
 | 
					 | 
				
			||||||
            $found = $stmt->fetch();
 | 
					 | 
				
			||||||
            $stmt->close();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if ($found === false || empty($followId)) {
 | 
					 | 
				
			||||||
                return false; // No such follow found
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            // Now, delete the row
 | 
					 | 
				
			||||||
            $sql = 'delete from follows where source_user = ? and target_user = ?';
 | 
					 | 
				
			||||||
            $stmt = $dbh->prepare($sql);
 | 
					 | 
				
			||||||
            if ($stmt === false) {
 | 
					 | 
				
			||||||
                throw new \Federator\Exceptions\ServerError("Followers::removeFollow Failed to prepare delete statement");
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            $stmt->bind_param("ss", $sourceUser, $targetUserId);
 | 
					 | 
				
			||||||
            $stmt->execute();
 | 
					 | 
				
			||||||
            $affectedRows = $stmt->affected_rows;
 | 
					 | 
				
			||||||
            $stmt->close();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            return $affectedRows > 0 ? $followId : false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return false;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -13,330 +13,47 @@ namespace Federator\DIO;
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
class Posts
 | 
					class Posts
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * get posts by user
 | 
					     * get posts by user
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @param \mysqli $dbh @unused-param
 | 
					     * @param \mysqli $dbh @unused-param
 | 
				
			||||||
     *          database handle
 | 
					     *          database handle
 | 
				
			||||||
     * @param string $userid
 | 
					     * @param string $id
 | 
				
			||||||
     *          user id
 | 
					     *          user id
 | 
				
			||||||
     * @param \Federator\Connector\Connector $connector
 | 
					     * @param \Federator\Connector\Connector $connector
 | 
				
			||||||
     *          connector to fetch use with
 | 
					     *          connector to fetch use with
 | 
				
			||||||
     * @param \Federator\Cache\Cache|null $cache
 | 
					     * @param \Federator\Cache\Cache|null $cache
 | 
				
			||||||
     *          optional caching service
 | 
					     *          optional caching service
 | 
				
			||||||
     * @param int $min
 | 
					     * @param string $minId
 | 
				
			||||||
     *          minimum timestamp
 | 
					     *          minimum ID
 | 
				
			||||||
     * @param int $max
 | 
					     * @param string $maxId
 | 
				
			||||||
     *          maximum timestamp
 | 
					     *          maximum ID
 | 
				
			||||||
     * @param int $limit
 | 
					     * @return \Federator\Data\ActivityPub\Common\APObject[]
 | 
				
			||||||
     *          maximum number of results
 | 
					 | 
				
			||||||
     * @return \Federator\Data\ActivityPub\Common\Activity[]
 | 
					 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public static function getPostsByUser($dbh, $userid, $connector, $cache, $min, $max, $limit)
 | 
					    public static function getPostsByUser($dbh, $id, $connector, $cache, $minId, $maxId)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        // ask cache
 | 
					        // ask cache
 | 
				
			||||||
        if ($cache !== null) {
 | 
					        if ($cache !== null) {
 | 
				
			||||||
            $posts = $cache->getRemotePostsByUser($userid, $min, $max, $limit);
 | 
					            $posts = $cache->getRemotePostsByUser($id, $minId, $maxId);
 | 
				
			||||||
            if ($posts !== false) {
 | 
					            if ($posts !== false) {
 | 
				
			||||||
                return $posts;
 | 
					                return $posts;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        $posts = self::getPostsFromDb($dbh, $userid, $min, $max, $limit);
 | 
					 | 
				
			||||||
        if ($posts === false) {
 | 
					 | 
				
			||||||
            $posts = [];
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Only override $min if we found posts in our DB
 | 
					 | 
				
			||||||
        $remoteMin = $min;
 | 
					 | 
				
			||||||
        if (!empty($posts)) {
 | 
					 | 
				
			||||||
            // Find the latest published date in the DB posts
 | 
					 | 
				
			||||||
            $latestPublished = null;
 | 
					 | 
				
			||||||
            foreach ($posts as $post) {
 | 
					 | 
				
			||||||
                $published = $post->getPublished();
 | 
					 | 
				
			||||||
                if ($published != null) {
 | 
					 | 
				
			||||||
                    if ($latestPublished === null || $published > $latestPublished) {
 | 
					 | 
				
			||||||
                        $latestPublished = $published;
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            if ($latestPublished !== null) {
 | 
					 | 
				
			||||||
                $remoteMin = $latestPublished;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Fetch newer posts from connector (if any) if max is not set and limit not reached
 | 
					 | 
				
			||||||
        if ($max == 0 && sizeof($posts) < $limit) {
 | 
					 | 
				
			||||||
            $newPosts = $connector->getRemotePostsByUser($userid, $remoteMin, $max, $limit);
 | 
					 | 
				
			||||||
            if ($newPosts !== false && is_array($newPosts)) {
 | 
					 | 
				
			||||||
                // Merge new posts with DB posts, avoiding duplicates by ID
 | 
					 | 
				
			||||||
                $existingIds = [];
 | 
					 | 
				
			||||||
                foreach ($posts as $post) {
 | 
					 | 
				
			||||||
                    $existingIds[$post->getID()] = true;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                foreach ($newPosts as $newPost) {
 | 
					 | 
				
			||||||
                    if (!isset($existingIds[$newPost->getID()])) {
 | 
					 | 
				
			||||||
                        if ($newPost->getID() !== "") {
 | 
					 | 
				
			||||||
                            self::savePost($dbh, $userid, $newPost);
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                        if (sizeof($posts) < $limit) {
 | 
					 | 
				
			||||||
                            $posts[] = $newPost;
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $originUrl = 'localhost';
 | 
					 | 
				
			||||||
        if (isset($_SERVER['HTTP_HOST'])) {
 | 
					 | 
				
			||||||
            $originUrl = $_SERVER['HTTP_HOST']; // origin of our request - e.g. mastodon
 | 
					 | 
				
			||||||
        } elseif (isset($_SERVER['HTTP_ORIGIN'])) {
 | 
					 | 
				
			||||||
            $origin = $_SERVER['HTTP_ORIGIN'];
 | 
					 | 
				
			||||||
            $parsed = parse_url($origin);
 | 
					 | 
				
			||||||
            if (isset($parsed) && isset($parsed['host'])) {
 | 
					 | 
				
			||||||
                $parsedHost = $parsed['host'];
 | 
					 | 
				
			||||||
                if (is_string($parsedHost) && $parsedHost !== "") {
 | 
					 | 
				
			||||||
                    $originUrl = $parsedHost;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if (!isset($originUrl) || $originUrl === "") {
 | 
					 | 
				
			||||||
            $originUrl = 'localhost'; // Fallback to localhost if no origin is set
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // optionally convert from article to note
 | 
					 | 
				
			||||||
        foreach ($posts as $post) {
 | 
					 | 
				
			||||||
            switch (strtolower($post->getType())) {
 | 
					 | 
				
			||||||
                case 'undo':
 | 
					 | 
				
			||||||
                    $object = $post->getObject();
 | 
					 | 
				
			||||||
                    if (is_object($object)) {
 | 
					 | 
				
			||||||
                        if (strtolower($object->getType()) === 'article') {
 | 
					 | 
				
			||||||
                            if ($object instanceof \Federator\Data\ActivityPub\Common\Article) {
 | 
					 | 
				
			||||||
                                $object = \Federator\DIO\Article::conditionalConvertToNote($object, $originUrl);
 | 
					 | 
				
			||||||
                                $post->setObject($object);
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    break;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                case 'create':
 | 
					 | 
				
			||||||
                case 'update':
 | 
					 | 
				
			||||||
                    $object = $post->getObject();
 | 
					 | 
				
			||||||
                    if (is_object($object)) {
 | 
					 | 
				
			||||||
                        if (strtolower($object->getType()) === 'article') {
 | 
					 | 
				
			||||||
                            if ($object instanceof \Federator\Data\ActivityPub\Common\Article) {
 | 
					 | 
				
			||||||
                                $object = \Federator\DIO\Article::conditionalConvertToNote($object, $originUrl);
 | 
					 | 
				
			||||||
                                $post->setObject($object);
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    break;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                default:
 | 
					 | 
				
			||||||
                    break;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if ($cache !== null) {
 | 
					 | 
				
			||||||
            $cache->saveRemotePostsByUser($userid, $min, $max, $limit, $posts);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return $posts;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Get posts for a user from the DB (optionally by date)
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param \mysqli $dbh
 | 
					 | 
				
			||||||
     * @param string $userId
 | 
					 | 
				
			||||||
     * @param int $min min timestamp
 | 
					 | 
				
			||||||
     * @param int $max max timestamp
 | 
					 | 
				
			||||||
     * @param int $limit
 | 
					 | 
				
			||||||
     * @return \Federator\Data\ActivityPub\Common\Activity[]|false
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public static function getPostsFromDb($dbh, $userId, $min, $max, $limit = 20)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        $sql = 'SELECT `id`, `url`, `user_id`, `actor`, `type`, `object`, `to`, `cc`, unix_timestamp(`published`) as published FROM posts WHERE user_id = ?';
 | 
					 | 
				
			||||||
        $params = [$userId];
 | 
					 | 
				
			||||||
        $types = 's';
 | 
					 | 
				
			||||||
        if ($min > 0) {
 | 
					 | 
				
			||||||
            $sql .= ' AND published >= from_unixtime(?)';
 | 
					 | 
				
			||||||
            $params[] = $min;
 | 
					 | 
				
			||||||
            $types .= 's';
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if ($max > 0) {
 | 
					 | 
				
			||||||
            $sql .= ' AND published <= from_unixtime(?)';
 | 
					 | 
				
			||||||
            $params[] = $max;
 | 
					 | 
				
			||||||
            $types .= 's';
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $sql .= ' ORDER BY published DESC LIMIT ' . $limit;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $stmt = $dbh->prepare($sql);
 | 
					 | 
				
			||||||
        if ($stmt === false) {
 | 
					 | 
				
			||||||
            throw new \Federator\Exceptions\ServerError();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $stmt->bind_param($types, ...$params);
 | 
					 | 
				
			||||||
        $stmt->execute();
 | 
					 | 
				
			||||||
        $result = $stmt->get_result();
 | 
					 | 
				
			||||||
        if (!($result instanceof \mysqli_result)) {
 | 
					 | 
				
			||||||
            $stmt->close();
 | 
					 | 
				
			||||||
            return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $posts = [];
 | 
					        $posts = [];
 | 
				
			||||||
        while ($row = $result->fetch_assoc()) {
 | 
					        // TODO: check our db
 | 
				
			||||||
            if (isset($row['to']) && $row['to'] !== null) {
 | 
					
 | 
				
			||||||
                $row['to'] = json_decode($row['to'], true);
 | 
					        if ($posts === []) {
 | 
				
			||||||
            }
 | 
					            // ask connector for user-id
 | 
				
			||||||
            if (isset($row['cc']) && $row['cc'] !== null) {
 | 
					            $posts = $connector->getRemotePostsByUser($id, $minId, $maxId);
 | 
				
			||||||
                $row['cc'] = json_decode($row['cc'], true);
 | 
					            if ($posts === false) {
 | 
				
			||||||
            }
 | 
					                $posts = [];
 | 
				
			||||||
            if (isset($row['object']) && $row['object'] !== null) {
 | 
					 | 
				
			||||||
                $decoded = json_decode($row['object'], true);
 | 
					 | 
				
			||||||
                // Only use decoded value if it's an array/object
 | 
					 | 
				
			||||||
                if (is_array($decoded)) {
 | 
					 | 
				
			||||||
                    $row['object'] = $decoded;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            if (isset($row['published']) && $row['published'] !== null) {
 | 
					 | 
				
			||||||
                // If it's numeric, keep as int. If it's a string, try to parse as ISO 8601.
 | 
					 | 
				
			||||||
                if (!is_numeric($row['published'])) {
 | 
					 | 
				
			||||||
                    // Try to parse as datetime string
 | 
					 | 
				
			||||||
                    $timestamp = strtotime($row['published']);
 | 
					 | 
				
			||||||
                    $row['published'] = $timestamp !== false ? $timestamp : null;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            $activity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($row);
 | 
					 | 
				
			||||||
            if ($activity !== false) {
 | 
					 | 
				
			||||||
                $posts[] = $activity;
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        $stmt->close();
 | 
					        // save posts to DB
 | 
				
			||||||
 | 
					        if ($cache !== null) {
 | 
				
			||||||
 | 
					            $cache->saveRemotePostsByUser($id, $posts);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        return $posts;
 | 
					        return $posts;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Save a post (insert or update)
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param \mysqli $dbh
 | 
					 | 
				
			||||||
     * @param string $userId
 | 
					 | 
				
			||||||
     * @param \Federator\Data\ActivityPub\Common\Activity $post
 | 
					 | 
				
			||||||
     * @param string|null $articleId the original id of the article
 | 
					 | 
				
			||||||
     *                           (used to identify the source article in the remote system)
 | 
					 | 
				
			||||||
     * @return bool
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public static function savePost($dbh, $userId, $post, $articleId = null)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        $sql = 'INSERT INTO posts (
 | 
					 | 
				
			||||||
                `id`, `url`, `user_id`, `actor`, `type`, `object`, `to`, `cc`, `published`, `article_id`
 | 
					 | 
				
			||||||
            ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
 | 
					 | 
				
			||||||
            ON DUPLICATE KEY UPDATE
 | 
					 | 
				
			||||||
                `url` = VALUES(`url`),
 | 
					 | 
				
			||||||
                `user_id` = VALUES(`user_id`),
 | 
					 | 
				
			||||||
                `actor` = VALUES(`actor`),
 | 
					 | 
				
			||||||
                `type` = VALUES(`type`),
 | 
					 | 
				
			||||||
                `object` = VALUES(`object`),
 | 
					 | 
				
			||||||
                `to` = VALUES(`to`),
 | 
					 | 
				
			||||||
                `cc` = VALUES(`cc`),
 | 
					 | 
				
			||||||
                `published` = VALUES(`published`),
 | 
					 | 
				
			||||||
                `article_id` = VALUES(`article_id`)';
 | 
					 | 
				
			||||||
        $stmt = $dbh->prepare($sql);
 | 
					 | 
				
			||||||
        if ($stmt === false) {
 | 
					 | 
				
			||||||
            throw new \Federator\Exceptions\ServerError();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $id = $post->getID();
 | 
					 | 
				
			||||||
        $url = $post->getUrl();
 | 
					 | 
				
			||||||
        $actor = $post->getAActor();
 | 
					 | 
				
			||||||
        $type = $post->getType();
 | 
					 | 
				
			||||||
        $object = $post->getObject();
 | 
					 | 
				
			||||||
        $objectJson = ($object instanceof \Federator\Data\ActivityPub\Common\APObject)
 | 
					 | 
				
			||||||
            ? json_encode($object)
 | 
					 | 
				
			||||||
            : $object;
 | 
					 | 
				
			||||||
        if ($objectJson === false) {
 | 
					 | 
				
			||||||
            $objectJson = null;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if (is_object($object)) {
 | 
					 | 
				
			||||||
            $id = $object->getID();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $to = $post->getTo();
 | 
					 | 
				
			||||||
        $cc = $post->getCC();
 | 
					 | 
				
			||||||
        $toJson = is_array($to) ? json_encode($to) : (is_string($to) ? json_encode([$to]) : null);
 | 
					 | 
				
			||||||
        $ccJson = is_array($cc) ? json_encode($cc) : (is_string($cc) ? json_encode([$cc]) : null);
 | 
					 | 
				
			||||||
        $published = $post->getPublished();
 | 
					 | 
				
			||||||
        $publishedStr = $published ? gmdate('Y-m-d H:i:s', $published) : gmdate('Y-m-d H:i:s');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $stmt->bind_param(
 | 
					 | 
				
			||||||
            "ssssssssss",
 | 
					 | 
				
			||||||
            $id,
 | 
					 | 
				
			||||||
            $url,
 | 
					 | 
				
			||||||
            $userId,
 | 
					 | 
				
			||||||
            $actor,
 | 
					 | 
				
			||||||
            $type,
 | 
					 | 
				
			||||||
            $objectJson,
 | 
					 | 
				
			||||||
            $toJson,
 | 
					 | 
				
			||||||
            $ccJson,
 | 
					 | 
				
			||||||
            $publishedStr,
 | 
					 | 
				
			||||||
            $articleId,
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        $result = $stmt->execute();
 | 
					 | 
				
			||||||
        $stmt->close();
 | 
					 | 
				
			||||||
        return $result;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Delete a post
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param \mysqli $dbh
 | 
					 | 
				
			||||||
     * @param string $id   The post ID
 | 
					 | 
				
			||||||
     * @return bool
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public static function deletePost($dbh, $id)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        $sql = 'delete from posts where id = ?';
 | 
					 | 
				
			||||||
        $stmt = $dbh->prepare($sql);
 | 
					 | 
				
			||||||
        if ($stmt === false) {
 | 
					 | 
				
			||||||
            throw new \Federator\Exceptions\ServerError();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $stmt->bind_param("s", $id);
 | 
					 | 
				
			||||||
        $stmt->execute();
 | 
					 | 
				
			||||||
        $affectedRows = $stmt->affected_rows;
 | 
					 | 
				
			||||||
        $stmt->close();
 | 
					 | 
				
			||||||
        return $affectedRows > 0;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /** retrieve original article id of post
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param \mysqli $dbh
 | 
					 | 
				
			||||||
     * @param \Federator\Data\ActivityPub\Common\Activity $post
 | 
					 | 
				
			||||||
     * @return string|null
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public static function getOriginalArticleId($dbh, $post)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        $sql = 'SELECT `article_id` FROM posts WHERE id = ?';
 | 
					 | 
				
			||||||
        $stmt = $dbh->prepare($sql);
 | 
					 | 
				
			||||||
        if ($stmt === false) {
 | 
					 | 
				
			||||||
            throw new \Federator\Exceptions\ServerError();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $id = $post->getID();
 | 
					 | 
				
			||||||
        $object = $post->getObject();
 | 
					 | 
				
			||||||
        if (is_object($object)) {
 | 
					 | 
				
			||||||
            $inReplyTo = $object->getInReplyTo();
 | 
					 | 
				
			||||||
            if ($inReplyTo !== "") {
 | 
					 | 
				
			||||||
                $id = $inReplyTo; // Use inReplyTo as ID if it's a string
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                $id = $object->getObject();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        } elseif (is_string($object)) {
 | 
					 | 
				
			||||||
            $id = $object; // If object is a string, use it directly
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $stmt->bind_param("s", $id);
 | 
					 | 
				
			||||||
        $articleId = null;
 | 
					 | 
				
			||||||
        $ret = $stmt->bind_result($articleId);
 | 
					 | 
				
			||||||
        $stmt->execute();
 | 
					 | 
				
			||||||
        if ($ret) {
 | 
					 | 
				
			||||||
            $stmt->fetch();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $stmt->close();
 | 
					 | 
				
			||||||
        return $articleId;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -17,7 +17,8 @@ class Stats
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * get remote stats
 | 
					     * get remote stats
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @param \Federator\Main $main main instance
 | 
					     * @param \Federator\Main $main
 | 
				
			||||||
 | 
					     *          main instance
 | 
				
			||||||
     * @return \Federator\Data\Stats
 | 
					     * @return \Federator\Data\Stats
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public static function getStats($main)
 | 
					    public static function getStats($main)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -26,7 +26,7 @@ class User
 | 
				
			||||||
        $sql = 'select unix_timestamp(`validuntil`) from users where id=?';
 | 
					        $sql = 'select unix_timestamp(`validuntil`) from users where id=?';
 | 
				
			||||||
        $stmt = $dbh->prepare($sql);
 | 
					        $stmt = $dbh->prepare($sql);
 | 
				
			||||||
        if ($stmt === false) {
 | 
					        if ($stmt === false) {
 | 
				
			||||||
            throw new \Federator\Exceptions\ServerError("User::addLocalUser Failed to prepare statement");
 | 
					            throw new \Federator\Exceptions\ServerError();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        $stmt->bind_param("s", $_user);
 | 
					        $stmt->bind_param("s", $_user);
 | 
				
			||||||
        $validuntil = 0;
 | 
					        $validuntil = 0;
 | 
				
			||||||
| 
						 | 
					@ -42,7 +42,6 @@ class User
 | 
				
			||||||
                throw new \Federator\Exceptions\ServerError();
 | 
					                throw new \Federator\Exceptions\ServerError();
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            $public = openssl_pkey_get_details($private_key)['key'];
 | 
					            $public = openssl_pkey_get_details($private_key)['key'];
 | 
				
			||||||
            $user->publicKey = $public;
 | 
					 | 
				
			||||||
            $private = '';
 | 
					            $private = '';
 | 
				
			||||||
            openssl_pkey_export($private_key, $private);
 | 
					            openssl_pkey_export($private_key, $private);
 | 
				
			||||||
            $sql = 'insert into users (id, externalid, rsapublic, rsaprivate, validuntil,';
 | 
					            $sql = 'insert into users (id, externalid, rsapublic, rsaprivate, validuntil,';
 | 
				
			||||||
| 
						 | 
					@ -50,7 +49,7 @@ class User
 | 
				
			||||||
            $sql .= ' values (?, ?, ?, ?, now() + interval 1 day, ?, ?, ?, ?, ?, ?, ?, ?)';
 | 
					            $sql .= ' values (?, ?, ?, ?, now() + interval 1 day, ?, ?, ?, ?, ?, ?, ?, ?)';
 | 
				
			||||||
            $stmt = $dbh->prepare($sql);
 | 
					            $stmt = $dbh->prepare($sql);
 | 
				
			||||||
            if ($stmt === false) {
 | 
					            if ($stmt === false) {
 | 
				
			||||||
                throw new \Federator\Exceptions\ServerError("User::addLocalUser Failed to prepare create statement");
 | 
					                throw new \Federator\Exceptions\ServerError();
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            $registered = gmdate('Y-m-d H:i:s', $user->registered);
 | 
					            $registered = gmdate('Y-m-d H:i:s', $user->registered);
 | 
				
			||||||
            $stmt->bind_param(
 | 
					            $stmt->bind_param(
 | 
				
			||||||
| 
						 | 
					@ -74,7 +73,7 @@ class User
 | 
				
			||||||
            $sql .= ' iconmediatype=?, iconurl=?, imagemediatype=?, imageurl=? where id=?';
 | 
					            $sql .= ' iconmediatype=?, iconurl=?, imagemediatype=?, imageurl=? where id=?';
 | 
				
			||||||
            $stmt = $dbh->prepare($sql);
 | 
					            $stmt = $dbh->prepare($sql);
 | 
				
			||||||
            if ($stmt === false) {
 | 
					            if ($stmt === false) {
 | 
				
			||||||
                throw new \Federator\Exceptions\ServerError("User::addLocalUser Failed to prepare update statement");
 | 
					                throw new \Federator\Exceptions\ServerError();
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            $registered = gmdate('Y-m-d H:i:s', $user->registered);
 | 
					            $registered = gmdate('Y-m-d H:i:s', $user->registered);
 | 
				
			||||||
            $stmt->bind_param(
 | 
					            $stmt->bind_param(
 | 
				
			||||||
| 
						 | 
					@ -101,42 +100,18 @@ class User
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * get private rsa key
 | 
					 | 
				
			||||||
     * @return string|false key or false
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public static function getrsaprivate(\mysqli $dbh, string $_user)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        $sql = "select rsaprivate from users where id=?";
 | 
					 | 
				
			||||||
        $stmt = $dbh->prepare($sql);
 | 
					 | 
				
			||||||
        if ($stmt === false) {
 | 
					 | 
				
			||||||
            throw new \Federator\Exceptions\ServerError("User::getrsaprivate Failed to prepare statement");
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $stmt->bind_param("s", $_user);
 | 
					 | 
				
			||||||
        $ret = $stmt->bind_result($rsaPrivateKey);
 | 
					 | 
				
			||||||
        $stmt->execute();
 | 
					 | 
				
			||||||
        if ($ret) {
 | 
					 | 
				
			||||||
            $stmt->fetch();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $stmt->close();
 | 
					 | 
				
			||||||
        if ($rsaPrivateKey !== null) {
 | 
					 | 
				
			||||||
            return $rsaPrivateKey;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return false;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * extend the given user with internal data
 | 
					     * extend the given user with internal data
 | 
				
			||||||
     * @param \mysqli $dbh database  handle
 | 
					     * @param \mysqli $dbh database  handle
 | 
				
			||||||
     * @param \Federator\Data\User $user user to extend
 | 
					     * @param \Federator\Data\User $user user to extend
 | 
				
			||||||
     * @param string $_user user/profile name
 | 
					     * @param string $_user user/profile name
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    protected static function extendUser(\mysqli $dbh, $user, $_user): void
 | 
					    protected static function extendUser(\mysqli $dbh, $user, $_user) : void
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $sql = 'select id,unix_timestamp(`validuntil`) from users where id=?';
 | 
					        $sql = 'select id,unix_timestamp(`validuntil`) from users where id=?';
 | 
				
			||||||
        $stmt = $dbh->prepare($sql);
 | 
					        $stmt = $dbh->prepare($sql);
 | 
				
			||||||
        if ($stmt === false) {
 | 
					        if ($stmt === false) {
 | 
				
			||||||
            throw new \Federator\Exceptions\ServerError("User::extendUser Failed to prepare statement");
 | 
					            throw new \Federator\Exceptions\ServerError();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        $stmt->bind_param("s", $_user);
 | 
					        $stmt->bind_param("s", $_user);
 | 
				
			||||||
        $validuntil = 0;
 | 
					        $validuntil = 0;
 | 
				
			||||||
| 
						 | 
					@ -170,7 +145,6 @@ 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);
 | 
				
			||||||
| 
						 | 
					@ -183,7 +157,7 @@ class User
 | 
				
			||||||
        $sql .= 'iconmediatype,iconurl,imagemediatype,imageurl from users where id=? and validuntil>=now()';
 | 
					        $sql .= 'iconmediatype,iconurl,imagemediatype,imageurl from users where id=? and validuntil>=now()';
 | 
				
			||||||
        $stmt = $dbh->prepare($sql);
 | 
					        $stmt = $dbh->prepare($sql);
 | 
				
			||||||
        if ($stmt === false) {
 | 
					        if ($stmt === false) {
 | 
				
			||||||
            throw new \Federator\Exceptions\ServerError("User::getUserByName Failed to prepare statement");
 | 
					            throw new \Federator\Exceptions\ServerError();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        $stmt->bind_param("s", $_name);
 | 
					        $stmt->bind_param("s", $_name);
 | 
				
			||||||
        $user = new \Federator\Data\User();
 | 
					        $user = new \Federator\Data\User();
 | 
				
			||||||
| 
						 | 
					@ -205,7 +179,6 @@ 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);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,97 +0,0 @@
 | 
				
			||||||
<?php
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
 | 
					 | 
				
			||||||
 * SPDX-License-Identifier: GPL-3.0-or-later
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * @author Yannis Vogel (vogeldevelopment)
 | 
					 | 
				
			||||||
 **/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace Federator\DIO;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * IO functions related to votes
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
class Votes
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Add a vote (like/dislike)
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param \mysqli $dbh
 | 
					 | 
				
			||||||
     * @param string $userId      The user who votes
 | 
					 | 
				
			||||||
     * @param string $targetId    The object being voted on (e.g., post id)
 | 
					 | 
				
			||||||
     * @param string $type        'like' or 'dislike'
 | 
					 | 
				
			||||||
     * @return string|false       The generated vote ID on success, false on failure
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public static function addVote($dbh, $userId, $targetId, $type)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        // Check if already voted
 | 
					 | 
				
			||||||
        $sql = 'SELECT id FROM votes WHERE user_id = ? AND target_id = ? AND type = ?';
 | 
					 | 
				
			||||||
        $stmt = $dbh->prepare($sql);
 | 
					 | 
				
			||||||
        if ($stmt === false) {
 | 
					 | 
				
			||||||
            throw new \Federator\Exceptions\ServerError();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $stmt->bind_param("sss", $userId, $targetId, $type);
 | 
					 | 
				
			||||||
        $foundId = 0;
 | 
					 | 
				
			||||||
        $ret = $stmt->bind_result($foundId);
 | 
					 | 
				
			||||||
        $stmt->execute();
 | 
					 | 
				
			||||||
        if ($ret) {
 | 
					 | 
				
			||||||
            $stmt->fetch();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $stmt->close();
 | 
					 | 
				
			||||||
        if ($foundId != 0) {
 | 
					 | 
				
			||||||
            return false; // Already voted
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Generate a unique ID for the vote
 | 
					 | 
				
			||||||
        do {
 | 
					 | 
				
			||||||
            $id = bin2hex(openssl_random_pseudo_bytes(16));
 | 
					 | 
				
			||||||
            // Check if the generated ID is unique
 | 
					 | 
				
			||||||
            $sql = 'SELECT id FROM votes WHERE id = ?';
 | 
					 | 
				
			||||||
            $stmt = $dbh->prepare($sql);
 | 
					 | 
				
			||||||
            if ($stmt === false) {
 | 
					 | 
				
			||||||
                throw new \Federator\Exceptions\ServerError();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            $stmt->bind_param("s", $id);
 | 
					 | 
				
			||||||
            $foundId = 0;
 | 
					 | 
				
			||||||
            $ret = $stmt->bind_result($foundId);
 | 
					 | 
				
			||||||
            $stmt->execute();
 | 
					 | 
				
			||||||
            if ($ret) {
 | 
					 | 
				
			||||||
                $stmt->fetch();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            $stmt->close();
 | 
					 | 
				
			||||||
        } while ($foundId > 0);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Add vote with created_at timestamp
 | 
					 | 
				
			||||||
        $sql = 'INSERT INTO votes (id, user_id, target_id, type, created_at) VALUES (?, ?, ?, ?, NOW())';
 | 
					 | 
				
			||||||
        $stmt = $dbh->prepare($sql);
 | 
					 | 
				
			||||||
        if ($stmt === false) {
 | 
					 | 
				
			||||||
            throw new \Federator\Exceptions\ServerError();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $stmt->bind_param("ssss", $id, $userId, $targetId, $type);
 | 
					 | 
				
			||||||
        $stmt->execute();
 | 
					 | 
				
			||||||
        $stmt->close();
 | 
					 | 
				
			||||||
        return $id; // Return the generated vote ID
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Remove a vote (like/dislike)
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param \mysqli $dbh
 | 
					 | 
				
			||||||
     * @param string $userId
 | 
					 | 
				
			||||||
     * @param string $targetId
 | 
					 | 
				
			||||||
     * @return bool             true on success
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public static function removeVote($dbh, $userId, $targetId)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        $sql = 'DELETE FROM votes WHERE user_id = ? AND target_id = ?';
 | 
					 | 
				
			||||||
        $stmt = $dbh->prepare($sql);
 | 
					 | 
				
			||||||
        if ($stmt === false) {
 | 
					 | 
				
			||||||
            throw new \Federator\Exceptions\ServerError();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $stmt->bind_param("ss", $userId, $targetId);
 | 
					 | 
				
			||||||
        $stmt->execute();
 | 
					 | 
				
			||||||
        $affectedRows = $stmt->affected_rows;
 | 
					 | 
				
			||||||
        $stmt->close();
 | 
					 | 
				
			||||||
        return $affectedRows > 0;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,70 +0,0 @@
 | 
				
			||||||
<?php
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace Federator\Jobs;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class InboxJob extends \Federator\Api
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    /** @var array<string, mixed> $args Arguments for the job */
 | 
					 | 
				
			||||||
    public $args = [];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * cache instance
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @var \Federator\Cache\Cache $cache
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    protected $cache;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * remote connector
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @var \Federator\Connector\Connector $connector
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    protected $connector = null;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * database instance
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @var \Mysqli $dbh
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    protected $dbh;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * constructor
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function __construct()
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        parent::__construct();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Set up environment for this job
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function setUp(): void
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        $this->openDatabase();
 | 
					 | 
				
			||||||
        $this->loadPlugins();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Perform the inbox job.
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @return bool true on success, false on failure
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function perform(): bool
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        error_log("InboxJob: Starting job");
 | 
					 | 
				
			||||||
        $user = $this->args['user'];
 | 
					 | 
				
			||||||
        $recipientId = $this->args['recipientId'];
 | 
					 | 
				
			||||||
        $activity = $this->args['activity'];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $inboxActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($activity);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if ($inboxActivity === false) {
 | 
					 | 
				
			||||||
            error_log("InboxJob: Failed to create inboxActivity from JSON");
 | 
					 | 
				
			||||||
            return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        \Federator\Api\FedUsers\Inbox::postForUser($this->dbh, $this->connector, $this->cache, $user, $recipientId, $inboxActivity);
 | 
					 | 
				
			||||||
        return true;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,74 +0,0 @@
 | 
				
			||||||
<?php
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace Federator\Jobs;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class NewContentJob extends \Federator\Api
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    /** @var array<string, mixed> $args Arguments for the job */
 | 
					 | 
				
			||||||
    public $args = [];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * cache instance
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @var \Federator\Cache\Cache $cache
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    protected $cache;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * remote connector
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @var \Federator\Connector\Connector $connector
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    protected $connector = null;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * database instance
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @var \Mysqli $dbh
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    protected $dbh;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * constructor
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function __construct()
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        parent::__construct();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Set up environment for this job
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function setUp(): void
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        $this->openDatabase();
 | 
					 | 
				
			||||||
        $this->loadPlugins();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Perform the inbox job.
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @return bool true on success, false on failure
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function perform(): bool
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        error_log("NewContentJob: Starting job");
 | 
					 | 
				
			||||||
        $user = $this->args['user'];
 | 
					 | 
				
			||||||
        $recipientId = $this->args['recipientId'];
 | 
					 | 
				
			||||||
        $activity = $this->args['activity'];
 | 
					 | 
				
			||||||
        $articleId = $this->args['articleId'] ?? null;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $activity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($activity);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if ($activity === false) {
 | 
					 | 
				
			||||||
            error_log("NewContentJob: Failed to create activity from JSON");
 | 
					 | 
				
			||||||
            return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $domain = $this->config['generic']['externaldomain'];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $ourUrl = 'https://' . $domain;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        \Federator\Api\V1\NewContent::postForUser($this->dbh, $this->connector, $this->cache, $ourUrl, $user, $recipientId, $activity, $articleId);
 | 
					 | 
				
			||||||
        return true;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -92,9 +92,12 @@ class Language
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if (! isset($this->lang[$group])) {
 | 
					        if (! isset($this->lang[$group])) {
 | 
				
			||||||
            $l = [];
 | 
					            $l = [];
 | 
				
			||||||
            $root = PROJECT_ROOT;
 | 
					            $root = $_SERVER['DOCUMENT_ROOT'];
 | 
				
			||||||
            if (@file_exists($root . '/lang/federator/' . $this->uselang . "/$group.inc")) {
 | 
					            if ($root === '') {
 | 
				
			||||||
                require($root . '/lang/federator/' . $this->uselang . "/$group.inc");
 | 
					                $root = '.';
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if (@file_exists($root . '../lang/federator/' . $this->uselang . "/$group.inc")) {
 | 
				
			||||||
 | 
					                require($root . '../lang/federator/' . $this->uselang . "/$group.inc");
 | 
				
			||||||
                $this->lang[$group] = $l;
 | 
					                $this->lang[$group] = $l;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
| 
						 | 
					@ -109,7 +112,7 @@ class Language
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            return $string;
 | 
					            return $string;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        $basedir = PROJECT_ROOT;
 | 
					        $basedir = $_SERVER['DOCUMENT_ROOT'] . '/../';
 | 
				
			||||||
        $fh = @fopen("$basedir/logs/missingtrans.txt", 'a');
 | 
					        $fh = @fopen("$basedir/logs/missingtrans.txt", 'a');
 | 
				
			||||||
        if ($fh !== false) {
 | 
					        if ($fh !== false) {
 | 
				
			||||||
            fwrite($fh, $this->uselang.":$group:$key\n");
 | 
					            fwrite($fh, $this->uselang.":$group:$key\n");
 | 
				
			||||||
| 
						 | 
					@ -129,7 +132,7 @@ class Language
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        if (! isset($this->lang[$group])) {
 | 
					        if (! isset($this->lang[$group])) {
 | 
				
			||||||
            $l = [];
 | 
					            $l = [];
 | 
				
			||||||
            require_once(PROJECT_ROOT . '/lang/' . $this->uselang . "/$group.inc");
 | 
					            require_once($_SERVER['DOCUMENT_ROOT'] . '/../lang/' . $this->uselang . "/$group.inc");
 | 
				
			||||||
            $this->lang[$group] = $l;
 | 
					            $this->lang[$group] = $l;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        // @phan-suppress-next-line PhanPartialTypeMismatchReturn
 | 
					        // @phan-suppress-next-line PhanPartialTypeMismatchReturn
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -33,12 +33,6 @@ class Main
 | 
				
			||||||
     * @var Connector\Connector $connector
 | 
					     * @var Connector\Connector $connector
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    protected $connector = null;
 | 
					    protected $connector = null;
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * remote host (f.e. https://contentnation.net)
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @var string $host
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    protected $host = null;
 | 
					 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * response content type
 | 
					     * response content type
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
| 
						 | 
					@ -78,9 +72,9 @@ class Main
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function __construct()
 | 
					    public function __construct()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        require_once(PROJECT_ROOT . '/vendor/autoload.php');
 | 
					        require_once($_SERVER['DOCUMENT_ROOT'] . '../vendor/autoload.php');
 | 
				
			||||||
        $this->responseCode = 200;
 | 
					        $this->responseCode = 200;
 | 
				
			||||||
        $rootDir = PROJECT_ROOT . '/';
 | 
					        $rootDir = $_SERVER['DOCUMENT_ROOT'] . '../';
 | 
				
			||||||
        $config = parse_ini_file($rootDir . 'config.ini', true);
 | 
					        $config = parse_ini_file($rootDir . 'config.ini', true);
 | 
				
			||||||
        if ($config !== false) {
 | 
					        if ($config !== false) {
 | 
				
			||||||
            $this->config = $config;
 | 
					            $this->config = $config;
 | 
				
			||||||
| 
						 | 
					@ -99,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);
 | 
				
			||||||
| 
						 | 
					@ -145,28 +139,20 @@ class Main
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        return $this->cache;
 | 
					        return $this->cache;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
         * get connector
 | 
					     * get connector
 | 
				
			||||||
         *
 | 
					     *
 | 
				
			||||||
         * @return \Federator\Connector\Connector
 | 
					     * @return \Federator\Connector\Connector
 | 
				
			||||||
         */
 | 
					     */
 | 
				
			||||||
    public function getConnector()
 | 
					    public function getConnector()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        return $this->connector;
 | 
					        return $this->connector;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
         * get host (f.e. https://contentnation.net)
 | 
					 | 
				
			||||||
         *
 | 
					 | 
				
			||||||
         * @return string
 | 
					 | 
				
			||||||
         */
 | 
					 | 
				
			||||||
    public function getHost()
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        return $this->host;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * get config
 | 
					     * get config
 | 
				
			||||||
     * @return array<string, mixed>
 | 
					     * @return Array<String, Mixed>
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function getConfig()
 | 
					    public function getConfig()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
| 
						 | 
					@ -186,10 +172,10 @@ 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 = PROJECT_ROOT . '/plugins/federator/';
 | 
					            $basepath = $_SERVER['DOCUMENT_ROOT'] . '../plugins/federator/';
 | 
				
			||||||
            $plugins = $this->config['plugins'];
 | 
					            $plugins = $this->config['plugins'];
 | 
				
			||||||
            foreach ($plugins as $name => $file) {
 | 
					            foreach ($plugins as $name => $file) {
 | 
				
			||||||
                require_once($basepath . $file);
 | 
					                require_once($basepath . $file);
 | 
				
			||||||
| 
						 | 
					@ -213,8 +199,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) {
 | 
				
			||||||
| 
						 | 
					@ -232,10 +218,10 @@ class Main
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function renderTemplate($template, $data)
 | 
					    public function renderTemplate($template, $data)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $rootDir = PROJECT_ROOT . '/';
 | 
					 | 
				
			||||||
        $smarty = new \Smarty\Smarty();
 | 
					        $smarty = new \Smarty\Smarty();
 | 
				
			||||||
        $smarty->setCompileDir($rootDir . $this->config['templates']['compiledir']);
 | 
					        $root = $_SERVER['DOCUMENT_ROOT'];
 | 
				
			||||||
        $smarty->setTemplateDir((string) realpath($rootDir . $this->config['templates']['path']));
 | 
					        $smarty->setCompileDir($root . $this->config['templates']['compiledir']);
 | 
				
			||||||
 | 
					        $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) {
 | 
				
			||||||
| 
						 | 
					@ -246,57 +232,27 @@ class Main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * set cache
 | 
					     * set cache
 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param \Federator\Cache\Cache $cache the new cache
 | 
					 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function setCache(Cache\Cache $cache): void
 | 
					    public function setCache(Cache\Cache $cache) : void
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $this->cache = $cache;
 | 
					        $this->cache = $cache;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * set connector
 | 
					     * set connector
 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param \Federator\Connector\Connector $connector the new connector
 | 
					 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function setConnector(Connector\Connector $connector) : void
 | 
					    public function setConnector(Connector\Connector $connector) : void
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        if (isset($this->connector)) {
 | 
					 | 
				
			||||||
            # echo "main::setConnector Setting new connector will override old one.\n"; // TODO CHANGE TO LOG WARNING
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $this->connector = $connector;
 | 
					        $this->connector = $connector;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * set host
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param string $host the new host url
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function setHost(string $host) : void
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        if (isset($this->host)) {
 | 
					 | 
				
			||||||
            # echo "main::setHost Setting new host will override old one.\n"; // TODO CHANGE TO LOG WARNING
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $this->host = $host;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * set content type
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param string $_type content type
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function setContentType($_type): void
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        $this->contentType = $_type;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * set response code
 | 
					     * set response code
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @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;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -314,7 +270,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);
 | 
				
			||||||
| 
						 | 
					@ -325,7 +281,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) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -24,13 +24,14 @@ class Maintenance
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        date_default_timezone_set("Europe/Berlin");
 | 
					        date_default_timezone_set("Europe/Berlin");
 | 
				
			||||||
        spl_autoload_register(static function (string $className) {
 | 
					        spl_autoload_register(static function (string $className) {
 | 
				
			||||||
            include PROJECT_ROOT . '/php/' . str_replace("\\", "/", strtolower($className)) . '.php';
 | 
					            $root = $_SERVER['DOCUMENT_ROOT'];
 | 
				
			||||||
 | 
					            include $root . '../php/' . str_replace("\\", "/", strtolower($className)) . '.php';
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
        if ($argc < 2) {
 | 
					        if ($argc < 2) {
 | 
				
			||||||
            self::printUsage();
 | 
					            self::printUsage();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        // pretend that we are running from web directory
 | 
					        // pretend that we are running from web directory
 | 
				
			||||||
        define('PROJECT_ROOT', dirname(__DIR__, 2));
 | 
					        $_SERVER['DOCUMENT_ROOT'] = realpath('../../htdocs') . '/';
 | 
				
			||||||
        $main = new \Federator\Main();
 | 
					        $main = new \Federator\Main();
 | 
				
			||||||
        switch ($argv[1]) {
 | 
					        switch ($argv[1]) {
 | 
				
			||||||
            case 'dbupgrade':
 | 
					            case 'dbupgrade':
 | 
				
			||||||
| 
						 | 
					@ -70,7 +71,7 @@ class Maintenance
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        echo "current version: $version\n";
 | 
					        echo "current version: $version\n";
 | 
				
			||||||
        $root = PROJECT_ROOT . '/';
 | 
					        $root = $_SERVER['DOCUMENT_ROOT'] . '../';
 | 
				
			||||||
        $updateFolder = opendir($root . 'sql');
 | 
					        $updateFolder = opendir($root . 'sql');
 | 
				
			||||||
        if ($updateFolder === false) {
 | 
					        if ($updateFolder === false) {
 | 
				
			||||||
            die();
 | 
					            die();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,199 +0,0 @@
 | 
				
			||||||
<?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;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * test functions
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
class Test
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * fetch outbox
 | 
					 | 
				
			||||||
     * @param string $_name user handle to fetch
 | 
					 | 
				
			||||||
     * @return void
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public static function fetchOutbox($_name)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        // make webfinger request
 | 
					 | 
				
			||||||
        $r = self::webfinger($_name);
 | 
					 | 
				
			||||||
        $outbox = $r['outbox'];
 | 
					 | 
				
			||||||
        $headers = ['Accept: application/activity+json'];
 | 
					 | 
				
			||||||
        [$response, $info] = \Federator\Main::getFromRemote($outbox, $headers);
 | 
					 | 
				
			||||||
        if ($info['http_code'] != 200) {
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $json = json_decode($response, true);
 | 
					 | 
				
			||||||
        if ($json === false) {
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $ap = \Federator\Data\ActivityPub\Factory::newFromJson($json, $response);
 | 
					 | 
				
			||||||
        if (!($ap instanceof \Federator\Data\ActivityPub\Common\OrderedCollection)) {
 | 
					 | 
				
			||||||
            echo "unsupport reply from $outbox\n";
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        // first and total items should be set
 | 
					 | 
				
			||||||
        $total = $ap->getTotalItems();
 | 
					 | 
				
			||||||
        echo "total items: " . $total;
 | 
					 | 
				
			||||||
        echo " first: " . $ap->getFirst() . "\n";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $page = $ap->getFirst();
 | 
					 | 
				
			||||||
        $count = 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        while ($count < $total) {
 | 
					 | 
				
			||||||
            echo "query $page\n";
 | 
					 | 
				
			||||||
            // query pages
 | 
					 | 
				
			||||||
            [$response, $info] = \Federator\Main::getFromRemote($page, $headers);
 | 
					 | 
				
			||||||
            if ($info['http_code'] != 200) {
 | 
					 | 
				
			||||||
                return;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            // echo "'$response'\n";
 | 
					 | 
				
			||||||
            $json = json_decode($response, true);
 | 
					 | 
				
			||||||
            $ap = \Federator\Data\ActivityPub\Factory::newFromJson($json, $response);
 | 
					 | 
				
			||||||
            if (!($ap instanceof \Federator\Data\ActivityPub\Common\OrderedCollectionPage)) {
 | 
					 | 
				
			||||||
                echo "unsupport reply from $page\n";
 | 
					 | 
				
			||||||
                return;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            $thisCount = $ap->getCount();
 | 
					 | 
				
			||||||
            echo "count: " . $thisCount . "\n";
 | 
					 | 
				
			||||||
            for ($i = 0; $i < $thisCount; ++$i) {
 | 
					 | 
				
			||||||
                $entry = $ap->get($i);
 | 
					 | 
				
			||||||
                if ($entry instanceof \Federator\Data\ActivityPub\Common\APObject) {
 | 
					 | 
				
			||||||
                    echo $entry->getID() . " " . $entry->getPublished() . "\n";
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            $count += $thisCount;
 | 
					 | 
				
			||||||
            $page = $ap->getNext();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        //print_r($ap);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * run test
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param int $argc number of arguments
 | 
					 | 
				
			||||||
     * @param string[] $argv arguments
 | 
					 | 
				
			||||||
     * @return void
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public static function run($argc, $argv)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        date_default_timezone_set("Europe/Berlin");
 | 
					 | 
				
			||||||
        spl_autoload_register(static function (string $className) {
 | 
					 | 
				
			||||||
            include PROJECT_ROOT . '/php/' . str_replace("\\", "/", strtolower($className)) . '.php';
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
        if ($argc < 2) {
 | 
					 | 
				
			||||||
            self::printUsage();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        // pretend that we are running from web directory
 | 
					 | 
				
			||||||
        define('PROJECT_ROOT', dirname(__DIR__, 2));
 | 
					 | 
				
			||||||
        $api = new \Federator\Api();
 | 
					 | 
				
			||||||
        $api->loadPlugins();
 | 
					 | 
				
			||||||
        $api->openDatabase();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        for ($i = 1; $i < $argc; ++$i) {
 | 
					 | 
				
			||||||
            switch ($argv[$i]) {
 | 
					 | 
				
			||||||
                case 'fetchoutbox':
 | 
					 | 
				
			||||||
                    self::fetchOutbox($argv[$i + 1]);
 | 
					 | 
				
			||||||
                    ++$i;
 | 
					 | 
				
			||||||
                    break;
 | 
					 | 
				
			||||||
                case 'upvote':
 | 
					 | 
				
			||||||
                    self::upvote($api, $argv[$i + 1]);
 | 
					 | 
				
			||||||
                    ++$i;
 | 
					 | 
				
			||||||
                    break;
 | 
					 | 
				
			||||||
                default:
 | 
					 | 
				
			||||||
                    self::printUsage();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * print usage of test tool
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @return void
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public static function printUsage()
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        echo "usage php test.php <command> <parameter> [<command> <parameter>...]\n";
 | 
					 | 
				
			||||||
        echo "command can be one of:\n";
 | 
					 | 
				
			||||||
        echo " fetchoutbox - fetch users outbox. parameter: username\n";
 | 
					 | 
				
			||||||
        echo " upvote - upvote. parameter: URL to upvote\n";
 | 
					 | 
				
			||||||
        echo " downvote - downvote. parameter: URL to downvote\n";
 | 
					 | 
				
			||||||
        echo " comment - comment. parameter: URL to comment, text to comment\n";
 | 
					 | 
				
			||||||
        echo " Run this after you updated the program files\n";
 | 
					 | 
				
			||||||
        exit();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * upvote given URL
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param \Federator\Api $api api instance
 | 
					 | 
				
			||||||
     * @param string $_url URL to upvote
 | 
					 | 
				
			||||||
     * @note uses hardcoded source
 | 
					 | 
				
			||||||
     * @return void
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public static function upvote($api, $_url)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        $dbh = $api->getDatabase();
 | 
					 | 
				
			||||||
        $inboxActivity = new \Federator\Data\ActivityPub\Common\Like();
 | 
					 | 
				
			||||||
        $inboxActivity->setAActor('https://mastodon.local/users/admin');
 | 
					 | 
				
			||||||
        $inboxActivity->setObject($_url);
 | 
					 | 
				
			||||||
        $inboxActivity->setID("https://mastodon.local/users/admin#like/" . md5($_url));
 | 
					 | 
				
			||||||
        \Federator\Api\FedUsers\Inbox::postForUser(
 | 
					 | 
				
			||||||
            $dbh,
 | 
					 | 
				
			||||||
            $api->getConnector(),
 | 
					 | 
				
			||||||
            null,
 | 
					 | 
				
			||||||
            'admin@mastodon.local',
 | 
					 | 
				
			||||||
            "grumpydevelop@192.168.178.143",
 | 
					 | 
				
			||||||
            $inboxActivity
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * do a webfinger request
 | 
					 | 
				
			||||||
     * @param string $_name name to query
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    private static function webfinger($_name): mixed
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        // make webfinger request
 | 
					 | 
				
			||||||
        if (preg_match("/^([^@]+)@(.*)$/", $_name, $matches) != 1) {
 | 
					 | 
				
			||||||
            echo "username is malformed";
 | 
					 | 
				
			||||||
            return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $remoteURL = 'https://' . $matches[2] . '/.well-known/webfinger?resource=acct:' . urlencode($_name);
 | 
					 | 
				
			||||||
        $headers = ['Accept: application/activity+json'];
 | 
					 | 
				
			||||||
        [$response, $info] = \Federator\Main::getFromRemote($remoteURL, $headers);
 | 
					 | 
				
			||||||
        if ($info['http_code'] != 200) {
 | 
					 | 
				
			||||||
            return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $r = json_decode($response, true);
 | 
					 | 
				
			||||||
        if (isset($r['links'])) {
 | 
					 | 
				
			||||||
            foreach ($r['links'] as $link) {
 | 
					 | 
				
			||||||
                if (isset($link['rel']) && $link['rel'] === 'self') {
 | 
					 | 
				
			||||||
                    $remoteURL = $link['href'];
 | 
					 | 
				
			||||||
                    break;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if (!isset($remoteURL)) {
 | 
					 | 
				
			||||||
            echo "FedUser::getUserByName Failed to find self link in webfinger for " . $_name . "\n";
 | 
					 | 
				
			||||||
            return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        // fetch the user
 | 
					 | 
				
			||||||
        $headers = ['Accept: application/activity+json'];
 | 
					 | 
				
			||||||
        [$response, $info] = \Federator\Main::getFromRemote($remoteURL, $headers);
 | 
					 | 
				
			||||||
        if ($info['http_code'] != 200) {
 | 
					 | 
				
			||||||
            echo "FedUser::getUserByName Failed to fetch user from remoteUrl for " . $_name . "\n";
 | 
					 | 
				
			||||||
            return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $r = json_decode($response, true);
 | 
					 | 
				
			||||||
        return $r;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Test::run($argc, $argv);
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,23 +0,0 @@
 | 
				
			||||||
<?php
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
define('PROJECT_ROOT', dirname(__DIR__, 3));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
require_once PROJECT_ROOT . '/vendor/autoload.php';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
$config = parse_ini_file(PROJECT_ROOT . '/rediscache.ini');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Set the Redis backend for Resque
 | 
					 | 
				
			||||||
$redisUrl = sprintf(
 | 
					 | 
				
			||||||
    'redis://%s:%s@%s:%d',
 | 
					 | 
				
			||||||
    urlencode($config['username']),
 | 
					 | 
				
			||||||
    urlencode($config['password']),
 | 
					 | 
				
			||||||
    $config['host'],
 | 
					 | 
				
			||||||
    intval($config['port'], 10)
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
\Resque::setBackend($redisUrl);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Start the worker
 | 
					 | 
				
			||||||
$worker = new \Resque_Worker(['inbox']);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
fwrite(STDOUT, "*** Starting worker for inbox queue\n");
 | 
					 | 
				
			||||||
$worker->work(10); // 10 seconds interval
 | 
					 | 
				
			||||||
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							| 
						 | 
					@ -19,39 +19,15 @@ class DummyConnector implements Connector
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * get followers of given user
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param string $userId user id @unused-param
 | 
					 | 
				
			||||||
     * @return \Federator\Data\FedUser[]|false
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function getRemoteFollowersOfUser($userId)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        return false;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * get following of given user
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param string $id user id @unused-param
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
     * @return \Federator\Data\FedUser[]|false
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function getRemoteFollowingForUser($id)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        return false;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * get posts by given user
 | 
					     * get posts by given user
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @param string $id user id @unused-param
 | 
					     * @param string $id user id @unused-param
 | 
				
			||||||
     * @param int $min min timestamp @unused-param
 | 
					     * @param string $minId min ID @unused-param
 | 
				
			||||||
     * @param int $max max timestamp @unused-param
 | 
					     * @param string $maxId max ID @unused-param
 | 
				
			||||||
     * @param int $limit limit number of results @unused-param
 | 
					     * @return \Federator\Data\ActivityPub\Common\APObject[]|false
 | 
				
			||||||
     * @return \Federator\Data\ActivityPub\Common\Activity[]|false
 | 
					 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function getRemotePostsByUser($id, $min, $max, $limit)
 | 
					    public function getRemotePostsByUser($id, $minId, $maxId)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        return false;
 | 
					        return false;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -70,18 +46,6 @@ class DummyConnector implements Connector
 | 
				
			||||||
        return $stats;
 | 
					        return $stats;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Convert jsonData to Activity format
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param array<string, mixed> $jsonData the json data from our platfrom @unused-param
 | 
					 | 
				
			||||||
     * @param string $articleId the original id of the article (if applicable)
 | 
					 | 
				
			||||||
     *                           (used to identify the article in the remote system) @unused-param
 | 
					 | 
				
			||||||
     * @return \Federator\Data\ActivityPub\Common\Activity|false
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function jsonToActivity(array $jsonData, &$articleId) {
 | 
					 | 
				
			||||||
        return false;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * get remote user by name
 | 
					     * get remote user by name
 | 
				
			||||||
     * @param string $_name user or profile name
 | 
					     * @param string $_name user or profile name
 | 
				
			||||||
| 
						 | 
					@ -106,34 +70,10 @@ class DummyConnector implements Connector
 | 
				
			||||||
        // validate $_session and $user
 | 
					        // validate $_session and $user
 | 
				
			||||||
        $user = new \Federator\Data\User();
 | 
					        $user = new \Federator\Data\User();
 | 
				
			||||||
        $user->externalid = $_user;
 | 
					        $user->externalid = $_user;
 | 
				
			||||||
        $user->permissions = ['publish'];
 | 
					        $user->permissions = ['PUBLISH'];
 | 
				
			||||||
        $user->session = $_session;
 | 
					        $user->session = $_session;
 | 
				
			||||||
        return $user;
 | 
					        return $user;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * send target-friendly json from ActivityPub activity
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param \Federator\Data\FedUser $sender the user of the sender @unused-param
 | 
					 | 
				
			||||||
     * @param \Federator\Data\ActivityPub\Common\Activity $activity the activity @unused-param
 | 
					 | 
				
			||||||
     * @return boolean did we successfully send the activity?
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function sendActivity($sender, $activity)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        return false;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * check if the headers include a valid signature
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param string[] $headers the headers @unused-param
 | 
					 | 
				
			||||||
     * @throws \Federator\Exceptions\PermissionDenied
 | 
					 | 
				
			||||||
     * @return string|\Federator\Exceptions\PermissionDenied
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function checkSignature($headers)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        return new \Federator\Exceptions\PermissionDenied("Dummy connector: no signature check");
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Federator;
 | 
					namespace Federator;
 | 
				
			||||||
| 
						 | 
					@ -147,6 +87,5 @@ namespace Federator;
 | 
				
			||||||
function dummy_load($main)
 | 
					function dummy_load($main)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    $dummy = new Connector\DummyConnector();
 | 
					    $dummy = new Connector\DummyConnector();
 | 
				
			||||||
    # echo "dummyconnector::dummy_load Loaded new connector, adding to main\n"; // TODO change to proper log
 | 
					 | 
				
			||||||
    $main->setConnector($dummy);
 | 
					    $main->setConnector($dummy);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -41,23 +41,15 @@ class RedisCache implements Cache
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    private $userTTL;
 | 
					    private $userTTL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * public key cache time to live in secods
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @var int $publicKeyPemTTL
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    private $publicKeyPemTTL;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * constructor
 | 
					     * constructor
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function __construct()
 | 
					    public function __construct()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $config = parse_ini_file(PROJECT_ROOT . '/rediscache.ini');
 | 
					        $config = parse_ini_file('../rediscache.ini');
 | 
				
			||||||
        if ($config !== false) {
 | 
					        if ($config !== false) {
 | 
				
			||||||
            $this->config = $config;
 | 
					            $this->config = $config;
 | 
				
			||||||
            $this->userTTL = array_key_exists('userttl', $config) ? intval($config['userttl'], 10) : 60;
 | 
					            $this->userTTL = array_key_exists('userttl', $config) ? intval($config['userttl'], 10) : 60;
 | 
				
			||||||
            $this->publicKeyPemTTL = array_key_exists('publickeypemttl', $config) ? intval($config['publickeypemttl'], 10) : 3600;
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -67,20 +59,10 @@ class RedisCache implements Cache
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    private function connect()
 | 
					    private function connect()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $this->redis = new \Redis();
 | 
					         $this->redis = new \Redis();
 | 
				
			||||||
        $this->redis->pconnect($this->config['host'], intval($this->config['port'], 10));
 | 
					         $this->redis->pconnect($this->config['host'], intval($this->config['port'], 10));
 | 
				
			||||||
        // @phan-suppress-next-line PhanTypeMismatchArgumentInternalProbablyReal
 | 
					         // @phan-suppress-next-line PhanTypeMismatchArgumentInternalProbablyReal
 | 
				
			||||||
        $this->redis->auth([$this->config['username'], $this->config['password']]);
 | 
					         $this->redis->auth([$this->config['username'], $this->config['password']]);
 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Set the Redis backend for Resque
 | 
					 | 
				
			||||||
        $redisUrl = sprintf(
 | 
					 | 
				
			||||||
            'redis://%s:%s@%s:%d',
 | 
					 | 
				
			||||||
            urlencode($this->config['username']),
 | 
					 | 
				
			||||||
            urlencode($this->config['password']),
 | 
					 | 
				
			||||||
            $this->config['host'],
 | 
					 | 
				
			||||||
            intval($this->config['port'], 10)
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        \Resque::setBackend($redisUrl);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
| 
						 | 
					@ -95,57 +77,16 @@ class RedisCache implements Cache
 | 
				
			||||||
        return $prefix . '_' . md5($input);
 | 
					        return $prefix . '_' . md5($input);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * get followers of given user
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param string $id user id @unused-param
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
     * @return \Federator\Data\FedUser[]|false
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function getRemoteFollowersOfUser($id)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        error_log("rediscache::getRemoteFollowersOfUser not implemented");
 | 
					 | 
				
			||||||
        return false;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * get following of given user
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param string $id user id @unused-param
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
     * @return \Federator\Data\FedUser[]|false
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function getRemoteFollowingForUser($id)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        error_log("rediscache::getRemoteFollowingForUser not implemented");
 | 
					 | 
				
			||||||
        return false;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Convert jsonData to Activity format
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param array<string, mixed> $jsonData the json data from our platfrom @unused-param
 | 
					 | 
				
			||||||
     * @param string $articleId the original id of the article (if applicable)
 | 
					 | 
				
			||||||
     *                           (used to identify the article in the remote system) @unused-param
 | 
					 | 
				
			||||||
     * @return \Federator\Data\ActivityPub\Common\Activity|false
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function jsonToActivity(array $jsonData, &$articleId)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        error_log("rediscache::jsonToActivity not implemented");
 | 
					 | 
				
			||||||
        return false;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * get posts by given user
 | 
					     * get posts by given user
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @param string $id user id @unused-param
 | 
					     * @param string $id user id @unused-param
 | 
				
			||||||
     * @param int $min min timestamp @unused-param
 | 
					     * @param string $minId min ID @unused-param
 | 
				
			||||||
     * @param int $max max timestamp @unused-param
 | 
					     * @param string $maxId max ID @unused-param
 | 
				
			||||||
     * @param int $limit limit results @unused-param
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
     * @return \Federator\Data\ActivityPub\Common\Activity[]|false
 | 
					     * @return \Federator\Data\ActivityPub\Common\APObject[]|false
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function getRemotePostsByUser($id, $min, $max, $limit)
 | 
					    public function getRemotePostsByUser($id, $minId, $maxId)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        error_log("rediscache::getRemotePostsByUser not implemented");
 | 
					        error_log("rediscache::getRemotePostsByUser not implemented");
 | 
				
			||||||
        return false;
 | 
					        return false;
 | 
				
			||||||
| 
						 | 
					@ -190,26 +131,6 @@ class RedisCache implements Cache
 | 
				
			||||||
        return $user;
 | 
					        return $user;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * get remote federation user by given name
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param string $_name user/profile name
 | 
					 | 
				
			||||||
     * @return \Federator\Data\FedUser | false
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function getRemoteFedUserByName(string $_name)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        if (!$this->connected) {
 | 
					 | 
				
			||||||
            $this->connect();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $key = self::createKey('u', $_name);
 | 
					 | 
				
			||||||
        $data = $this->redis->get($key);
 | 
					 | 
				
			||||||
        if ($data === false) {
 | 
					 | 
				
			||||||
            return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $user = \Federator\Data\FedUser::createFromJson($data);
 | 
					 | 
				
			||||||
        return $user;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * get remote user by given session
 | 
					     * get remote user by given session
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
| 
						 | 
					@ -231,56 +152,14 @@ class RedisCache implements Cache
 | 
				
			||||||
        return $user;
 | 
					        return $user;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Retrieve the public key for a given keyId
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param string $keyId The keyId (e.g., actor URL + #main-key)
 | 
					 | 
				
			||||||
     * @return string|false The cached public key PEM or false if not found
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function getPublicKey(string $keyId)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        if (!$this->connected) {
 | 
					 | 
				
			||||||
            $this->connect();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $key = self::createKey('publickey', $keyId);
 | 
					 | 
				
			||||||
        return $this->redis->get($key);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * save remote followers by user
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param string $user user name @unused-param
 | 
					 | 
				
			||||||
     * @param \Federator\Data\FedUser[]|false $followers user followers @unused-param
 | 
					 | 
				
			||||||
     * @return void
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function saveRemoteFollowersOfUser($user, $followers)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        error_log("rediscache::saveRemoteFollowersOfUser not implemented");
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * save remote following for user
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param string $user user name @unused-param
 | 
					 | 
				
			||||||
     * @param \Federator\Data\FedUser[]|false $following user following @unused-param
 | 
					 | 
				
			||||||
     * @return void
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function saveRemoteFollowingForUser($user, $following)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        error_log("rediscache::saveRemoteFollowingForUser not implemented");
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * save remote posts by user
 | 
					     * save remote posts by user
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @param string $user user name @unused-param
 | 
					     * @param string $user user name @unused-param
 | 
				
			||||||
     * @param int $min min timestamp @unused-param
 | 
					 | 
				
			||||||
     * @param int $max max timestamp @unused-param
 | 
					 | 
				
			||||||
     * @param int $limit limit results @unused-param
 | 
					 | 
				
			||||||
     * @param \Federator\Data\ActivityPub\Common\APObject[]|false $posts user posts @unused-param
 | 
					     * @param \Federator\Data\ActivityPub\Common\APObject[]|false $posts user posts @unused-param
 | 
				
			||||||
     * @return void
 | 
					     * @return void
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function saveRemotePostsByUser($user, $min, $max, $limit, $posts)
 | 
					    public function saveRemotePostsByUser($user, $posts)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        error_log("rediscache::saveRemotePostsByUser not implemented");
 | 
					        error_log("rediscache::saveRemotePostsByUser not implemented");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -308,23 +187,6 @@ class RedisCache implements Cache
 | 
				
			||||||
     * @return void
 | 
					     * @return void
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function saveRemoteUserByName($_name, $user)
 | 
					    public function saveRemoteUserByName($_name, $user)
 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        if (!$this->connected) {
 | 
					 | 
				
			||||||
            $this->connect();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $key = self::createKey('u', $_name);
 | 
					 | 
				
			||||||
        $serialized = $user->toJson();
 | 
					 | 
				
			||||||
        $this->redis->setEx($key, $this->userTTL, $serialized);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * save remote federation user by given name
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param string $_name user/profile name
 | 
					 | 
				
			||||||
     * @param \Federator\Data\FedUser $user user data
 | 
					 | 
				
			||||||
     * @return void
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function saveRemoteFedUserByName(string $_name, \Federator\Data\FedUser $user)
 | 
					 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $key = self::createKey('u', $_name);
 | 
					        $key = self::createKey('u', $_name);
 | 
				
			||||||
        $serialized = $user->toJson();
 | 
					        $serialized = $user->toJson();
 | 
				
			||||||
| 
						 | 
					@ -343,47 +205,7 @@ class RedisCache implements Cache
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $key = self::createKey('s', $_session . $_user);
 | 
					        $key = self::createKey('s', $_session . $_user);
 | 
				
			||||||
        $serialized = $user->toJson();
 | 
					        $serialized = $user->toJson();
 | 
				
			||||||
        $this->redis->setEx($key, $this->userTTL, $serialized);
 | 
					        $this->redis->setEx($key, $this->userTTL, $serialized,);
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Save the public key for a given keyId
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param string $keyId The keyId (e.g., actor URL + #main-key)
 | 
					 | 
				
			||||||
     * @param string $publicKeyPem The public key PEM to cache
 | 
					 | 
				
			||||||
     * @return void
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function savePublicKey(string $keyId, string $publicKeyPem)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        if (!$this->connected) {
 | 
					 | 
				
			||||||
            $this->connect();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $key = self::createKey('publickey', $keyId);
 | 
					 | 
				
			||||||
        $this->redis->setEx($key, $this->publicKeyPemTTL, $publicKeyPem);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * send target-friendly json from ActivityPub activity
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param \Federator\Data\FedUser $sender the user of the sender @unused-param
 | 
					 | 
				
			||||||
     * @param \Federator\Data\ActivityPub\Common\Activity $activity the activity @unused-param
 | 
					 | 
				
			||||||
     * @return boolean did we successfully send the activity?
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function sendActivity($sender, $activity)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        return false;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * check if the headers include a valid signature
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param string[] $headers the headers @unused-param
 | 
					 | 
				
			||||||
     * @throws \Federator\Exceptions\PermissionDenied
 | 
					 | 
				
			||||||
     * @return string|\Federator\Exceptions\PermissionDenied
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function checkSignature($headers)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        return new \Federator\Exceptions\PermissionDenied("RedisCache: no signature check");
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,5 +4,4 @@ port = 6379
 | 
				
			||||||
username = federator
 | 
					username = federator
 | 
				
			||||||
password = redis*change*password
 | 
					password = redis*change*password
 | 
				
			||||||
userttl = 10
 | 
					userttl = 10
 | 
				
			||||||
publickeypemttl = 3600
 | 
					 | 
				
			||||||
statsttl = 60
 | 
					statsttl = 60
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,3 +0,0 @@
 | 
				
			||||||
create table follows(`id` varchar(255) unique primary key,`source_user` varchar(255) not null,`target_user` varchar(255) not null,`created_at` timestamp default current_timestamp,unique key `unique_follow` (`source_user`, `target_user`));
 | 
					 | 
				
			||||||
create table fedusers(`id` varchar(255) unique primary key,`url` varchar(255) not null,`name` varchar(255) default '',`publickey` text default '',`summary` text default '',`validuntil` timestamp null default null,`type` enum('person', 'group') default 'person',`inboxurl` varchar(255) default null,`sharedinboxurl` varchar(255) default null,`followersurl` varchar(255) default null,`followingurl` varchar(255) default null,`publickeyid` varchar(255) default null,`outboxurl` varchar(255) default null,unique key `unique_feduser` (`url`),unique key `unique_feduser_id` (`url`));
 | 
					 | 
				
			||||||
update settings set `value`="2025-05-06" where `key`="database_version";
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,2 +0,0 @@
 | 
				
			||||||
create table posts(`id` varchar(255) primary key,`url` varchar(255) not null,`user_id` varchar(255) not null,`actor` varchar(255) not null,`type` varchar(255) not null default 'note',`object` text default null,`to` text default null,`cc` text default null,`published` timestamp not null default current_timestamp);
 | 
					 | 
				
			||||||
update settings set `value`="2025-05-19" where `key`="database_version";
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,2 +0,0 @@
 | 
				
			||||||
alter table posts add `article_id` varchar(255) null default null comment 'The optional original article id (of non-federated system, e.g. CN)';
 | 
					 | 
				
			||||||
update settings set `value`="2025-05-27" where `key`="database_version";
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,4 @@
 | 
				
			||||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
					version="1.0" encoding="UTF-8"?>
 | 
				
			||||||
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
 | 
					<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
 | 
				
			||||||
  <Link rel="lrdd" template="https://{$fqdn}/.well-known/webfinger?resource={ldelim}uri{rdelim}"/>
 | 
					  <Link rel="lrdd" template="https://{$fqdn}/.well-known/webfinger?resource={ldelim}uri{rdelim}"/>
 | 
				
			||||||
</XRD>
 | 
					</XRD>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -58,14 +58,14 @@
 | 
				
			||||||
            {rdelim}
 | 
					            {rdelim}
 | 
				
			||||||
        {rdelim}
 | 
					        {rdelim}
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
    "id":"https://{$fqdn}/{$username}",
 | 
					    "id":"https://{$fqdn}/users/{$username}",
 | 
				
			||||||
    "type":"{$type}",
 | 
					    "type":"{$type}",
 | 
				
			||||||
    "following":"https://{$fqdn}/{$username}/following",
 | 
					    "following":"https://{$fqdn}/users/{$username}/following",
 | 
				
			||||||
    "followers":"https://{$fqdn}/{$username}/followers",
 | 
					    "followers":"https://{$fqdn}/users/{$username}/followers",
 | 
				
			||||||
    "inbox":"https://{$fqdn}/{$username}/inbox",
 | 
					    "inbox":"https://{$fqdn}/users/{$username}/inbox",
 | 
				
			||||||
    "outbox":"https://{$fqdn}/{$username}/outbox",
 | 
					    "outbox":"https://{$fqdn}/users/{$username}/outbox",
 | 
				
			||||||
    "featured":"https://{$fqdn}/{$username}/collections/featured",
 | 
					    "featured":"https://{$fqdn}/users/{$username}/collections/featured",
 | 
				
			||||||
    "featuredTags":"https://{$fqdn}/{$username}/collections/tags",
 | 
					    "featuredTags":"https://{$fqdn}/users/{$username}/collections/tags",
 | 
				
			||||||
    "preferredUsername":"{$username}",
 | 
					    "preferredUsername":"{$username}",
 | 
				
			||||||
    "name":"{$name}",
 | 
					    "name":"{$name}",
 | 
				
			||||||
    "summary":"{$summary}",
 | 
					    "summary":"{$summary}",
 | 
				
			||||||
| 
						 | 
					@ -74,8 +74,8 @@
 | 
				
			||||||
    "discoverable":true,
 | 
					    "discoverable":true,
 | 
				
			||||||
    "published":"{$registered}",
 | 
					    "published":"{$registered}",
 | 
				
			||||||
    "publicKey":{ldelim}
 | 
					    "publicKey":{ldelim}
 | 
				
			||||||
    "id":"https://{$fqdn}/{$username}#main-key",
 | 
					    "id":"https://{$fqdn}/users/{$username}#main-key",
 | 
				
			||||||
    "owner":"https://{$fqdn}/{$username}",
 | 
					    "owner":"https://{$fqdn}/users/{$username}",
 | 
				
			||||||
    "publicKeyPem":"{$publickey}"
 | 
					    "publicKeyPem":"{$publickey}"
 | 
				
			||||||
    {rdelim},
 | 
					    {rdelim},
 | 
				
			||||||
    "tag":[],
 | 
					    "tag":[],
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,8 @@
 | 
				
			||||||
{ldelim}
 | 
					{ldelim}
 | 
				
			||||||
  "subject": "acct:{$username}@{$domain}",
 | 
					  "subject": "acct:{$username}@{$domain}",
 | 
				
			||||||
  "aliases": [
 | 
					  "aliases": [
 | 
				
			||||||
    "https://{$domain}/@{$username}"
 | 
					    "https://{$domain}/@{$username}",
 | 
				
			||||||
 | 
					    "https://{$domain}/users/{$username}"
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
  "links": [
 | 
					  "links": [
 | 
				
			||||||
    {ldelim}"rel": "self", "type": "application/activity+json", "href": "https://{$domain}/{$username}"{rdelim},
 | 
					    {ldelim}"rel": "self", "type": "application/activity+json", "href": "https://{$domain}/{$username}"{rdelim},
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		
		Reference in a new issue