initial import

This commit is contained in:
Sascha Nitsch 2024-07-15 20:46:44 +02:00
commit ef25c5b3af
23 changed files with 1693 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
composer.lock
vendor
php/version.php

369
.phan/config.php Normal file
View file

@ -0,0 +1,369 @@
<?php
use Phan\Issue;
/**
* This configuration file was automatically generated by 'phan --init --init-level=1'
*
* TODOs (added by 'phan --init'):
*
* - Go through this file and verify that there are no missing/unnecessary files/directories.
* (E.g. this only includes direct composer dependencies - You may have to manually add indirect composer dependencies to 'directory_list')
* - Look at 'plugins' and add or remove plugins if appropriate (see https://github.com/phan/phan/tree/v5/.phan/plugins#plugins)
* - Add global suppressions for pre-existing issues to suppress_issue_types (https://github.com/phan/phan/wiki/Tutorial-for-Analyzing-a-Large-Sloppy-Code-Base)
* - Consider setting up a baseline if there are a large number of pre-existing issues (see `phan --extended-help`)
*
* This configuration will be read and overlaid on top of the
* default configuration. Command line arguments will be applied
* after this file is read.
*
* @see https://github.com/phan/phan/wiki/Phan-Config-Settings for all configurable options
* @see https://github.com/phan/phan/tree/v5/src/Phan/Config.php
*
* A Note About Paths
* ==================
*
* Files referenced from this file should be defined as
*
* ```
* Config::projectPath('relative_path/to/file')
* ```
*
* where the relative path is relative to the root of the
* project which is defined as either the working directory
* of the phan executable or a path passed in via the CLI
* '-d' flag.
*/
return [
// The PHP version that the codebase will be checked for compatibility against.
// For best results, the PHP binary used to run Phan should have the same PHP version.
// (Phan relies on Reflection for some types, param counts,
// and checks for undefined classes/methods/functions)
//
// Supported values: `'5.6'`, `'7.0'`, `'7.1'`, `'7.2'`, `'7.3'`, `'7.4'`,
// `'8.0'`, `'8.1'`, `'8.2'`, `'8.3'`, `null`.
// If this is set to `null`,
// then Phan assumes the PHP version which is closest to the minor version
// of the php executable used to execute Phan.
//
// Note that the **only** effect of choosing `'5.6'` is to infer that functions removed in php 7.0 exist.
// (See `backward_compatibility_checks` for additional options)
// TODO: Choose a target_php_version for this project, or leave as null and remove this comment
'target_php_version' => '8.3',
// If enabled, missing properties will be created when
// they are first seen. If false, we'll report an
// error message if there is an attempt to write
// to a class property that wasn't explicitly
// defined.
'allow_missing_properties' => false,
// If enabled, null can be cast to any type and any
// type can be cast to null. Setting this to true
// will cut down on false positives.
'null_casts_as_any_type' => false,
// If enabled, allow null to be cast as any array-like type.
//
// This is an incremental step in migrating away from `null_casts_as_any_type`.
// If `null_casts_as_any_type` is true, this has no effect.
'null_casts_as_array' => false,
// If enabled, allow any array-like type to be cast to null.
// This is an incremental step in migrating away from `null_casts_as_any_type`.
// If `null_casts_as_any_type` is true, this has no effect.
'array_casts_as_null' => false,
// If enabled, scalars (int, float, bool, string, null)
// are treated as if they can cast to each other.
// This does not affect checks of array keys. See `scalar_array_key_cast`.
'scalar_implicit_cast' => false,
// If enabled, any scalar array keys (int, string)
// are treated as if they can cast to each other.
// E.g. `array<int,stdClass>` can cast to `array<string,stdClass>` and vice versa.
// Normally, a scalar type such as int could only cast to/from int and mixed.
'scalar_array_key_cast' => false,
// If this has entries, scalars (int, float, bool, string, null)
// are allowed to perform the casts listed.
//
// E.g. `['int' => ['float', 'string'], 'float' => ['int'], 'string' => ['int'], 'null' => ['string']]`
// allows casting null to a string, but not vice versa.
// (subset of `scalar_implicit_cast`)
'scalar_implicit_partial' => [],
// If enabled, Phan will warn if **any** type in a method invocation's object
// is definitely not an object,
// or if **any** type in an invoked expression is not a callable.
// Setting this to true will introduce numerous false positives
// (and reveal some bugs).
'strict_method_checking' => true,
// If enabled, Phan will warn if **any** type of the object expression for a property access
// does not contain that property.
'strict_object_checking' => true,
// If enabled, Phan will warn if **any** type in the argument's union type
// cannot be cast to a type in the parameter's expected union type.
// Setting this to true will introduce numerous false positives
// (and reveal some bugs).
'strict_param_checking' => true,
// If enabled, Phan will warn if **any** type in a property assignment's union type
// cannot be cast to a type in the property's declared union type.
// Setting this to true will introduce numerous false positives
// (and reveal some bugs).
'strict_property_checking' => true,
// If enabled, Phan will warn if **any** type in a returned value's union type
// cannot be cast to the declared return type.
// Setting this to true will introduce numerous false positives
// (and reveal some bugs).
'strict_return_checking' => true,
// If true, seemingly undeclared variables in the global
// scope will be ignored.
//
// This is useful for projects with complicated cross-file
// globals that you have no hope of fixing.
'ignore_undeclared_variables_in_global_scope' => false,
// Set this to false to emit `PhanUndeclaredFunction` issues for internal functions that Phan has signatures for,
// but aren't available in the codebase, or from Reflection.
// (may lead to false positives if an extension isn't loaded)
//
// If this is true(default), then Phan will not warn.
//
// Even when this is false, Phan will still infer return values and check parameters of internal functions
// if Phan has the signatures.
'ignore_undeclared_functions_with_known_signatures' => false,
// Backwards Compatibility Checking. This is slow
// and expensive, but you should consider running
// it before upgrading your version of PHP to a
// new version that has backward compatibility
// breaks.
//
// If you are migrating from PHP 5 to PHP 7,
// you should also look into using
// [php7cc (no longer maintained)](https://github.com/sstalle/php7cc)
// and [php7mar](https://github.com/Alexia/php7mar),
// which have different backwards compatibility checks.
//
// If you are still using versions of php older than 5.6,
// `PHP53CompatibilityPlugin` may be worth looking into if you are not running
// syntax checks for php 5.3 through another method such as
// `InvokePHPNativeSyntaxCheckPlugin` (see .phan/plugins/README.md).
'backward_compatibility_checks' => false,
// If true, check to make sure the return type declared
// in the doc-block (if any) matches the return type
// declared in the method signature.
'check_docblock_signature_return_type_match' => true,
// This setting maps case-insensitive strings to union types.
//
// This is useful if a project uses phpdoc that differs from the phpdoc2 standard.
//
// If the corresponding value is the empty string,
// then Phan will ignore that union type (E.g. can ignore 'the' in `@return the value`)
//
// If the corresponding value is not empty,
// then Phan will act as though it saw the corresponding UnionTypes(s)
// when the keys show up in a UnionType of `@param`, `@return`, `@var`, `@property`, etc.
//
// This matches the **entire string**, not parts of the string.
// (E.g. `@return the|null` will still look for a class with the name `the`, but `@return the` will be ignored with the below setting)
//
// (These are not aliases, this setting is ignored outside of doc comments).
// (Phan does not check if classes with these names exist)
//
// Example setting: `['unknown' => '', 'number' => 'int|float', 'char' => 'string', 'long' => 'int', 'the' => '']`
'phpdoc_type_mapping' => [],
// Set to true in order to attempt to detect dead
// (unreferenced) code. Keep in mind that the
// results will only be a guess given that classes,
// properties, constants and methods can be referenced
// as variables (like `$class->$property` or
// `$class->$method()`) in ways that we're unable
// to make sense of.
//
// To more aggressively detect dead code,
// you may want to set `dead_code_detection_prefer_false_negative` to `false`.
'dead_code_detection' => false,
// Set to true in order to attempt to detect unused variables.
// `dead_code_detection` will also enable unused variable detection.
//
// This has a few known false positives, e.g. for loops or branches.
'unused_variable_detection' => true,
// Set to true in order to attempt to detect redundant and impossible conditions.
//
// This has some false positives involving loops,
// variables set in branches of loops, and global variables.
'redundant_condition_detection' => true,
// If enabled, Phan will act as though it's certain of real return types of a subset of internal functions,
// even if those return types aren't available in reflection (real types were taken from php 7.3 or 8.0-dev, depending on target_php_version).
//
// Note that with php 7 and earlier, php would return null or false for many internal functions if the argument types or counts were incorrect.
// As a result, enabling this setting with target_php_version 8.0 may result in false positives for `--redundant-condition-detection` when codebases also support php 7.x.
'assume_real_types_for_internal_functions' => true,
// If true, this runs a quick version of checks that takes less
// time at the cost of not running as thorough
// of an analysis. You should consider setting this
// to true only when you wish you had more **undiagnosed** issues
// to fix in your code base.
//
// In quick-mode the scanner doesn't rescan a function
// or a method's code block every time a call is seen.
// This means that the problem here won't be detected:
//
// ```php
// <?php
// function test($arg):int {
// return $arg;
// }
// test("abc");
// ```
//
// This would normally generate:
//
// ```
// test.php:3 PhanTypeMismatchReturn Returning type string but test() is declared to return int
// ```
//
// The initial scan of the function's code block has no
// type information for `$arg`. It isn't until we see
// the call and rescan `test()`'s code block that we can
// detect that it is actually returning the passed in
// `string` instead of an `int` as declared.
'quick_mode' => false,
// Override to hardcode existence and types of (non-builtin) globals in the global scope.
// Class names should be prefixed with `\`.
//
// (E.g. `['_FOO' => '\FooClass', 'page' => '\PageClass', 'userId' => 'int']`)
'globals_type_map' => [],
// The minimum severity level to report on. This can be
// set to `Issue::SEVERITY_LOW`, `Issue::SEVERITY_NORMAL` or
// `Issue::SEVERITY_CRITICAL`. Setting it to only
// critical issues is a good place to start on a big
// sloppy mature code base.
'minimum_severity' => Issue::SEVERITY_LOW,
// Add any issue types (such as `'PhanUndeclaredMethod'`)
// to this list to inhibit them from being reported.
'suppress_issue_types' => [],
// A regular expression to match files to be excluded
// from parsing and analysis and will not be read at all.
//
// This is useful for excluding groups of test or example
// directories/files, unanalyzable files, or files that
// can't be removed for whatever reason.
// (e.g. `'@Test\.php$@'`, or `'@vendor/.*/(tests|Tests)/@'`)
'exclude_file_regex' => '@^vendor/.*/(tests?|Tests?)/@',
// A list of files that will be excluded from parsing and analysis
// and will not be read at all.
//
// This is useful for excluding hopelessly unanalyzable
// files that can't be removed for whatever reason.
'exclude_file_list' => [],
// A directory list that defines files that will be excluded
// from static analysis, but whose class and method
// information should be included.
//
// Generally, you'll want to include the directories for
// third-party code (such as "vendor/") in this list.
//
// n.b.: If you'd like to parse but not analyze 3rd
// party code, directories containing that code
// should be added to the `directory_list` as well as
// to `exclude_analysis_directory_list`.
'exclude_analysis_directory_list' => [
'vendor/',
],
// Enable this to enable checks of require/include statements referring to valid paths.
// The settings `include_paths` and `warn_about_relative_include_statement` affect the checks.
'enable_include_path_checks' => true,
// The number of processes to fork off during the analysis
// phase.
'processes' => 1,
// List of case-insensitive file extensions supported by Phan.
// (e.g. `['php', 'html', 'htm']`)
'analyzed_file_extensions' => [
'php',
],
// You can put paths to stubs of internal extensions in this config option.
// If the corresponding extension is **not** loaded, then Phan will use the stubs instead.
// Phan will continue using its detailed type annotations,
// but load the constants, classes, functions, and classes (and their Reflection types)
// from these stub files (doubling as valid php files).
// Use a different extension from php to avoid accidentally loading these.
// The `tools/make_stubs` script can be used to generate your own stubs (compatible with php 7.0+ right now)
//
// (e.g. `['xdebug' => '.phan/internal_stubs/xdebug.phan_php']`)
'autoload_internal_extension_signatures' => [],
// A list of plugin files to execute.
//
// Plugins which are bundled with Phan can be added here by providing their name (e.g. `'AlwaysReturnPlugin'`)
//
// Documentation about available bundled plugins can be found [here](https://github.com/phan/phan/tree/v5/.phan/plugins).
//
// Alternately, you can pass in the full path to a PHP file with the plugin's implementation (e.g. `'vendor/phan/phan/.phan/plugins/AlwaysReturnPlugin.php'`)
'plugins' => [
'AlwaysReturnPlugin',
'DollarDollarPlugin',
'DuplicateArrayKeyPlugin',
'DuplicateExpressionPlugin',
'PregRegexCheckerPlugin',
'PrintfCheckerPlugin',
'SleepCheckerPlugin',
'UnreachableCodePlugin',
'NonBoolBranchPlugin',
'NonBoolInLogicalArithPlugin',
'InvalidVariableIssetPlugin',
'NumericalComparisonPlugin',
'PHPUnitNotDeadCodePlugin',
'UnusedSuppressionPlugin',
'UnknownElementTypePlugin',
'UseReturnValuePlugin',
'EmptyStatementListPlugin',
'StrictComparisonPlugin',
'LoopVariableReusePlugin',
'WhitespacePlugin',
'PossiblyStaticMethodPlugin',
'PHPDocRedundantPlugin',
],
// A list of directories that should be parsed for class and
// method information. After excluding the directories
// defined in `exclude_analysis_directory_list`, the remaining
// files will be statically analyzed for errors.
//
// Thus, both first-party and third-party code being used by
// your application should be included in this list.
'directory_list' => [
'vendor/phan/phan/src/Phan',
'vendor/smarty/smarty/src',
],
// A list of individual files to include in analysis
// with a path relative to the root directory of the
// project.
'file_list' => [],
];

