commit ef25c5b3afecae05db273d31d85914b8cea12d03 Author: Sascha Nitsch Date: Mon Jul 15 20:46:44 2024 +0200 initial import diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aba6f62 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +composer.lock +vendor +php/version.php diff --git a/.phan/config.php b/.phan/config.php new file mode 100644 index 0000000..eabb1da --- /dev/null +++ b/.phan/config.php @@ -0,0 +1,369 @@ + '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` can cast to `array` 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 + // 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' => [], +]; diff --git a/README.md b/README.md new file mode 100644 index 0000000..61c7ed5 --- /dev/null +++ b/README.md @@ -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: + + + RewriteEngine on + RewriteBase / + RewriteRule ^api/(.+)$ api.php?_call=$1 [L] + + +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. + diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..5deb518 --- /dev/null +++ b/composer.json @@ -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" + } +} diff --git a/config.ini b/config.ini new file mode 100644 index 0000000..8e0269d --- /dev/null +++ b/config.ini @@ -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' diff --git a/htdocs/api.php b/htdocs/api.php new file mode 100644 index 0000000..9e921b7 --- /dev/null +++ b/htdocs/api.php @@ -0,0 +1,22 @@ +run(); diff --git a/php/api.php b/php/api.php new file mode 100644 index 0000000..5a74b39 --- /dev/null +++ b/php/api.php @@ -0,0 +1,252 @@ + 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', '<${1}>', $_input); + $out = preg_replace('/<\/(script)>/i', '</${1};>', $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 $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; + } +} diff --git a/php/api/v1.php b/php/api/v1.php new file mode 100644 index 0000000..69663c6 --- /dev/null +++ b/php/api/v1.php @@ -0,0 +1,28 @@ + $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"; + } +} diff --git a/php/connector/connector.php b/php/connector/connector.php new file mode 100644 index 0000000..362c11d --- /dev/null +++ b/php/connector/connector.php @@ -0,0 +1,23 @@ + 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); + } +} diff --git a/php/dio/user.php b/php/dio/user.php new file mode 100644 index 0000000..c1e5d86 --- /dev/null +++ b/php/dio/user.php @@ -0,0 +1,60 @@ +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; + } +} diff --git a/php/exceptions/exception.php b/php/exceptions/exception.php new file mode 100644 index 0000000..8f52def --- /dev/null +++ b/php/exceptions/exception.php @@ -0,0 +1,25 @@ +message = ($message === null) ? "filenotfound" : $message; + } + + /** + * + * {@inheritDoc} + * @see \Exceptions\Exception::getRetCode() + */ + public function getRetCode() : int + { + return 404; + } +} diff --git a/php/exceptions/invalidargument.php b/php/exceptions/invalidargument.php new file mode 100644 index 0000000..4f62fa3 --- /dev/null +++ b/php/exceptions/invalidargument.php @@ -0,0 +1,34 @@ +message = ($message === null) ? "invalidargument" : $message; + } + + /** + * + * {@inheritDoc} + * @see \Exceptions\Exception::getRetCode() + */ + public function getRetCode() : int + { + return 400; + } +} diff --git a/php/exceptions/permissiondenied.php b/php/exceptions/permissiondenied.php new file mode 100644 index 0000000..ebc8c3b --- /dev/null +++ b/php/exceptions/permissiondenied.php @@ -0,0 +1,36 @@ +message = ($message === null) ? "permissiondenied" : $message; + } + + /** + * + * {@inheritdoc} + * @see \Exceptions\Exception::getRetCode() + */ + public function getRetCode() : int + { + return 403; + } +} diff --git a/php/exceptions/servererror.php b/php/exceptions/servererror.php new file mode 100644 index 0000000..b0bd6a4 --- /dev/null +++ b/php/exceptions/servererror.php @@ -0,0 +1,34 @@ +message = ($message === null) ? "servererror" : $message; + } + + /** + * + * {@inheritDoc} + * @see \Exceptions\Exception::getRetCode() + */ + public function getRetCode() : int + { + return 500; + } +} diff --git a/php/exceptions/unauthorized.php b/php/exceptions/unauthorized.php new file mode 100644 index 0000000..b6f7fea --- /dev/null +++ b/php/exceptions/unauthorized.php @@ -0,0 +1,25 @@ + true, + "en" => true, + "xy" => true + ); + + /** + * language to use + * @var string + */ + private $uselang; + + /** + * actual language data + * @var array> + */ + 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 $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 ">>$group:$key<<"; + } + + + /** + * get keys (valid ids) of a named group + * + * @param string $group + * group name to fetch keys + * @return array 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 $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 $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 $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); +} diff --git a/php/main.php b/php/main.php new file mode 100644 index 0000000..d40f619 --- /dev/null +++ b/php/main.php @@ -0,0 +1,178 @@ + 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 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 + */ + 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; + } +} diff --git a/plugins/dummyconnector.php b/plugins/dummyconnector.php new file mode 100644 index 0000000..d44f422 --- /dev/null +++ b/plugins/dummyconnector.php @@ -0,0 +1,43 @@ +id = $_user; + $user->permissions = ['PUBLISH']; + $user->session = $_session; + return $user; + } +} + +function dummy_load(Main $main) +{ + $dummy = new DummyConnector(); + $main->setConnector($dummy); +} diff --git a/progress.md b/progress.md new file mode 100644 index 0000000..851b42c --- /dev/null +++ b/progress.md @@ -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 diff --git a/release.sh b/release.sh new file mode 100644 index 0000000..0dc7ef3 --- /dev/null +++ b/release.sh @@ -0,0 +1 @@ +git log | head -n1 | awk '{print " php/version.php