41
README.md Normal file
View file

@ -0,0 +1,41 @@
# A system to connect non-federated system to federation (ActivityPub)
NOTE: this is work in progress, it is nowhere near completion nor function
## installation
install dependencies by using
> composer install
Upload/copy the files to a directory where php can be run from.
The files in the htdocs should be the only reachable, the others should not be served via the web server.
Structure:
- htdocs <= reachable
- php <= implementation
- plugins <= plugin directory for well - plugins
- config.ini <= the configuration
The default config includes a dummy plugin to connect to a non-exisitng server. It accepts any session id and profile name.
The database is not used yet, but it must be created and the user with given account data must be able to reach it.
Needed SQL commands:
create database federator;
create user if not exists 'federator'@'localhost' identified by '*change*me*';
grant select,insert,update,delete on federator.* to 'federator'@'localhost';
This will be changed, but works for the current develop verison.
To configure an apache server, add the following rewrite rules:
<Directory /where/ever/you/put/it>
RewriteEngine on
RewriteBase /
RewriteRule ^api/(.+)$ api.php?_call=$1 [L]
</Directory>
With the dummy plugin and everything installed correctly a
> curl -v http://localhost/api/v1/dummy/moo -H "X-Session: somethingvalid" -H "X-Profile: ihaveone"
should return a piece of ascii art.

18
composer.json Normal file
View file

@ -0,0 +1,18 @@
{
"name": "contentnation/federator",
"description": "A federation service",
"type": "project",
"require": {
"smarty/smarty": "^5.3"
},
"license": "GPL-3.0-or-later",
"authors": [
{
"name": "Sascha Nitsch",
"email": "grumpydevelop@contentnation.net"
}
],
"require-dev": {
"phan/phan": "^5.4"
}
}

13
config.ini Normal file
View file

@ -0,0 +1,13 @@
[database]
host = '127.0.0.1'
username = 'federator'
password = '*change*me*'
database = 'federator'
[templates]
path = '../templates/'
compiledir = '../cache'
[plugins]
rediscache = '../plugins/rediscache.php'
dummy = '../plugins/dummyconnector.php'

22
htdocs/api.php Normal file
View file

@ -0,0 +1,22 @@
<?php
/**
* SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
* SPDX-License-Identifier: GPL-3.0-or-later
*
* @author Sascha Nitsch (grumpydeveloper)
**/
if (! array_key_exists('_call', $_REQUEST)) {
http_response_code(404);
exit();
}
date_default_timezone_set("Europe/Berlin");
spl_autoload_register(function ($className) {
// strip Federator from class path
$className = str_replace('Federator\\', '', $className);
include '../php/' . str_replace("\\", "/", strtolower($className)) . '.php';
});
/// main instance
$contentnation = new \Federator\Api();
$contentnation->run();

252
php/api.php Normal file
View file

@ -0,0 +1,252 @@
<?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;
/**
* main API class
*/
class Api extends Main
{
/** @var string called path */
private $path;
/** @var array<string> path elements for the API call */
private $paths;
/** @var Data\User current user */
private $user;
/** @var int cache time default to 0 */
private $cacheTime = 0;
/**
* constructor
*/
public function __construct()
{
$this->smarty = null;
$this->contentType = "application/json";
Main::__construct();
}
/**
* set path
*
* @param string $call
* path of called function
*/
public function setPath(string $call) : void
{
$this->path = $call;
while ($this->path[0] === '/') {
$this->path = substr($this->path, 1);
}
$this->paths = explode("/", $this->path);
}
/**
* main API function
*/
public function run() : void
{
$this->setPath($_REQUEST["_call"]);
$this->openDatabase();
$this->loadPlugins();
$retval = "";
/** @var \Api\Api */
$handler = null;
if (!array_key_exists('HTTP_X_SESSION', $_SERVER) || !array_key_exists('HTTP_X_PROFILE', $_SERVER)) {
http_response_code(403);
return;
}
if ($this->connector === null) {
http_response_code(500);
return;
}
$this->user = DIO\User::getUserBySession(
$this->dbh,
$_SERVER['HTTP_X_SESSION'],
$_SERVER['HTTP_X_PROFILE'],
$this->connector,
$this->cache
);
if ($this->user === false) {
http_response_code(403);
return;
}
switch ($this->path[0]) {
case 'v':
if ($this->paths[0] === "v1") {
switch ($this->paths[1]) {
case 'dummy':
$handler = new Api\V1\Dummy($this);
break;
}
}
break;
}
$printresponse = true;
if ($handler !== null) {
try {
$printresponse = $handler->exec($this->paths);
if ($printresponse) {
$retval = $handler->toJson();
}
} catch (Exceptions\Exception $e) {
$this->setResponseCode($e->getRetCode());
$retval = json_encode(array(
"error" => $e->getMessage()
));
}
} else {
$this->responseCode = 404;
}
if (sizeof($this->headers) != 0) {
foreach ($this->headers as $name => $value) {
header($name . ': ' . $value);
}
}
if ($printresponse) {
if ($this->redirect !== null) {
header("Location: $this->redirect");
}
if ($this->responseCode != 200) {
http_response_code($this->responseCode);
}
if ($this->responseCode != 404) {
header("Content-type: " . $this->contentType);
header("Access-Control-Allow-Origin: *");
}
if ($this->cacheTime == 0) {
header("Cache-Control: no-cache, no-store, must-revalidate");
header("Pragma: no-cache");
header("Expires: 0");
} else {
$ts = gmdate("D, d M Y H:i:s", time() + $this->cacheTime) . " GMT";
header("Expires: $ts");
header("Pragma: cache");
header("Cache-Control: max-age=" . $this->cacheTime);
}
echo $retval;
} else {
if (!headers_sent()) {
header("Content-type: " . $this->contentType);
}
}
}
/**
* check if the current user has the given permission
*
* @param string|string[] $permission
* permission(s) to check for
* @param string $exception Exception Type
* @param string $message optional message
* @throws \Exceptions\PermissionDenied
*/
public function checkPermission($permission, $exception = "\Exceptions\PermissionDenied", $message = null) : void
{
// generic check first
if ($this->user->id == 0) {
throw new Exceptions\PermissionDenied();
}
if (!is_array($permission)) {
$permission = array(
$permission
);
}
// LoggedIn is handled above
foreach ($permission as $p) {
if ($this->user->hasPermission($p)) {
return;
}
}
throw new $exception($message);
}
/**
* remove unwanted elements from html input
*
* @param string $_input
* input to strip
* @return string stripped input
*/
public static function stripHTML(string $_input) : string
{
$out = preg_replace('/<(script[^>]*)>/i', '&lt;${1}&gt;', $_input);
$out = preg_replace('/<\/(script)>/i', '&lt;/${1};&gt;', $out);
return $out;
}
/**
* is given parameter in POST data
*
* @param string $_key
* parameter to check
* @return bool true if in
*/
public static function hasPost(string $_key) : bool
{
return array_key_exists($_key, $_POST);
}
/**
* SQL escape given POST parameter
*
* @param string $key
* key to escape
* @param boolean $int
* is parameter an int
* @return int|string
*/
public function escapePost(string $key, $int = false)
{
if (! array_key_exists($key, $_POST)) {
return $int ? 0 : "";
}
if ($int === true) {
return intval($_POST[$key]);
}
$ret = $this->dbh->escape_string($this->stripHTML($_POST[$key]));
if ($ret === false) {
return $int ? 0 : "";
}
return $ret;
}
/**
* update $data with POST info using optional alias $altName
*
* @param string $name
* parameter name
* @param array<string,mixed> $data
* array to update
* @param bool $int
* is data an integer
* @param string $altName
* optional alternative name in POST
*/
public function updateString(string $name, array &$data, bool $int = false, string $altName = "") : void
{
if ($this->hasPost($altName ?: $name)) {
$content = $this->escapePost($altName ?: $name, $int);
$data[$name] = $content;
}
}
/**
* set cache time
*
* @param int $time time in seconds
*/
public function setCacheTime(int $time) : void
{
$this->cacheTime = $time;
}
}

28
php/api/v1.php Normal file
View file

@ -0,0 +1,28 @@
<?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\Api;
/**
* API interface
*/
interface V1
{
/**
* run given url path
* @param string[] $paths path array split by /
* @return bool true on success
*/
public function exec($paths) : bool;
/**
* get internal represenation as json string
* @return string json string or html
*/
public function toJson() : string;
}

92
php/api/v1/dummy.php Normal file
View file

@ -0,0 +1,92 @@
<?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\Api\V1;
/**
* dummy api class for functional poc
*/
class Dummy implements \Federator\Api\V1
{
/** @var \Main $main main instance */
private $main;
/** @var Array<string, string> $message internal message to output */
private $message = [];
/**
* constructor
* @param \Main $main main instance
*/
public function __construct(\Federator\Main $main)
{
$this->main = $main;
}
/**
* run given url path
* @param string[] $paths path array split by /
* @return bool true on success
*/
public function exec($paths) : bool
{
$method = $_SERVER["REQUEST_METHOD"];
switch ($method) {
case 'GET':
switch (sizeof($paths)) {
case 3:
if ($paths[2] === 'moo') {
return $this->getDummy();
}
}
break;
case 'POST':
switch (sizeof($paths)) {
case 3:
if ($paths[2] === 'moo') {
return $this->postDummy();
}
break;
}
}
$this->main->setResponseCode(404);
return false;
}
/**
* get function for "/v1/dummy/moo"
*/
public function getDummy(): bool
{
$this->message = [
'r1' => ' (__) ',
'r2' => ' `------(oo) ',
'r3' => ' || __ (__) ',
'r4' => ' ||w || ',
'r5' => ' '
];
return true;
}
/**
* post function for /v1/dummy/moo"
*/
public function postDummy() : bool
{
return $this->getDummy();
}
/**
* get internal represenation as json string
* @return string json string
*/
public function toJson() : string
{
return json_encode($this->message, JSON_PRETTY_PRINT) . "\n";
}
}

View file

@ -0,0 +1,23 @@
<?php
/**
* SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
* SPDX-License-Identifier: GPL-3.0-or-later
*
* @author Sascha Nitsch (grumpydeveloper)
**/
namespace Federator\Connector;
/**
* base class for remote authentication
*/
interface Connector
{
/**
* get remote user by given session
* @param string $_session session id
* @param string $_user user/profile name
* @return \Federator\Data\User | null
*/
public function getRemoteUserBySession(string $_session, string $_user);
}

39
php/data/user.php Normal file
View file

@ -0,0 +1,39 @@
<?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\Data;
/**
* storage class for user attributes
*/
class User
{
/** @var string user id */
public $id;
/* @var string user language */
//public $lang;
/** @var array<string> user permissions */
public $permissions = [];
/** @var string session id */
public $session;
/**
* check if use has asked permission
* @param string $p @unused-param
* permission to check
*
* @return bool true if user has permission, false if not
*/
public function hasPermission(string $p)
{
return in_array($p, $this->permissions);
}
}

60
php/dio/user.php Normal file
View file

@ -0,0 +1,60 @@
<?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\DIO;
/**
* IO functions related to users
*/
class User
{
/**
* extend the given user with internal data
* @param \mysqli $dbh database handle @unused-param
* @param \Federator\Data\User $user user to extend @unused-param
*/
protected static function extendUser(\mysqli $dbh, \Federator\Data\User $user) : void
{
// do nothing for now
// TODO: if a new user, create own database entry with additionally needed info
}
/**
* get User by session id
*
* @param \mysqli $dbh
* database handle
* @param string $_session
* session
* @param string $_user
* user/profile name
* @param \Connector\Connector? $connector @unused-param
* connector to fetch use with
* @param \Cache\Cache? $cache
* optional caching service
* @return \Data\User|bool
*/
public static function getUserBySession($dbh, $_session, $_user, $connector, $cache)
{
$user = false;
if ($cache) {
$user = $cache->getRemoteUserBySession($_session, $_user);
}
// ask connector for user-id
$user = $connector->getRemoteUserBySession($_session, $_user);
if ($user === false) {
return false;
}
if ($cache) {
$cache->saveRemoteUserBySession($_session, $_user, $user);
}
self::extendUser($dbh, $user);
return $user;
}
}

View file

@ -0,0 +1,25 @@
<?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\Exceptions;
/**
* base class for our Exceptions
*/
class Exception extends \Error
{
/**
* get matching HTTP response code
*
* @return int number HTTP response code
*/
public function getRetCode() : int
{
return 500;
}
}

View file

@ -0,0 +1,34 @@
<?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\Exceptions;
/**
* File not found exceptions
*/
class FileNotFound extends Exception
{
/**
* constructor
* @param ?string $message optional error message
*/
public function __construct($message = null)
{
$this->message = ($message === null) ? "filenotfound" : $message;
}
/**
*
* {@inheritDoc}
* @see \Exceptions\Exception::getRetCode()
*/
public function getRetCode() : int
{
return 404;
}
}

View file

@ -0,0 +1,34 @@
<?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\Exceptions;
/**
* Invalid Argument Exception
*/
class InvalidArgument extends Exception
{
/**
* constructor
* @param ?string $message optional error message
*/
public function __construct($message = null)
{
$this->message = ($message === null) ? "invalidargument" : $message;
}
/**
*
* {@inheritDoc}
* @see \Exceptions\Exception::getRetCode()
*/
public function getRetCode() : int
{
return 400;
}
}

View file

@ -0,0 +1,36 @@
<?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\Exceptions;
/**
* Permission denied exception
*/
class PermissionDenied extends Exception
{
/**
* constructor
*
* @param ?string $message
* error message
*/
public function __construct($message = null)
{
$this->message = ($message === null) ? "permissiondenied" : $message;
}
/**
*
* {@inheritdoc}
* @see \Exceptions\Exception::getRetCode()
*/
public function getRetCode() : int
{
return 403;
}
}

View file

@ -0,0 +1,34 @@
<?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\Exceptions;
/**
* Server Error Exception
*/
class ServerError extends Exception
{
/**
* constructor
* @param ?string $message optional error message
*/
public function __construct($message = null)
{
$this->message = ($message === null) ? "servererror" : $message;
}
/**
*
* {@inheritDoc}
* @see \Exceptions\Exception::getRetCode()
*/
public function getRetCode() : int
{
return 500;
}
}

View file

@ -0,0 +1,25 @@
<?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\Exceptions;
/**
* Unauthorized Exception
*/
class Unauthorized extends Exception
{
/**
*
* {@inheritDoc}
* @see \Exceptions\Exception::getRetCode()
*/
public function getRetCode() : int
{
return 401;
}
}

296
php/language.php Normal file
View file

@ -0,0 +1,296 @@
-<?php
/**
* SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
* SPDX-License-Identifier: GPL-3.0-or-later
* @author Author: Sascha Nitsch (grumpydeveloper)
*/
require_once '../vendor/autoload.php';
/**
* Language abstraction class
* @author Sascha Nitsch
*
*/
class Language {
/**
* list of valid languages
* @var array
*/
private $validLanguages = array(
"de" => true,
"en" => true,
"xy" => true
);
/**
* language to use
* @var string
*/
private $uselang;
/**
* actual language data
* @var array<string,array<string,string>>
*/
private $lang = [];
/**
* constructor that tries to autodetect language
*
* @param ?string $uselang
* use this language instead of autodetection, set to null if no preference
*/
function __construct($uselang = null) {
$this->lang = Array();
if ($uselang !== null) {
if (! array_key_exists($uselang, $this->validLanguages)) {
$uselang = null;
}
}
if ($uselang === null && array_key_exists('_lang', $_REQUEST)) {
$language = $_REQUEST['_lang'];
if (array_key_exists($language, $this->validLanguages)) {
$uselang = $language;
}
}
if ($uselang === null && array_key_exists('HTTP_ACCEPT_LANGUAGE', $_SERVER)) {
$matches = array();
if (preg_match("/^(\S\S)\-/",$_SERVER['HTTP_ACCEPT_LANGUAGE'], $matches) == 1) {
$language = $matches[1];
if (array_key_exists($language, $this->validLanguages)) {
$uselang = $language;
}
}
}
if ($uselang === null) {
$uselang = 'de';
}
$this->uselang = $uselang;
}
/**
* print translation of given group and id, optionally using variables
*
* @param string $group
* group name
* @param string $key
* string name
* @param array<mixed> $values
* optional values to replace
* @return string translated string
*/
function printlang($group, $key, array $values = array()) {
if ($this->uselang === 'xy') {
return "$group:$key";
}
if (! isset($this->lang[$group])) {
$l = [];
$root = $_SERVER['DOCUMENT_ROOT'];
if ($root === '') $root = '.';
if (@file_exists($root . '/../lang/' . $this->uselang . "/$group.inc")) {
require ($root . '/../lang/' . $this->uselang . "/$group.inc");
$this->lang[$group] = $l;
}
}
if (array_key_exists($group, $this->lang) && array_key_exists($key, $this->lang[$group])) {
$string = $this->lang[$group][$key];
for ($i = 0; $i < 9; $i ++) {
if (isset($values[$i])) {
$string = str_replace("\$$i", $values[$i], $string);
} else {
$string = str_replace("\$$i", "", $string);
}
}
return $string;
}
$basedir = $_SERVER['DOCUMENT_ROOT'] . '/../';
$fh = @fopen("$basedir/logs/missingtrans.txt", 'a');
if ($fh !== false) {
fwrite($fh, $this->uselang.":$group:$key\n");
fclose($fh);
}
return "&gt;&gt;$group:$key&lt;&lt;";
}
/**
* get keys (valid ids) of a named group
*
* @param string $group
* group name to fetch keys
* @return array<string> list of keys
*/
function getKeys(string $group) {
if (! isset($this->lang[$group])) {
$l = [];
require_once ($_SERVER['DOCUMENT_ROOT'] . '/../lang/' . $this->uselang . "/$group.inc");
$this->lang[$group] = $l;
}
return array_keys($this->lang[$group]);
}
/**
* return current used language
*
* @return string current language
*/
function getLang() {
return $this->uselang;
}
/**
* guess langauge of text
* @param string $text
* @param string $default
* @return string detected language
*/
static function guessLanguage(string $text, string $default, bool $debug = false) {
$supported_languages = array(
'en',
'de',
);
$wordList = [];
// German word list
// from http://wortschatz.uni-leipzig.de/Papers/top100de.txt
$wordList['de'] = array (
'die', 'der', 'und', /*'in',*/ 'zu', 'den', 'das', 'nicht', 'von', 'sie',
'ist', 'des', 'sich', 'mit', 'dem', 'dass', 'er', 'es', 'ein', 'ich',
'auf', 'so', 'eine', 'auch', 'als', 'an', 'nach', 'wie', 'im', 'für',
'man', 'aber', 'aus', 'durch', 'wenn', 'nur', 'war', 'noch', 'werden',
'bei', 'hat', 'wir', 'was', 'wird', 'sein', 'einen', 'welche', 'sind',
'oder', 'zur', 'um', 'haben', 'einer', 'mir', 'über', 'ihm', 'diese',
'einem', 'ihr', 'uns', 'da', 'zum', 'kann', 'doch', 'vor', 'dieser',
'mich', 'ihn', 'du', 'hatte', 'seine', 'mehr', 'am', 'denn', 'nun',
'unter', 'sehr', 'selbst', 'schon', 'hier', 'bis', 'habe', 'ihre',
'dann', 'ihnen', 'seiner', 'alle', 'wieder', 'meine', 'Zeit', 'gegen',
'vom', 'ganz', 'einzelnen', 'wo', 'muss', 'ohne', 'eines', 'können',
'sei', 'geschrieben', 'instanzen', 'deutsch','aktualisierung', 'registrierung'
);
// English word list
// from http://en.wikipedia.org/wiki/Most_common_words_in_English
$wordList['en'] = array ('the', 'be', 'to', 'of', 'and', 'a', /*'in',*/
'that', 'have', 'I', 'it', 'for', 'not', 'on', 'with', 'he',
'as', 'you', 'do', 'at', 'this', 'but', 'his', 'by', 'from', 'they',
'we', 'say', 'her', 'she', 'or', 'an', 'will', 'my', 'one', 'all',
'would', 'there', 'their', 'what', 'so', 'up', 'out', 'if', 'about'
);
// French word list
// from https://1000mostcommonwords.com/1000-most-common-french-words/
/*$wordList['fr'] = array ('comme', 'que', 'tait', 'pour', 'sur', 'sont', 'avec',
'tre', 'un', 'ce', 'par', 'mais', 'que', 'est',
'il', 'eu', 'la', 'et', 'dans');*/
// Spanish word list
// from https://spanishforyourjob.com/commonwords/
/*$wordList['es'] = array ('que', 'no', 'a', 'la', 'el', 'es', 'y',
'en', 'lo', 'un', 'por', 'qu', 'si', 'una',
'los', 'con', 'para', 'est', 'eso', 'las');*/
// clean out the input string - note we don't have any non-ASCII
// characters in the word lists... change this if it is not the
// case in your language wordlists!
$txt = strip_tags($text);
$txt = preg_replace("/[^A-Za-z:\\/\\.]+/", ' ', $txt);
if ($debug) echo "text: '$txt'\n";
$counter = [];
// count the occurrences of the most frequent words
foreach ($supported_languages as $language) {
$counter[$language]=0;
}
foreach ($supported_languages as $language) {
for ($i = 0; $i < sizeof($wordList[$language]); ++$i) {
$count = substr_count($txt, ' ' .$wordList[$language][$i] . ' ');
if ($debug && $count > 0) {
echo $language . " " . $wordList[$language][$i] . " => $count\n";
}
$counter[$language] += $count;
}
}
if ($debug)
print_r($counter);
// get max counter value
$max = max($counter);
$maxs = array_keys($counter, $max);
// if there are two winners - fall back to default!
if (count($maxs) == 1) {
$winner = $maxs[0];
$second = 0;
// get runner-up (second place)
foreach ($supported_languages as $language) {
if ($language !== $winner) {
if ($counter[$language]>$second) {
$second = $counter[$language];
}
}
}
// apply arbitrary threshold of 50%
if (($second / $max) < 0.5) {
return $winner;
}
}
return $default;
}
}
/**
* function called from smarty templates to print language translation
*
* @param array<string,mixed> $params
* smarty params, used are 'group', 'txt' and optionally 'var'
* @param \Smarty\Template $template
* template instance
* @return string translated text
*/
function smarty_function_printlang($params, $template) : string {
$lang = $template->getTemplateVars("language");
<<<'PHAN'
@phan-var \Language $lang
PHAN;
$forcelang = array_key_exists('lang', $params) ? $params['lang'] : null;
if ($forcelang !== null) {
$lang = new \Language($forcelang);
}
if (isset($params['var'])) {
return $lang->printlang($params['group'], $params['key'], $params['var']);
} else {
return $lang->printlang($params['group'], $params['key']);
}
}
/**
* function called from smarty templates to set language translation as JS string
*
* @param array<string, mixed> $params
* smarty params, used are 'group', 'txt' and optionally 'var'
* @param \Smarty\Template $template
* template instance
* @return string translated text as JS line
*/
function smarty_function_printjslang($params, $template) : string {
$lang = $template->getTemplateVars("language");
$prefix = 'window.translations.' . $params['group'] . '.' . $params['key'] . ' = \'';
$postfix = '\';';
if (isset($params['var']))
return $prefix . $lang->printlang($params['group'], $params['key'], $params['var']) . $postfix;
else
return $prefix . $lang->printlang($params['group'], $params['key']) . $postfix;
}
/**
* print translation of given group and id, optionally using variables
*
* @param string $group
* group name
* @param string $key
* string name
* @param array<mixed> $values
* optional values to replace
* @return string translated string
*/
function printlang(string $group, string $key, array $values=array()) {
global $contentnation;
return $contentnation->translate(null, $group, $key, $values);
}

178
php/main.php Normal file
View file

@ -0,0 +1,178 @@
<?php
/**
* SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
* SPDX-License-Identifier: GPL-3.0-or-later
* @author Author: Sascha Nitsch (grumpydeveloper)
**/
namespace Federator;
/**
* Base class for ContentNation and Api
* @author Sascha Nitsch
*/
class Main
{
/** @var Cache\Cache cache instance */
protected $cache;
/** @var array<string,mixed> current config */
protected $config;
/** @var Connector\Connector remote connector */
protected $connector = null;
/** @var string response content type */
protected $contentType = "text/html";
/** @var \Mysqli database instance */
protected $dbh;
/** @var array<string,string> extra headers */
protected $headers = [];
/** @var \Language languange instance */
protected $lang = null;
/** @var ?string redirect URL */
protected $redirect = null;
/** @var int response code */
protected $responseCode = 200;
/** @var \Smarty\Smarty|null smarty instance */
protected $smarty;
/**
* constructor
*/
public function __construct()
{
$this->config = parse_ini_file('../config.ini', true);
}
/**
* do a remote call and return results
* @param string $remoteURL remote URL
* @param string[]|null $headers optional headers to send
* @return array{string, mixed} response and status information
*/
public static function getFromRemote(string $remoteURL, $headers)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $remoteURL);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
if ($headers !== null) {
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
}
curl_setopt($ch, CURLOPT_VERBOSE, true);
curl_setopt($ch, CURLINFO_HEADER_OUT, true);
$ret = curl_exec($ch);
$info = curl_getinfo($ch);
curl_close($ch);
return [$ret, $info];
}
/**
* get config
* @return Array<String, Mixed>
*/
public function getConfig()
{
return $this->config;
}
/**
* load plugins
*/
public function loadPlugins() : void
{
if (array_key_exists('plugins', $this->config)) {
$plugins = $this->config['plugins'];
foreach ($plugins as $name => $file) {
require_once($file);
$fktn = 'Federator\\' . $name . '_load';
if (function_exists($fktn)) {
$fktn($this);
}
}
}
}
/**
* open database
*/
public function openDatabase() : void
{
$dbconf = $this->config["database"];
$this->dbh = new \mysqli($dbconf['host'], $dbconf['username'], $dbconf['password'], $dbconf['database']);
if ($this->dbh->connect_error !== null) {
http_response_code(500);
die('Database Connect Error');
}
}
/**
* set cache
*/
public function setCache(Cache\Cache $cache) : void
{
$this->cache = $cache;
}
/**
* set connector
*/
public function setConnector(Connector\Connector $connector) : void
{
$this->connector = $connector;
}
/**
* set response code
*
* @param int $code
* new response code
*/
public function setResponseCode(int $code) : void
{
$this->responseCode = $code;
}
/**
* translate given group and key using given language
*
* @param ?string $lang
* language to use
* @param string $group
* language group to use
* @param string $key
* language key to use
* @param mixed[] $parameters
* optional parameters
* @return string translation
*/
public function translate(?string $lang, string $group, string $key, array $parameters = array()) : string
{
if ($this->lang === null) {
$this->validLanguage($lang);
}
if ($this->lang !== null) {
if ($this->lang->getLang() !== $lang) {
$l = new \Language($lang);
return $l->printlang($group, $key, $parameters);
}
return $this->lang->printlang($group, $key, $parameters);
} else {
return $key;
}
}
/**
* check if language is valid by loading it
*
* @param ?string $lang
*/
public function validLanguage(?string $lang) : bool
{
$language = new \Language($lang);
if ($language->getLang() === $lang) {
$this->lang = $language;
return true;
}
return false;
}
}

View file

@ -0,0 +1,43 @@
<?php
/**
* SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
* SPDX-License-Identifier: GPL-3.0-or-later
* @author Author: Sascha Nitsch (grumpydeveloper)
**/
namespace Federator;
/**
* dummy connector that always return the same permission
*/
class DummyConnector implements Connector\Connector
{
/**
* constructor
*/
public function __construct()
{
}
/**
* get remote user by given session
* @param string $_session session id
* @param string $_user user or profile name
* @return Data\User | false
*/
public function getRemoteUserBySession(string $_session, string $_user)
{
// validate $_session and $user
$user = new Data\User();
$user->id = $_user;
$user->permissions = ['PUBLISH'];
$user->session = $_session;
return $user;
}
}
function dummy_load(Main $main)
{
$dummy = new DummyConnector();
$main->setConnector($dummy);
}

27
progress.md Normal file
View file

@ -0,0 +1,27 @@
# progress tracker
## done
## goals v1.0
primary goal is to connect ContentNation via ActivityPub again.
- [ ] browse the fediverse as a logged in user
- [ ] reply to posts
- [ ] like posts
- [ ] share posts
- [ ] comment on CN article via AP
- [ ] get notifications on AP if someone interacts with comment
## roadmap to v1.0
- [X] API framework
- [X] interfact to connect to existing service
- [ ] overlay to extend with needed info like private keys, urls, ...
- [ ] cache layer for users
- [ ] webfinger
- [ ] discovery endpoints
- [ ] ap outbox
- [ ] ap inbox
- [ ] support for AP profile in service
- [ ] support for article
- [ ] support for comment
- [ ] posting comments from ap to service
- [ ] callback from service to add new input

1
release.sh Normal file
View file

@ -0,0 +1 @@
git log | head -n1 | awk '{print "<?php\nglobal $version;\n$version=\"" $2 "\";\n"}' > php/version.php