diff --git a/.gitignore b/.gitignore index 0d18ab0bd..e70f651f5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,11 @@ favicon.* -.htconfig.php -.htpreconfig.php +/.htconfig.php +/.htpreconfig.php \#* *.log *.out *.version* home.html - *~ robots.txt @@ -74,3 +73,6 @@ venv/ #ignore filesystem storage default path /storage + +#Ignore log folder +/log diff --git a/.travis.yml b/.travis.yml index c66457325..e2aa84f5c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,6 +19,11 @@ before_script: - cp config/local-sample.config.php config/local.config.php - mysql -e 'CREATE DATABASE IF NOT EXISTS test;' - mysql -utravis test < database.sql - - echo "extension=redis.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini - - echo "extension=memcached.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini + - pecl channel-update pecl.php.net + - pecl config-set preferred_state beta + - if [[ $TRAVIS_PHP_VERSION != "7.1" ]]; then echo yes | pecl upgrade apcu; fi + - if [[ $TRAVIS_PHP_VERSION != "7.1" ]]; then phpenv config-add .travis/apcu.ini; fi + - phpenv config-add .travis/redis.ini + - phpenv config-add .travis/memcached.ini + after_success: bash <(curl -s https://codecov.io/bash) diff --git a/.travis/apcu.ini b/.travis/apcu.ini new file mode 100644 index 000000000..92598662c --- /dev/null +++ b/.travis/apcu.ini @@ -0,0 +1,4 @@ +extension="apcu.so" + +apc.enabled = 1 +apc.enable_cli = 1 \ No newline at end of file diff --git a/.travis/memcached.ini b/.travis/memcached.ini new file mode 100644 index 000000000..c9a2ff0c9 --- /dev/null +++ b/.travis/memcached.ini @@ -0,0 +1 @@ +extension="memcached.so" \ No newline at end of file diff --git a/.travis/redis.ini b/.travis/redis.ini new file mode 100644 index 000000000..ab995b837 --- /dev/null +++ b/.travis/redis.ini @@ -0,0 +1 @@ +extension="redis.so" \ No newline at end of file diff --git a/CHANGELOG b/CHANGELOG index 127f7e68e..1fa98ee87 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,78 @@ +Version 2019.06 (2019-06-23) + Friendica Core: + Update to the tranlation (CS, DE, EN-GB, EN-US, ET, FR, IT, PL, PT-BR, SV) [translation teams] + Update to the documentation [nupplaphil, realkinetix, MrPetovan] + Update to the themes (frio, vier) [BinkaDroid, MrPetovan, tobiasd] + Enhancements to the API [annando, MrPetovan] + Enhancements to the way reshares are handled [annando] + Enhancements to the redis configuration [nupplaphil] + Enhancements to the federation stats display in the admin panel [tobiasd] + Enhancements to the processing of changed storage engine [MrPetovan] + Enhancements to ActivityPub support [annando, MrPetovan] + Enhancements to code security [MrPetovan] + Enhancements to delivery counter [annando] + Fixed the notification order [JeroenED] + Fixed the timezone of Friendica logs [nupplaphil] + Fixed tag completion painfully slow [AlfredSK] + Fixed a regression in notifications [MrPetovan, annando] + Fixed an issue with smilies and code blocks [MrPetovan] + Fixed an AP issue with unavailable local profiles [MrPetovan] + Fixed an issue with the File to Folder feature [MrPetovan] + Fixed an issue with the legacy storage engine [fabrixxm] + Fixed an issue with the theme and addon path items [MrPetovan] + Fixed an issue occuring when the BasePath was not set [tobiasd] + Fixed an issue with additionally opened Sessions [MrPetovan] + Fixed an issue with legacy loglevel mapping [nupplaphil] + Fixed contact suggestions [annando] + Fixed an issue with frio hovercard [nupplaphil] + Fixed event interaction federation [annando] + Fixed remote image permission [deantownsley] + General Code cleaning and restructuring [annando, nupplaphil, tobiasd] + Added frio color scheme sharing [JeroenED] + Added syslog and stream Logger [nupplaphil] + Added storage move cronjob [MrPetovan] + Added collapsible panel for connector permission fields [MrPetovan] + Added rule-based router [MrPetovan] + Added Estonian translation [Rain Hawk] + Added APCu caching [nupplaphil] + Added BlockServer command to the Friendica console [nupplaphil] + Added reshare count [annando] + Added rule-based router [MrPetovan, nupplaphil] + Added themed error pages with mascot [MrPetovan, lostinlight] + Added contact relationship filter [MrPetovan] + Removed the old queue mechanism (deferred workers are now used) [annando] + Removed BasePath and Hostname settings from the admin panel [nupplaphil] + Remove support for defunct F-Droid Friendica app [MrPetovan] + + Friendica Addons: + Update to the tranlation (ET, SV, ZH_CN) [translation teams] + botdetection: + Added a new addon for preventing access by bots [nupplaphil, annando] + buffer: + Traces of Google+ were removed [annando] + curweather: + Fixed a problem with the display of the correct temperature unit [tobiasd] + fromgplus: + Deprecated the addon as Google+ was closed [tobiasd] + fortunate: + Deprecated addon for incompatibility with latest Friendica version [MrPetovan] + phpmailer: + Added a new addon to use external SMTP for email [M-arcus, kecalcze, MrPetovan] + pledgie: + Deprecated addon as service was discontinued [M-arcus] + xmpp: + Marked addon as unsupported because of various incompatibilities with themes [MrPetovan] + + Closed Issues: + 1012, 2209, 2528, 3309, 3717, 3816, 3869, 4453, 4999, 5011, 5047, 5276, 5850, 5983, 6303, 6319, 6379, 6410, 6477, + 6478, 6720, 6799, 6813, 6819, 6861, 6864, 6879, 6903, 6916, 6917, 6918, 6921, 6927, 6929, 6936, 6938, 6941, 6943, + 6947, 6948, 6950, 6952, 6983, 6999, 7023, 7036, 7047, 7106, 7112, 7119, 7128, 7130, 7131, 7141, 7142, 7150, 7171, + 7183, 7196, 7209, 7223, 7226, 7240, 7241, 7249, 7264, 7269, 7271, 7275, 7300, 7303 + Version 2019.04 (2019-04-28) Friendica Core: Fixed a privacy problem with postings accessed by feed [MrPetovan] - + Version 2019.03 (2019-03-22) Friendica Core: Update to the translation (CS, DE, EN-GB, EN-US, ES, FR, IT, PL, SV, ZH-CN) [translation teams] diff --git a/CREDITS.txt b/CREDITS.txt index 3b3e6ad30..c47a8a623 100644 --- a/CREDITS.txt +++ b/CREDITS.txt @@ -1,3 +1,5 @@ + + 23n Abinoam P. Marques Jr. Abraham Pérez Hernández @@ -26,6 +28,7 @@ Andy Hee Angristan Anthronaut Arian - Cazare Muncitori +Asher Pen Athalbert aweiher axelt @@ -37,6 +40,8 @@ Beluga Ben Ben Roberts ben-utzer +BinkaDroid +Bjoessi bufalo1973 Calango Jr Carlos Solís @@ -107,6 +112,7 @@ Jens Tautenhahn jensp Jeroen De Meerleer jeroenpraat +JOduMonT Johannes Schwab John Brazil Jonatan Nyberg @@ -175,6 +181,7 @@ R C Rabuzarus Radek Rafael Garau +Rain Hawk Rainulf Pineda Ralf Thees Ralph @@ -183,6 +190,7 @@ rcmaniac rebeka-catalina repat Ricardo Pereira +Rik 4 RJ Madsen Roland Häder Rui Andrada @@ -207,6 +215,7 @@ Steffen K9 StefOfficiel Sveinn í Felli Sven Anders +Sylke Vicious Sylvain Lagacé szymon.filip Sérgio Lima @@ -225,6 +234,7 @@ tomacat tomamplius tomtom84 Tony Baldwin +Torbjörn Andersson TORminator trebor tschlotfeldt @@ -234,6 +244,7 @@ U-SOUND\mike ufic Ulf Rompe Unknown +Valvin Vasudev Kamath Vasya Novikov Vinzenz Vietzke diff --git a/INSTALL.md b/INSTALL.md index b6107e006..50e86e24c 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -29,7 +29,7 @@ is self-signed). ## 1. Requirements - Apache with mod-rewrite enabled and "Options All" so you can use a local .htaccess file -- PHP 5.6.1+ (PHP 7.1+ recommended for performance and official support). +- PHP 7+ (PHP 7.1+ recommended for performance and official support). - PHP *command line* with `register_argc_argv = true` in php.ini - curl, gd (with at least jpeg support), mysql, mbstring, xml, zip and openssl extensions - Some form of email server or email gateway such that PHP mail() works diff --git a/README.md b/README.md index 20d6b0ef5..d849a4395 100644 --- a/README.md +++ b/README.md @@ -16,26 +16,26 @@ Join today and [get your Friendica profile!](https://dir.friendica.social/server ### Friendica Screenshots -| ![Frio theme in mobile browser](/images/screenshots/friendica-frio-mobile-profle-1.png?raw=true "Frio theme in mobile browser") ![Frio theme in mobile browser](/images/screenshots/friendica-frio-mobile-profle-2.png?raw=true "Frio theme in mobile browser") +| ![Frio theme in mobile browser](images/screenshots/friendica-frio-mobile-profle-1.png?raw=true "Frio theme in mobile browser") ![Frio theme in mobile browser](images/screenshots/friendica-frio-mobile-profle-2.png?raw=true "Frio theme in mobile browser") |:--:| |*Frio theme, mobile browser. Timeline and composer view.*| -|![Frio theme in desktop browser](/images/screenshots/friendica-frio-green-profle-1.png?raw=true "Frio theme in desktop browser") +|![Frio theme in desktop browser](images/screenshots/friendica-frio-green-profle-1.png?raw=true "Frio theme in desktop browser") |*Frio theme, desktop browser. Timeline view, contact info popped up, control menu open.*| -|![Frio theme in desktop browser](/images/screenshots/friendica-frio-green-profle-2.png?raw=true "Frio theme in desktop browser") +|![Frio theme in desktop browser](images/screenshots/friendica-frio-green-profle-2.png?raw=true "Frio theme in desktop browser") |*Frio theme, desktop browser. Menu open for controlling individual posts.*| -|![Frio theme in desktop browser](/images/screenshots/friendica-frio-red-profle-3.png?raw=true "Frio theme in desktop browser") +|![Frio theme in desktop browser](images/screenshots/friendica-frio-red-profle-3.png?raw=true "Frio theme in desktop browser") |*Frio theme, desktop browser. Profile view, notification menu open.*| -|![Frio theme in desktop browser](/images/screenshots/friendica-frio-red-profle-2.png?raw=true "Frio theme in desktop browser") +|![Frio theme in desktop browser](images/screenshots/friendica-frio-red-profle-2.png?raw=true "Frio theme in desktop browser") |*Number of new posts, in total and by group.*| -|![Frio theme in desktop browser](/images/screenshots/friendica-frio-red-profle-1.png?raw=true "Frio theme in desktop browser") +|![Frio theme in desktop browser](images/screenshots/friendica-frio-red-profle-1.png?raw=true "Frio theme in desktop browser") |*Calender with popup of event.*| -|![Frio theme default colour in standard browser on tablet](/images/screenshots/friendica-frio-default-profile-1.png?raw=true "Frio theme default colour in standard browser on tablet") +|![Frio theme default colour in standard browser on tablet](images/screenshots/friendica-frio-default-profile-1.png?raw=true "Frio theme default colour in standard browser on tablet") |*Notifications menu and private messages counter, standard browser on tablet.*| -|![Frio theme in desktop browser](/images/screenshots/friendica-frio-brown-profile-2.png?raw=true "Frio theme in desktop browser") +|![Frio theme in desktop browser](images/screenshots/friendica-frio-brown-profile-2.png?raw=true "Frio theme in desktop browser") |*Number of visible contacts, standard browser.*| -|![Frio theme in desktop browser](/images/screenshots/friendica-frio-brown-profile-1.png?raw=true "Frio theme in desktop browser") +|![Frio theme in desktop browser](images/screenshots/friendica-frio-brown-profile-1.png?raw=true "Frio theme in desktop browser") |*Network posts chronologically ordered, standard browser.*| -|![Vier theme in desktop browser](/images/screenshots/friendica-vier-profile.png?raw=true "Vier theme in desktop browser") +|![Vier theme in desktop browser](images/screenshots/friendica-vier-profile.png?raw=true "Vier theme in desktop browser") |*Vier theme, desktop browser. Public timeline view.*| -|![Vier theme in desktop browser](/images/screenshots/friendica-vier-community.png?raw=true "Vier theme in desktop browser") +|![Vier theme in desktop browser](images/screenshots/friendica-vier-community.png?raw=true "Vier theme in desktop browser") |*Vier theme, desktop browser. Community post displayed.*| diff --git a/VERSION b/VERSION index 3365fe156..4eda9a567 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2019.04 +2019.06 diff --git a/bin/daemon.php b/bin/daemon.php index 298cfa253..047bf71be 100755 --- a/bin/daemon.php +++ b/bin/daemon.php @@ -144,9 +144,7 @@ if (!$foreground) { file_put_contents($pidfile, $pid); // We lose the database connection upon forking - /// @todo refactoring during https://github.com/friendica/friendica/issues/6720 - $basePath = \Friendica\Util\BasePath::create(dirname(__DIR__), $_SERVER); - Factory\DBFactory::init($basePath, $a->getConfigCache(), $a->getProfiler(), $_SERVER); + Factory\DBFactory::init($a->getConfigCache(), $a->getProfiler(), $_SERVER); } Config::set('system', 'worker_daemon_mode', true); diff --git a/bin/worker.php b/bin/worker.php index c7174d81e..2b5915f47 100755 --- a/bin/worker.php +++ b/bin/worker.php @@ -33,7 +33,7 @@ require dirname(__DIR__) . '/vendor/autoload.php'; $a = Factory\DependencyFactory::setUp('worker', dirname(__DIR__)); // Check the database structure and possibly fixes it -Update::check($a->getBasePath(), true); +Update::check($a->getBasePath(), true, $a->getMode()); // Quit when in maintenance if (!$a->getMode()->has(App\Mode::MAINTENANCEDISABLED)) { diff --git a/boot.php b/boot.php index 95ea6579a..4ade9eb14 100644 --- a/boot.php +++ b/boot.php @@ -31,7 +31,7 @@ use Friendica\Util\DateTimeFormat; define('FRIENDICA_PLATFORM', 'Friendica'); define('FRIENDICA_CODENAME', 'Dalmatian Bellflower'); -define('FRIENDICA_VERSION', '2019.04'); +define('FRIENDICA_VERSION', '2019.06'); define('DFRN_PROTOCOL_VERSION', '2.23'); define('NEW_UPDATE_ROUTINE_VERSION', 1170); @@ -82,17 +82,6 @@ define('MAX_IMAGE_LENGTH', -1); */ define('DEFAULT_DB_ENGINE', 'InnoDB'); -/** - * @name SSL Policy - * - * SSL redirection policies - * @{ - */ -define('SSL_POLICY_NONE', 0); -define('SSL_POLICY_FULL', 1); -define('SSL_POLICY_SELFSIGN', 2); -/* @}*/ - /** @deprecated since version 2019.03, please use \Friendica\Module\Register::CLOSED instead */ define('REGISTER_CLOSED', \Friendica\Module\Register::CLOSED); /** @deprecated since version 2019.03, please use \Friendica\Module\Register::APPROVE instead */ @@ -204,6 +193,7 @@ define('NAMESPACE_ZOT', 'http://purl.org/zot'); define('NAMESPACE_DFRN', 'http://purl.org/macgirvin/dfrn/1.0'); define('NAMESPACE_THREAD', 'http://purl.org/syndication/thread/1.0'); define('NAMESPACE_TOMB', 'http://purl.org/atompub/tombstones/1.0'); +define('NAMESPACE_ACTIVITY2', 'https://www.w3.org/ns/activitystreams#'); define('NAMESPACE_ACTIVITY', 'http://activitystrea.ms/spec/1.0/'); define('NAMESPACE_ACTIVITY_SCHEMA', 'http://activitystrea.ms/schema/1.0/'); define('NAMESPACE_MEDIA', 'http://purl.org/syndication/atommedia'); @@ -246,6 +236,7 @@ define('ACTIVITY_FAVORITE', NAMESPACE_ACTIVITY_SCHEMA . 'favorite'); define('ACTIVITY_UNFAVORITE', NAMESPACE_ACTIVITY_SCHEMA . 'unfavorite'); define('ACTIVITY_SHARE', NAMESPACE_ACTIVITY_SCHEMA . 'share'); define('ACTIVITY_DELETE', NAMESPACE_ACTIVITY_SCHEMA . 'delete'); +define('ACTIVITY2_ANNOUNCE', NAMESPACE_ACTIVITY2 . 'Announce'); define('ACTIVITY_POKE', NAMESPACE_ZOT . '/activity/poke'); @@ -543,39 +534,6 @@ function is_site_admin() return local_user() && $admin_email && in_array(defaults($a->user, 'email', ''), $adminlist); } -/** - * @brief Returns querystring as string from a mapped array. - * - * @param array $params mapped array with query parameters - * @param string $name of parameter, default null - * - * @return string - */ -function build_querystring($params, $name = null) -{ - $ret = ""; - foreach ($params as $key => $val) { - if (is_array($val)) { - /// @TODO maybe not compare against null, use is_null() - if ($name == null) { - $ret .= build_querystring($val, $key); - } else { - $ret .= build_querystring($val, $name . "[$key]"); - } - } else { - $val = urlencode($val); - /// @TODO maybe not compare against null, use is_null() - if ($name != null) { - /// @TODO two string concated, can be merged to one - $ret .= $name . "[$key]" . "=$val&"; - } else { - $ret .= "$key=$val&"; - } - } - } - return $ret; -} - function explode_querystring($query) { $arg_st = strpos($query, '?'); diff --git a/composer.json b/composer.json index 765ec23a2..aac5c10bc 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ "issues": "https://github.com/friendica/friendica/issues" }, "require": { - "php": ">=5.6.1", + "php": ">=7.0", "ext-ctype": "*", "ext-curl": "*", "ext-dom": "*", @@ -27,6 +27,7 @@ "ext-simplexml": "*", "ext-xml": "*", "asika/simple-console": "^1.0", + "bacon/bacon-qr-code": "^1.0", "divineomega/password_exposed": "^2.4", "ezyang/htmlpurifier": "~4.7.0", "friendica/json-ld": "^1.0", @@ -35,14 +36,18 @@ "michelf/php-markdown": "^1.7", "mobiledetect/mobiledetectlib": "2.8.*", "monolog/monolog": "^1.24", - "paragonie/random_compat": "^2.0", + "nikic/fast-route": "^1.3", + "paragonie/hidden-string": "^1.0", "pear/text_languagedetect": "1.*", + "pragmarx/google2fa": "^5.0", + "pragmarx/recovery": "^0.1.0", "psr/container": "^1.0", "seld/cli-prompt": "^1.0", "smarty/smarty": "^3.1", "fxp/composer-asset-plugin": "~1.3", "bower-asset/base64": "^1.0", "bower-asset/chart-js": "^2.7", + "bower-asset/dompurify": "^1.0", "bower-asset/perfect-scrollbar": "^0.6", "bower-asset/vue": "^2.5", "npm-asset/jquery": "^2.0", @@ -52,7 +57,8 @@ "npm-asset/moment": "^2.20.1", "npm-asset/fullcalendar": "^3.0.1", "npm-asset/cropperjs": "1.2.2", - "npm-asset/imagesloaded": "4.1.4" + "npm-asset/imagesloaded": "4.1.4", + "pear/console_table": "^1.3" }, "repositories": [ { diff --git a/composer.lock b/composer.lock index ff38c8105..af51b6dfe 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8897c1f6912cc9b889534a8c59deead1", + "content-hash": "eb985236d64ed0b0fe1fc2e4ac6616e2", "packages": [ { "name": "asika/simple-console", @@ -39,6 +39,52 @@ "description": "One file console framework to help you write build scripts.", "time": "2018-03-08T12:05:40+00:00" }, + { + "name": "bacon/bacon-qr-code", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/Bacon/BaconQrCode.git", + "reference": "5a91b62b9d37cee635bbf8d553f4546057250bee" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/5a91b62b9d37cee635bbf8d553f4546057250bee", + "reference": "5a91b62b9d37cee635bbf8d553f4546057250bee", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": "^5.4|^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8" + }, + "suggest": { + "ext-gd": "to generate QR code images" + }, + "type": "library", + "autoload": { + "psr-0": { + "BaconQrCode": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Ben Scholzen 'DASPRiD'", + "email": "mail@dasprids.de", + "homepage": "http://www.dasprids.de", + "role": "Developer" + } + ], + "description": "BaconQrCode is a QR code generator for PHP.", + "homepage": "https://github.com/Bacon/BaconQrCode", + "time": "2017-10-17T09:59:25+00:00" + }, { "name": "bower-asset/Chart-js", "version": "v2.7.2", @@ -102,6 +148,51 @@ "description": "Base64 encoding and decoding", "time": "2017-03-25T21:16:21+00:00" }, + { + "name": "bower-asset/dompurify", + "version": "1.0.10", + "source": { + "type": "git", + "url": "https://github.com/cure53/DOMPurify.git", + "reference": "b537cab466329b1b077e0e5e3c14edad2b7142f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cure53/DOMPurify/zipball/b537cab466329b1b077e0e5e3c14edad2b7142f7", + "reference": "b537cab466329b1b077e0e5e3c14edad2b7142f7", + "shasum": "" + }, + "type": "bower-asset-library", + "extra": { + "bower-asset-main": "src/purify.js", + "bower-asset-ignore": [ + "**/.*", + "demos", + "scripts", + "test", + "website" + ] + }, + "license": [ + "MPL-2.0", + "Apache-2.0" + ], + "description": "A DOM-only, super-fast, uber-tolerant XSS sanitizer for HTML, MathML and SVG", + "keywords": [ + "cross site scripting", + "dom", + "filter", + "html", + "mathml", + "sanitize", + "sanitizer", + "secure", + "security", + "svg", + "xss" + ], + "time": "2019-02-19T13:27:01+00:00" + }, { "name": "bower-asset/perfect-scrollbar", "version": "0.6.16", @@ -887,6 +978,52 @@ ], "time": "2018-11-05T09:00:11+00:00" }, + { + "name": "nikic/fast-route", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/FastRoute.git", + "reference": "181d480e08d9476e61381e04a71b34dc0432e812" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/FastRoute/zipball/181d480e08d9476e61381e04a71b34dc0432e812", + "reference": "181d480e08d9476e61381e04a71b34dc0432e812", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35|~5.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "FastRoute\\": "src/" + }, + "files": [ + "src/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov", + "email": "nikic@php.net" + } + ], + "description": "Fast request router for PHP", + "keywords": [ + "router", + "routing" + ], + "time": "2018-02-13T20:26:39+00:00" + }, { "name": "npm-asset/cropperjs", "version": "1.2.2", @@ -1586,25 +1723,24 @@ }, { "name": "paragonie/constant_time_encoding", - "version": "v1.0.4", + "version": "v2.2.3", "source": { "type": "git", "url": "https://github.com/paragonie/constant_time_encoding.git", - "reference": "2132f0f293d856026d7d11bd81b9f4a23a1dc1f6" + "reference": "55af0dc01992b4d0da7f6372e2eac097bbbaffdb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/2132f0f293d856026d7d11bd81b9f4a23a1dc1f6", - "reference": "2132f0f293d856026d7d11bd81b9f4a23a1dc1f6", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/55af0dc01992b4d0da7f6372e2eac097bbbaffdb", + "reference": "55af0dc01992b4d0da7f6372e2eac097bbbaffdb", "shasum": "" }, "require": { - "php": "^5.3|^7" + "php": "^7" }, "require-dev": { - "paragonie/random_compat": "^1.4|^2", - "phpunit/phpunit": "4.*|5.*", - "vimeo/psalm": "^0.3|^1" + "phpunit/phpunit": "^6|^7", + "vimeo/psalm": "^1|^2" }, "type": "library", "autoload": { @@ -1645,37 +1781,82 @@ "hex2bin", "rfc4648" ], - "time": "2018-04-30T17:57:16+00:00" + "time": "2019-01-03T20:26:31+00:00" }, { - "name": "paragonie/random_compat", - "version": "v2.0.17", + "name": "paragonie/hidden-string", + "version": "v1.0.0", "source": { "type": "git", - "url": "https://github.com/paragonie/random_compat.git", - "reference": "29af24f25bab834fcbb38ad2a69fa93b867e070d" + "url": "https://github.com/paragonie/hidden-string.git", + "reference": "0bbb00be0e33b8e1d48fa79ea35cd42d3091a936" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/random_compat/zipball/29af24f25bab834fcbb38ad2a69fa93b867e070d", - "reference": "29af24f25bab834fcbb38ad2a69fa93b867e070d", + "url": "https://api.github.com/repos/paragonie/hidden-string/zipball/0bbb00be0e33b8e1d48fa79ea35cd42d3091a936", + "reference": "0bbb00be0e33b8e1d48fa79ea35cd42d3091a936", "shasum": "" }, "require": { - "php": ">=5.2.0" + "paragonie/constant_time_encoding": "^2", + "paragonie/sodium_compat": "^1.6", + "php": "^7" }, "require-dev": { - "phpunit/phpunit": "4.*|5.*" + "phpunit/phpunit": "^6|^7", + "vimeo/psalm": "^1" + }, + "type": "library", + "autoload": { + "psr-4": { + "ParagonIE\\HiddenString\\": "./src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MPL-2.0" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "info@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "Encapsulate strings in an object to hide them from stack traces", + "homepage": "https://github.com/paragonie/hidden-string", + "keywords": [ + "hidden", + "stack trace", + "string" + ], + "time": "2018-05-07T20:28:06+00:00" + }, + { + "name": "paragonie/random_compat", + "version": "v9.99.99", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95", + "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95", + "shasum": "" + }, + "require": { + "php": "^7" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*", + "vimeo/psalm": "^1" }, "suggest": { "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." }, "type": "library", - "autoload": { - "files": [ - "lib/random.php" - ] - }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" @@ -1694,7 +1875,7 @@ "pseudorandom", "random" ], - "time": "2018-07-04T16:31:37+00:00" + "time": "2018-07-02T15:55:56+00:00" }, { "name": "paragonie/sodium_compat", @@ -1778,6 +1959,61 @@ ], "time": "2018-09-22T03:59:58+00:00" }, + { + "name": "pear/console_table", + "version": "v1.3.1", + "source": { + "type": "git", + "url": "https://github.com/pear/Console_Table.git", + "reference": "1930c11897ca61fd24b95f2f785e99e0f36dcdea" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pear/Console_Table/zipball/1930c11897ca61fd24b95f2f785e99e0f36dcdea", + "reference": "1930c11897ca61fd24b95f2f785e99e0f36dcdea", + "shasum": "" + }, + "require": { + "php": ">=5.2.0" + }, + "suggest": { + "pear/Console_Color2": ">=0.1.2" + }, + "type": "library", + "autoload": { + "classmap": [ + "Table.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Jan Schneider", + "homepage": "http://pear.php.net/user/yunosh" + }, + { + "name": "Tal Peer", + "homepage": "http://pear.php.net/user/tal" + }, + { + "name": "Xavier Noguer", + "homepage": "http://pear.php.net/user/xnoguer" + }, + { + "name": "Richard Heyes", + "homepage": "http://pear.php.net/user/richard" + } + ], + "description": "Library that makes it easy to build console style tables.", + "homepage": "http://pear.php.net/package/Console_Table/", + "keywords": [ + "console" + ], + "time": "2018-01-25T20:47:17+00:00" + }, { "name": "pear/text_languagedetect", "version": "v1.0.0", @@ -1822,6 +2058,189 @@ "homepage": "http://pear.php.net/package/Text_LanguageDetect", "time": "2017-03-02T16:14:08+00:00" }, + { + "name": "pragmarx/google2fa", + "version": "v5.0.0", + "source": { + "type": "git", + "url": "https://github.com/antonioribeiro/google2fa.git", + "reference": "17c969c82f427dd916afe4be50bafc6299aef1b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/17c969c82f427dd916afe4be50bafc6299aef1b4", + "reference": "17c969c82f427dd916afe4be50bafc6299aef1b4", + "shasum": "" + }, + "require": { + "paragonie/constant_time_encoding": "~1.0|~2.0", + "paragonie/random_compat": ">=1", + "php": ">=5.4", + "symfony/polyfill-php56": "~1.2" + }, + "require-dev": { + "phpunit/phpunit": "~4|~5|~6" + }, + "type": "library", + "extra": { + "component": "package", + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "PragmaRX\\Google2FA\\": "src/", + "PragmaRX\\Google2FA\\Tests\\": "tests/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Antonio Carlos Ribeiro", + "email": "acr@antoniocarlosribeiro.com", + "role": "Creator & Designer" + } + ], + "description": "A One Time Password Authentication package, compatible with Google Authenticator.", + "keywords": [ + "2fa", + "Authentication", + "Two Factor Authentication", + "google2fa" + ], + "time": "2019-03-19T22:44:16+00:00" + }, + { + "name": "pragmarx/random", + "version": "v0.2.2", + "source": { + "type": "git", + "url": "https://github.com/antonioribeiro/random.git", + "reference": "daf08a189c5d2d40d1a827db46364d3a741a51b7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/antonioribeiro/random/zipball/daf08a189c5d2d40d1a827db46364d3a741a51b7", + "reference": "daf08a189c5d2d40d1a827db46364d3a741a51b7", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "fzaninotto/faker": "~1.7", + "phpunit/phpunit": "~6.4", + "pragmarx/trivia": "~0.1", + "squizlabs/php_codesniffer": "^2.3" + }, + "suggest": { + "fzaninotto/faker": "Allows you to get dozens of randomized types", + "pragmarx/trivia": "For the trivia database" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "PragmaRX\\Random\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Antonio Carlos Ribeiro", + "email": "acr@antoniocarlosribeiro.com", + "homepage": "https://antoniocarlosribeiro.com", + "role": "Developer" + } + ], + "description": "Create random chars, numbers, strings", + "homepage": "https://github.com/antonioribeiro/random", + "keywords": [ + "Randomize", + "faker", + "pragmarx", + "random", + "random number", + "random pattern", + "random string" + ], + "time": "2017-11-21T05:26:22+00:00" + }, + { + "name": "pragmarx/recovery", + "version": "v0.1.0", + "source": { + "type": "git", + "url": "https://github.com/antonioribeiro/recovery.git", + "reference": "e16573a1ae5345cc3b100eec6d0296a1a15a90fe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/antonioribeiro/recovery/zipball/e16573a1ae5345cc3b100eec6d0296a1a15a90fe", + "reference": "e16573a1ae5345cc3b100eec6d0296a1a15a90fe", + "shasum": "" + }, + "require": { + "php": "~7.0", + "pragmarx/random": "~0.1" + }, + "require-dev": { + "phpunit/phpunit": ">=5.4.3", + "squizlabs/php_codesniffer": "^2.3", + "tightenco/collect": "^5" + }, + "suggest": { + "tightenco/collect": "Allows to generate recovery codes as collections" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "PragmaRX\\Recovery\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Antonio Carlos Ribeiro", + "email": "acr@antoniocarlosribeiro.com", + "homepage": "https://antoniocarlosribeiro.com", + "role": "Developer" + } + ], + "description": "Create recovery codes for two factor auth", + "homepage": "https://github.com/antonioribeiro/recovery", + "keywords": [ + "2fa", + "account recovery", + "auth", + "backup codes", + "google2fa", + "pragmarx", + "recovery", + "recovery codes", + "two factor auth" + ], + "time": "2017-09-19T16:58:00+00:00" + }, { "name": "psr/cache", "version": "1.0.1", @@ -2114,9 +2533,159 @@ "templating" ], "time": "2018-09-12T20:54:16+00:00" + }, + { + "name": "symfony/polyfill-php56", + "version": "v1.11.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php56.git", + "reference": "f4dddbc5c3471e1b700a147a20ae17cdb72dbe42" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php56/zipball/f4dddbc5c3471e1b700a147a20ae17cdb72dbe42", + "reference": "f4dddbc5c3471e1b700a147a20ae17cdb72dbe42", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "symfony/polyfill-util": "~1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.11-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php56\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 5.6+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "time": "2019-02-06T07:57:58+00:00" + }, + { + "name": "symfony/polyfill-util", + "version": "v1.11.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-util.git", + "reference": "b46c6cae28a3106735323f00a0c38eccf2328897" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-util/zipball/b46c6cae28a3106735323f00a0c38eccf2328897", + "reference": "b46c6cae28a3106735323f00a0c38eccf2328897", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.11-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Util\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony utilities for portability of PHP codes", + "homepage": "https://symfony.com", + "keywords": [ + "compat", + "compatibility", + "polyfill", + "shim" + ], + "time": "2019-02-08T14:16:39+00:00" } ], "packages-dev": [ + { + "name": "dasprid/enum", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/DASPRiD/Enum.git", + "reference": "631ef6e638e9494b0310837fa531bedd908fc22b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/631ef6e638e9494b0310837fa531bedd908fc22b", + "reference": "631ef6e638e9494b0310837fa531bedd908fc22b", + "shasum": "" + }, + "require-dev": { + "phpunit/phpunit": "^6.4", + "squizlabs/php_codesniffer": "^3.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "DASPRiD\\Enum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Ben Scholzen 'DASPRiD'", + "email": "mail@dasprids.de", + "homepage": "https://dasprids.de/" + } + ], + "description": "PHP 7.1 enum implementation", + "keywords": [ + "enum", + "map" + ], + "time": "2017-10-25T22:45:27+00:00" + }, { "name": "doctrine/instantiator", "version": "1.0.5", @@ -2272,12 +2841,12 @@ "version": "v1.6.5", "source": { "type": "git", - "url": "https://github.com/mikey179/vfsStream.git", + "url": "https://github.com/bovigo/vfsStream.git", "reference": "d5fec95f541d4d71c4823bb5e30cf9b9e5b96145" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mikey179/vfsStream/zipball/d5fec95f541d4d71c4823bb5e30cf9b9e5b96145", + "url": "https://api.github.com/repos/bovigo/vfsStream/zipball/d5fec95f541d4d71c4823bb5e30cf9b9e5b96145", "reference": "d5fec95f541d4d71c4823bb5e30cf9b9e5b96145", "shasum": "" }, @@ -3180,7 +3749,7 @@ } ], "description": "Provides the functionality to compare PHP values for equality", - "homepage": "http://www.github.com/sebastianbergmann/comparator", + "homepage": "https://github.com/sebastianbergmann/comparator", "keywords": [ "comparator", "compare", @@ -3282,7 +3851,7 @@ } ], "description": "Provides functionality to handle HHVM/PHP environments", - "homepage": "http://www.github.com/sebastianbergmann/environment", + "homepage": "https://github.com/sebastianbergmann/environment", "keywords": [ "Xdebug", "environment", @@ -3350,7 +3919,7 @@ } ], "description": "Provides the functionality to export PHP variables for visualization", - "homepage": "http://www.github.com/sebastianbergmann/exporter", + "homepage": "https://github.com/sebastianbergmann/exporter", "keywords": [ "export", "exporter" @@ -3402,7 +3971,7 @@ } ], "description": "Snapshotting of global state", - "homepage": "http://www.github.com/sebastianbergmann/global-state", + "homepage": "https://github.com/sebastianbergmann/global-state", "keywords": [ "global state" ], @@ -3504,7 +4073,7 @@ } ], "description": "Provides functionality to recursively process PHP variables", - "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "homepage": "https://github.com/sebastianbergmann/recursion-context", "time": "2016-11-19T07:33:16+00:00" }, { @@ -3768,7 +4337,7 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": ">=5.6.1", + "php": ">=7.0", "ext-ctype": "*", "ext-curl": "*", "ext-dom": "*", diff --git a/config/dbstructure.config.php b/config/dbstructure.config.php old mode 100644 new mode 100755 index f03132add..57e437837 --- a/config/dbstructure.config.php +++ b/config/dbstructure.config.php @@ -34,10 +34,22 @@ use Friendica\Database\DBA; if (!defined('DB_UPDATE_VERSION')) { - define('DB_UPDATE_VERSION', 1304); + define('DB_UPDATE_VERSION', 1313); } return [ + "2fa_recovery_codes" => [ + "comment" => "Two-factor authentication recovery codes", + "fields" => [ + "uid" => ["type" => "mediumint unsigned", "not null" => "1", "primary" => "1", "relation" => ["user" => "uid"], "comment" => "User ID"], + "code" => ["type" => "varchar(50)", "not null" => "1", "primary" => "1", "comment" => "Recovery code string"], + "generated" => ["type" => "datetime", "not null" => "1", "comment" => "Datetime the code was generated"], + "used" => ["type" => "datetime", "comment" => "Datetime the code was used"], + ], + "indexes" => [ + "PRIMARY" => ["uid", "code"] + ] + ], "addon" => [ "comment" => "registered addons", "fields" => [ @@ -74,6 +86,7 @@ return [ "alias" => ["type" => "varchar(255)", "comment" => ""], "pubkey" => ["type" => "text", "comment" => ""], "baseurl" => ["type" => "varchar(255)", "comment" => "baseurl of the ap contact"], + "generator" => ["type" => "varchar(255)", "comment" => "Name of the contact's system"], "updated" => ["type" => "datetime", "not null" => "1", "default" => DBA::NULL_DATETIME, "comment" => ""] ], "indexes" => [ @@ -180,14 +193,16 @@ return [ "id" => ["type" => "int unsigned", "not null" => "1", "extra" => "auto_increment", "primary" => "1", "comment" => "sequential ID"], "uid" => ["type" => "mediumint unsigned", "not null" => "1", "default" => "0", "relation" => ["user" => "uid"], "comment" => "Owner User id"], "created" => ["type" => "datetime", "not null" => "1", "default" => DBA::NULL_DATETIME, "comment" => ""], + "updated" => ["type" => "datetime", "default" => DBA::NULL_DATETIME, "comment" => "Date of last contact update"], "self" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => "1 if the contact is the user him/her self"], "remote_self" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => ""], "rel" => ["type" => "tinyint unsigned", "not null" => "1", "default" => "0", "comment" => "The kind of the relation between the user and the contact"], "duplex" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => ""], - "network" => ["type" => "char(4)", "not null" => "1", "default" => "", "comment" => "Network protocol of the contact"], + "network" => ["type" => "char(4)", "not null" => "1", "default" => "", "comment" => "Network of the contact"], + "protocol" => ["type" => "char(4)", "not null" => "1", "default" => "", "comment" => "Protocol of the contact"], "name" => ["type" => "varchar(255)", "not null" => "1", "default" => "", "comment" => "Name that this contact is known by"], "nick" => ["type" => "varchar(255)", "not null" => "1", "default" => "", "comment" => "Nick- and user name of the contact"], - "location" => ["type" => "varchar(255)", "not null" => "1", "default" => "", "comment" => ""], + "location" => ["type" => "varchar(255)", "default" => "", "comment" => ""], "about" => ["type" => "text", "comment" => ""], "keywords" => ["type" => "text", "comment" => "public keywords (interests) of the contact"], "gender" => ["type" => "varchar(32)", "not null" => "1", "default" => "", "comment" => ""], @@ -226,7 +241,8 @@ return [ "term-date" => ["type" => "datetime", "not null" => "1", "default" => DBA::NULL_DATETIME, "comment" => ""], "last-item" => ["type" => "datetime", "not null" => "1", "default" => DBA::NULL_DATETIME, "comment" => "date of the last post"], "priority" => ["type" => "tinyint unsigned", "not null" => "1", "default" => "0", "comment" => ""], - "blocked" => ["type" => "boolean", "not null" => "1", "default" => "1", "comment" => ""], + "blocked" => ["type" => "boolean", "not null" => "1", "default" => "1", "comment" => "Node-wide block status"], + "block_reason" => ["type" => "text", "comment" => "Node-wide block reason"], "readonly" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => "posts of the contact are readonly"], "writable" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => ""], "forum" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => "contact is a forum"], @@ -529,6 +545,21 @@ return [ "hook_file_function" => ["UNIQUE", "hook", "file", "function"], ] ], + "inbox-status" => [ + "comment" => "Status of ActivityPub inboxes", + "fields" => [ + "url" => ["type" => "varbinary(255)", "not null" => "1", "primary" => "1", "comment" => "URL of the inbox"], + "created" => ["type" => "datetime", "not null" => "1", "default" => DBA::NULL_DATETIME, "comment" => "Creation date of this entry"], + "success" => ["type" => "datetime", "not null" => "1", "default" => DBA::NULL_DATETIME, "comment" => "Date of the last successful delivery"], + "failure" => ["type" => "datetime", "not null" => "1", "default" => DBA::NULL_DATETIME, "comment" => "Date of the last failed delivery"], + "previous" => ["type" => "datetime", "not null" => "1", "default" => DBA::NULL_DATETIME, "comment" => "Previous delivery date"], + "archive" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => "Is the inbox archived?"], + "shared" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => "Is it a shared inbox?"] + ], + "indexes" => [ + "PRIMARY" => ["url"] + ] + ], "intro" => [ "comment" => "", "fields" => [ @@ -1103,26 +1134,6 @@ return [ "next_try" => ["next_try"], ] ], - "queue" => [ - "comment" => "Queue for messages that couldn't be delivered", - "fields" => [ - "id" => ["type" => "int unsigned", "not null" => "1", "extra" => "auto_increment", "primary" => "1", "comment" => "sequential ID"], - "cid" => ["type" => "int unsigned", "not null" => "1", "default" => "0", "relation" => ["contact" => "id"], "comment" => "Message receiver"], - "network" => ["type" => "char(4)", "not null" => "1", "default" => "", "comment" => "Receiver's network"], - "guid" => ["type" => "varchar(255)", "not null" => "1", "default" => "", "comment" => "Unique GUID of the message"], - "created" => ["type" => "datetime", "not null" => "1", "default" => DBA::NULL_DATETIME, "comment" => "Date, when the message was created"], - "last" => ["type" => "datetime", "not null" => "1", "default" => DBA::NULL_DATETIME, "comment" => "Date of last trial"], - "next" => ["type" => "datetime", "not null" => "1", "default" => DBA::NULL_DATETIME, "comment" => "Next retrial date"], - "retrial" => ["type" => "tinyint", "not null" => "1", "default" => "0", "comment" => "Retrial counter"], - "content" => ["type" => "mediumtext", "comment" => ""], - "batch" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => ""], - ], - "indexes" => [ - "PRIMARY" => ["id"], - "last" => ["last"], - "next" => ["next"], - ] - ], "register" => [ "comment" => "registrations requiring admin approval", "fields" => [ @@ -1195,6 +1206,7 @@ return [ ], "indexes" => [ "PRIMARY" => ["tid"], + "term_type" => ["term(64)", "type"], "oid_otype_type_term" => ["oid", "otype", "type", "term(32)"], "uid_otype_type_term_global_created" => ["uid", "otype", "type", "term(32)", "global", "created"], "uid_otype_type_url" => ["uid", "otype", "type", "url(64)"], diff --git a/config/defaults.config.php b/config/defaults.config.php index a6f90f319..b4471af25 100644 --- a/config/defaults.config.php +++ b/config/defaults.config.php @@ -72,6 +72,10 @@ return [ // Deny public access to the local user directory. 'block_local_dir' => false, + // cron_interval (Integer) + // Minimal period in minutes between two calls of the "Cron" worker job. + 'cron_interval' => 5, + // cache_driver (database|memcache|memcached|redis) // Whether to use Memcache or Memcached or Redis to store temporary cache. 'cache_driver' => 'database', @@ -214,6 +218,10 @@ return [ // If activated, all hashtags will point to the local server. 'local_tags' => false, + // logger_config (String) + // Sets the logging adapter of Friendica globally (monolog, syslog, stream) + 'logger_config' => 'stream', + // max_batch_queue (Integer) // Maximum number of batched queue items for a single contact before subsequent messages are discarded. 'max_batch_queue' => 1000, @@ -335,6 +343,14 @@ return [ // Port number of the redis daemon. 'redis_port' => 6379, + // redis_db (Integer) + // The sub-database of redis (0 - 15 possible sub-databases) + 'redis_db' => 0, + + // redis_password (String) + // The authentication password for the redis database + 'redis_password' => null, + // session_handler (database|cache|native) // Whether to use Cache to store session data or to use PHP native session storage. 'session_handler' => 'database', @@ -373,11 +389,6 @@ return [ // Maximum number of posts that a user can send per month with the API. 0 to disable monthly throttling. 'throttle_limit_month' => 0, - // urlpath (String) - // If you are using a subdirectory of your domain you will need to put the relative path (from the root of your domain) here. - // For instance if your URL is 'http://example.com/directory/subdirectory', set urlpath to 'directory/subdirectory'. - 'urlpath' => '', - // username_min_length (Integer) // The minimum character length a username can be. // This length is check once the username has been trimmed and multiple spaces have been collapsed into one. diff --git a/config/settings.config.php b/config/settings.config.php index 31e8dbe5b..bf8b62f15 100644 --- a/config/settings.config.php +++ b/config/settings.config.php @@ -74,7 +74,7 @@ return [ // logfile (String) // The logfile for storing logs. // Can be a full path or a relative path to the Friendica home directory - 'logfile' => 'friendica.log', + 'logfile' => 'log/friendica.log', // loglevel (String) // The loglevel for all logs. diff --git a/database.sql b/database.sql index 10a428fff..dde37cd37 100644 --- a/database.sql +++ b/database.sql @@ -1,6 +1,6 @@ -- ------------------------------------------ --- Friendica 2019.03-dev (The Tazmans Flax-lily) --- DB_UPDATE_VERSION 1300 +-- Friendica 2019.06-dev (Dalmatian Bellflower) +-- DB_UPDATE_VERSION 1311 -- ------------------------------------------ @@ -40,6 +40,7 @@ CREATE TABLE IF NOT EXISTS `apcontact` ( `alias` varchar(255) COMMENT '', `pubkey` text COMMENT '', `baseurl` varchar(255) COMMENT 'baseurl of the ap contact', + `generator` varchar(255) COMMENT 'Name of the contact\'s system', `updated` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT '', PRIMARY KEY(`url`), INDEX `addr` (`addr`(32)), @@ -138,14 +139,16 @@ CREATE TABLE IF NOT EXISTS `contact` ( `id` int unsigned NOT NULL auto_increment COMMENT 'sequential ID', `uid` mediumint unsigned NOT NULL DEFAULT 0 COMMENT 'Owner User id', `created` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT '', + `updated` datetime DEFAULT '0001-01-01 00:00:00' COMMENT 'Date of last contact update', `self` boolean NOT NULL DEFAULT '0' COMMENT '1 if the contact is the user him/her self', `remote_self` boolean NOT NULL DEFAULT '0' COMMENT '', `rel` tinyint unsigned NOT NULL DEFAULT 0 COMMENT 'The kind of the relation between the user and the contact', `duplex` boolean NOT NULL DEFAULT '0' COMMENT '', - `network` char(4) NOT NULL DEFAULT '' COMMENT 'Network protocol of the contact', + `network` char(4) NOT NULL DEFAULT '' COMMENT 'Network of the contact', + `protocol` char(4) NOT NULL DEFAULT '' COMMENT 'Protocol of the contact', `name` varchar(255) NOT NULL DEFAULT '' COMMENT 'Name that this contact is known by', `nick` varchar(255) NOT NULL DEFAULT '' COMMENT 'Nick- and user name of the contact', - `location` varchar(255) NOT NULL DEFAULT '' COMMENT '', + `location` varchar(255) DEFAULT '' COMMENT '', `about` text COMMENT '', `keywords` text COMMENT 'public keywords (interests) of the contact', `gender` varchar(32) NOT NULL DEFAULT '' COMMENT '', @@ -470,6 +473,20 @@ CREATE TABLE IF NOT EXISTS `hook` ( UNIQUE INDEX `hook_file_function` (`hook`,`file`,`function`) ) DEFAULT COLLATE utf8mb4_general_ci COMMENT='addon hook registry'; +-- +-- TABLE inbox-status +-- +CREATE TABLE IF NOT EXISTS `inbox-status` ( + `url` varbinary(255) NOT NULL COMMENT 'URL of the inbox', + `created` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT 'Creation date of this entry', + `success` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT 'Date of the last successful delivery', + `failure` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT 'Date of the last failed delivery', + `previous` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT 'Previous delivery date', + `archive` boolean NOT NULL DEFAULT '0' COMMENT 'Is the inbox archived?', + `shared` boolean NOT NULL DEFAULT '0' COMMENT 'Is it a shared inbox?', + PRIMARY KEY(`url`) +) DEFAULT COLLATE utf8mb4_general_ci COMMENT='Status of ActivityPub inboxes'; + -- -- TABLE intro -- @@ -879,7 +896,7 @@ CREATE TABLE IF NOT EXISTS `photo` ( `deny_gid` mediumtext COMMENT 'Access Control - list of denied groups', `backend-class` tinytext COMMENT 'Storage backend class', `backend-ref` text COMMENT 'Storage backend data reference', - `updated` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT 'edited timestamp', + `updated` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT '', PRIMARY KEY(`id`), INDEX `contactid` (`contact-id`), INDEX `uid_contactid` (`uid`,`contact-id`), @@ -1013,25 +1030,6 @@ CREATE TABLE IF NOT EXISTS `push_subscriber` ( INDEX `next_try` (`next_try`) ) DEFAULT COLLATE utf8mb4_general_ci COMMENT='Used for OStatus: Contains feed subscribers'; --- --- TABLE queue --- -CREATE TABLE IF NOT EXISTS `queue` ( - `id` int unsigned NOT NULL auto_increment COMMENT 'sequential ID', - `cid` int unsigned NOT NULL DEFAULT 0 COMMENT 'Message receiver', - `network` char(4) NOT NULL DEFAULT '' COMMENT 'Receiver\'s network', - `guid` varchar(255) NOT NULL DEFAULT '' COMMENT 'Unique GUID of the message', - `created` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT 'Date, when the message was created', - `last` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT 'Date of last trial', - `next` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT 'Next retrial date', - `retrial` tinyint NOT NULL DEFAULT 0 COMMENT 'Retrial counter', - `content` mediumtext COMMENT '', - `batch` boolean NOT NULL DEFAULT '0' COMMENT '', - PRIMARY KEY(`id`), - INDEX `last` (`last`), - INDEX `next` (`next`) -) DEFAULT COLLATE utf8mb4_general_ci COMMENT='Queue for messages that couldn\'t be delivered'; - -- -- TABLE register -- @@ -1099,6 +1097,7 @@ CREATE TABLE IF NOT EXISTS `term` ( `global` boolean NOT NULL DEFAULT '0' COMMENT '', `uid` mediumint unsigned NOT NULL DEFAULT 0 COMMENT 'User id', PRIMARY KEY(`tid`), + INDEX `term_type` (`term`(64),`type`), INDEX `oid_otype_type_term` (`oid`,`otype`,`type`,`term`(32)), INDEX `uid_otype_type_term_global_created` (`uid`,`otype`,`type`,`term`(32),`global`,`created`), INDEX `uid_otype_type_url` (`uid`,`otype`,`type`,`url`(64)), @@ -1270,13 +1269,12 @@ CREATE TABLE IF NOT EXISTS `workerqueue` ( `retrial` tinyint NOT NULL DEFAULT 0 COMMENT 'Retrial counter', `done` boolean NOT NULL DEFAULT '0' COMMENT 'Marked 1 when the task was done - will be deleted later', PRIMARY KEY(`id`), - INDEX `pid` (`pid`), - INDEX `parameter` (`parameter`(64)), - INDEX `priority_created_next_try` (`priority`,`created`,`next_try`), - INDEX `done_priority_executed_next_try` (`done`,`priority`,`executed`,`next_try`), - INDEX `done_executed_next_try` (`done`,`executed`,`next_try`), + INDEX `done_parameter` (`done`,`parameter`(64)), + INDEX `done_executed` (`done`,`executed`), + INDEX `done_priority_created` (`done`,`priority`,`created`), INDEX `done_priority_next_try` (`done`,`priority`,`next_try`), - INDEX `done_next_try` (`done`,`next_try`) + INDEX `done_pid_next_try` (`done`,`pid`,`next_try`), + INDEX `done_pid_priority_created` (`done`,`pid`,`priority`,`created`) ) DEFAULT COLLATE utf8mb4_general_ci COMMENT='Background tasks queue entries'; -- diff --git a/doc/Addons.md b/doc/Addons.md index 29cf22bfe..47d16085a 100644 --- a/doc/Addons.md +++ b/doc/Addons.md @@ -7,48 +7,65 @@ Please see the sample addon 'randplace' for a working example of using some of t Addons work by intercepting event hooks - which must be registered. Modules work by intercepting specific page requests (by URL path). -Addon names cannot contain spaces or other punctuation and are used as filenames and function names. -You may supply a "friendly" name within the comment block. -Each addon must contain both an install and an uninstall function based on the addon name. -For instance "addon1name_install()". -These two functions take no arguments and are usually responsible for registering (and unregistering) event hooks that your addon will require. -The install and uninstall functions will also be called (i.e. re-installed) if the addon changes after installation. +## Naming + +Addon names are used in file paths and functions names, and as such: +- Can't contain spaces or punctuation. +- Can't start with a number. + +## Metadata + +You can provide human-readable information about your addon in the first multi-line comment of your addon file. + +Here's the structure: + +```php +/** + * Name: {Human-readable name} + * Description: {Short description} + * Version: 1.0 + * Author: {Author1 Name} + * Author: {Author2 Name} <{Author profile link}> + * Maintainer: {Maintainer1 Name} + * Maintainer: {Maintainer2 Name} <{Maintainer profile link}> + * Status: {Unsupported|Arbitrary status} + */ +``` + +You can also provide a longer documentation in a `README` or `README.md` file. +The latter will be converted from Markdown to HTML in the addon detail page. + +## Install/Uninstall + +If your addon uses hooks, they have to be registered in a `_install()` function. +This function also allows to perform arbitrary actions your addon needs to function properly. + +Uninstalling an addon automatically unregisters any hook it registered, but if you need to provide specific uninstallation steps, you can add them in a `_uninstall()` function. + +The install and uninstall functions will be called (i.e. re-installed) if the addon changes after installation. Therefore your uninstall should not destroy data and install should consider that data may already exist. Future extensions may provide for "setup" amd "remove". -Addons should contain a comment block with the four following parameters: - - /* - * Name: My Great Addon - * Description: This is what my addon does. It's really cool. - * Version: 1.0 - * Author: John Q. Public - */ - -Please also add a README or README.md file to the addon directory. -It will be displayed in the admin panel and should include some further information in addition to the header information. - ## PHP addon hooks Register your addon hooks during installation. \Friendica\Core\Hook::register($hookname, $file, $function); -$hookname is a string and corresponds to a known Friendica PHP hook. +`$hookname` is a string and corresponds to a known Friendica PHP hook. -$file is a pathname relative to the top-level Friendica directory. -This *should* be 'addon/*addon_name*/*addon_name*.php' in most cases. +`$file` is a pathname relative to the top-level Friendica directory. +This *should* be 'addon/*addon_name*/*addon_name*.php' in most cases and can be shortened to `__FILE__`. -$function is a string and is the name of the function which will be executed when the hook is called. +`$function` is a string and is the name of the function which will be executed when the hook is called. ### Arguments Your hook callback functions will be called with at least one and possibly two arguments - function myhook_function(App $a, &$b) { + function _(App $a, &$b) { } - If you wish to make changes to the calling data, you must declare them as reference variables (with `&`) during function declaration. #### $a @@ -67,6 +84,12 @@ $b can be called anything you like. This is information specific to the hook currently being processed, and generally contains information that is being immediately processed or acted on that you can use, display, or alter. Remember to declare it with `&` if you wish to alter it. +## Admin settings + +Your addon can provide user-specific settings via the `addon_settings` PHP hook, but it can also provide node-wide settings in the administration page of your addon. + +Simply declare a `_addon_admin(App $a)` function to display the form and a `_addon_admin_post(App $a)` function to process the data from the form. + ## Global stylesheets If your addon requires adding a stylesheet on all pages of Friendica, add the following hook: @@ -132,11 +155,11 @@ No additional data is provided. ## Modules Addons may also act as "modules" and intercept all page requests for a given URL path. -In order for a addon to act as a module it needs to define a function "addon_name_module()" which takes no arguments and needs not do anything. +In order for a addon to act as a module it needs to declare an empty function `_module()`. -If this function exists, you will now receive all page requests for "http://my.web.site/addon_name" - with any number of URL components as additional arguments. +If this function exists, you will now receive all page requests for `https://my.web.site/` - with any number of URL components as additional arguments. These are parsed into an array $a->argv, with a corresponding $a->argc indicating the number of URL components. -So http://my.web.site/addon/arg1/arg2 would look for a module named "addon" and pass its module functions the $a App structure (which is available to many components). +So `https://my.web.site/addon/arg1/arg2` would look for a module named "addon" and pass its module functions the $a App structure (which is available to many components). This will include: ```php @@ -144,9 +167,9 @@ $a->argc = 3 $a->argv = array(0 => 'addon', 1 => 'arg1', 2 => 'arg2'); ``` -Your module functions will often contain the function addon_name_content(App $a), which defines and returns the page body content. -They may also contain addon_name_post(App $a) which is called before the _content function and typically handles the results of POST forms. -You may also have addon_name_init(App $a) which is called very early on and often does module initialisation. +To display a module page, you need to declare the function `_content(App $a)`, which defines and returns the page body content. +They may also contain `_post(App $a)` which is called before the `_content` function and typically handles the results of POST forms. +You may also have `_init(App $a)` which is called before `_content` and should include common logic to your module. ## Templates @@ -160,7 +183,7 @@ In your code, like in the function addon_name_content(), load the template file ```php # load template file. first argument is the template name, # second is the addon path relative to friendica top folder -$tpl = Renderer::getMarkupTemplate('mytemplate.tpl', 'addon/addon_name/'); +$tpl = Renderer::getMarkupTemplate('mytemplate.tpl', __DIR__); # apply template. first argument is the loaded template, # second an array of 'name' => 'values' to pass to template @@ -335,6 +358,7 @@ Called from `Emailer::send()` before building the mime message. - **htmlVersion**: html version of the message - **textVersion**: text only version of the message - **additionalMailHeader**: additions to the smtp mail header +- **sent**: default false, if set to true in the hook, the default mailer will be skipped. ### emailer_send Called before calling PHP's `mail()`. @@ -344,6 +368,7 @@ Called before calling PHP's `mail()`. - **subject** - **body** - **headers** +- **sent**: default false, if set to true in the hook, the default mailer will be skipped. ### load_config Called during `App` initialization to allow addons to load their own configuration file(s) with `App::loadConfigFile()`. @@ -411,6 +436,34 @@ Hook data: visitor => array with the contact record of the visitor url => the query string +### jot_networks +Called when displaying the post permission screen. +Hook data is a list of form fields that need to be displayed along the ACL. +Form field array structure is: + +- **type**: `checkbox` or `select`. +- **field**: Standard field data structure to be used by `field_checkbox.tpl` and `field_select.tpl`. + +For `checkbox`, **field** is: + - [0] (String): Form field name; Mandatory. + - [1]: (String): Form field label; Optional, default is none. + - [2]: (Boolean): Whether the checkbox should be checked by default; Optional, default is false. + - [3]: (String): Additional help text; Optional, default is none. + - [4]: (String): Additional HTML attributes; Optional, default is none. + +For `select`, **field** is: + - [0] (String): Form field name; Mandatory. + - [1] (String): Form field label; Optional, default is none. + - [2] (Boolean): Default value to be selected by default; Optional, default is none. + - [3] (String): Additional help text; Optional, default is none. + - [4] (Array): Associative array of options. Item key is option value, item value is option label; Mandatory. + +### route_collection +Called just before dispatching the router. +Hook data is a `\FastRoute\RouterCollector` object that should be used to add addon routes pointing to classes. + +**Notice**: The class whose name is provided in the route handler must be reachable via auto-loader. + ## Complete list of hook callbacks Here is a complete list of all hook callbacks with file locations (as of 24-Sep-2018). Please see the source for details of any hooks not documented above. @@ -586,6 +639,7 @@ Here is a complete list of all hook callbacks with file locations (as of 24-Sep- Hook::callAll('load_config'); Hook::callAll('head'); Hook::callAll('footer'); + Hook::callAll('route_collection'); ### src/Model/Item.php @@ -661,11 +715,6 @@ Here is a complete list of all hook callbacks with file locations (as of 24-Sep- Hook::callAll('notifier_end', $target_item); -### src/Worker/Queue.php - - Hook::callAll('queue_predeliver', $r); - Hook::callAll('queue_deliver', $params); - ### src/Module/Login.php Hook::callAll('authenticate', $addon_auth); diff --git a/doc/FAQ.md b/doc/FAQ.md index 140718178..83bdce3a2 100644 --- a/doc/FAQ.md +++ b/doc/FAQ.md @@ -126,16 +126,15 @@ A **hidden contact** will not be displayed in any "friend list" (except to you). However a hidden contact will appear normally in conversations and this may expose his/her hidden status to anybody who can see the conversation. -### What happens when an account is removed? Is it truly deleted? +### What happens when an account is removed? -If you delete your account, we will immediately remove all your content on **your** server. +If you remove your account, it will be scheduled for permanent deletion in *seven days*. +As soon as you activate the deletion process you won't be able to login any more. +Only the administrator of your node can halt this process prior to permanent deletion. -Then Friendica issues requests to all your contacts to remove you. -This will also remove you from the global directory. -Doing this requires your account and profile still to be "partially" available for up to 24 hours in order to establish contact with all your friends. -We can block it in several ways so that it appears empty and all profile information erased, but will then wait for 24 hours (or after all of your contacts have been notified) before we can physically remove it. - -After that, your account is deleted. +After the elapsed time of seven days, all your posts, messages, photos, and personal information stored on your node will be deleted. +Your node will also issue removal requests to all your contacts; this will also remove your profile from the global directory if you are listed. +Your username cannot be reissued for future sign-ups for security reasons. ### Can I follow a hashtag? diff --git a/doc/Install.md b/doc/Install.md index 005db5b7f..5e9448f76 100644 --- a/doc/Install.md +++ b/doc/Install.md @@ -25,16 +25,21 @@ Requirements --- * Apache with mod-rewrite enabled and "Options All" so you can use a local .htaccess file -* PHP 5.6.1+ (PHP 7 is recommended for performance) +* PHP 7+ (PHP 7.1+ is recommended for performance and official support) * PHP *command line* access with register_argc_argv set to true in the php.ini file * Curl, GD, PDO, MySQLi, hash, xml, zip and OpenSSL extensions * The POSIX module of PHP needs to be activated (e.g. [RHEL, CentOS](http://www.bigsoft.co.uk/blog/index.php/2014/12/08/posix-php-commands-not-working-under-centos-7) have disabled it) * some form of email server or email gateway such that PHP mail() works -* Mysql 5.5.3+ or an equivalent alternative for MySQL (MariaDB, Percona Server etc.) +* Mysql 5.6+ or an equivalent alternative for MySQL (MariaDB, Percona Server etc.) * the ability to schedule jobs with cron (Linux/Mac) or Scheduled Tasks (Windows) (Note: other options are presented in Section 7 of this document.) * Installation into a top-level domain or sub-domain (without a directory/path component in the URL) is preferred. Directory paths will not be as convenient to use and have not been thoroughly tested. * If your hosting provider doesn't allow Unix shell access, you might have trouble getting everything to work. +Optional +--- + +* PHP ImageMagick extension (php-imagick) for animated GIF support. + Installation procedure --- diff --git a/doc/Quick-Start-guide.md b/doc/Quick-Start-guide.md index 5c88ffaa0..2725fe4f8 100644 --- a/doc/Quick-Start-guide.md +++ b/doc/Quick-Start-guide.md @@ -3,17 +3,19 @@ If you're not already logged in, do so in the frame below. Once you've logged in (or if you are already logged in), you'll now be looking at your profile page. -This is a bit like your Facebook wall. +This is a bit like a Facebook wall. It's where all your status messgages are kept, and where your friends come to post on your wall. -To write your status, simply click in the box that says "share". -When you do this, the box will expand. -You can see some formatting options at the top such as Bold, Italics and Underline, as well as ways to add links and pictures. -At the bottom you'll find some more links. + +To write your status, simply click on the Pencil & Paper icon in the top right (in the Frio theme), or click in the box that says "share" (other themes). +When you do this, the posting dialog box will appear or the share box will expand. + +You can see some formatting options such as Bold, Italics and Underline, as well as ways to add links, pictures (dependent on the theme), and a paperclip icon to attach or embed content. You can use these to upload pictures and files from your computer, share websites with a bit of preview text, or embed video and audio files from elsewhere on the web. +With the Frio theme, the browser tab can be used to upload and post media from your account. You can also set your post location here. -Once you've finished writing your post, click on the padlock icon to select who can see it. -If you do not use the padlock icon, your post will be public. +Once you've finished writing your post, click on the padlock icon or permissions tab to select who can see it. +If you do not change anything, your post will be public. This means it will appear to anybody who views your profile, and in the community tab if your site has it enabled, as well as in the network tab of any of your contacts. Play around with this a bit, then when you're ready to move on, we'll take a look at the Network Tab diff --git a/doc/Settings.md b/doc/Settings.md index ed25e580a..f2af9617c 100644 --- a/doc/Settings.md +++ b/doc/Settings.md @@ -169,7 +169,6 @@ Your local users will always have access to both pages. Comma separated list of domains which are allowed to establish friendships with this site. Wildcards are accepted. -(Wildcard support on Windows platforms requires PHP5.3). By default, any (valid) domain may establish friendships with this site. This is useful if you want to setup a closed network for educational groups, cooperatives and similar communities that don't want to communicate with the rest of the network. @@ -179,7 +178,6 @@ This is useful if you want to setup a closed network for educational groups, coo Comma separated list of domains which are allowed in email addresses for registrations to this site. This can lockout those who are not part of this organisation from registering here. Wildcards are accepted. -(Wildcard support on Windows platforms requires PHP5.3). By default, any (valid) email address is allowed in registrations. #### Allow Users to set remote_self diff --git a/doc/Text_editor.md b/doc/Text_editor.md index 3e38ca5f1..07e1fa929 100644 --- a/doc/Text_editor.md +++ b/doc/Text_editor.md @@ -11,71 +11,85 @@ Creating posts Here you can find an overview of the different ways to create and edit your post. -One click on "Share" text box on top of your Home or Network page, and the post editor shows up: +One click on the Pencil & Paper icon in the top right of your Home or Network page, or the "Share" text box, and the post editor shows up. +Below are examples of the post editor in 3 of Friendica's common themes:
-default editor -
Default post editor, with default Friendica theme (duepuntozero)
+frio editor +
Post editor, with the Frio (popular default) theme.
+
+

+
+vier editor +
Post editor, with the Vier theme.
+
+

+
+duepuntozero editor +
Post editor, with the Duepuntozero theme.
-Post title is optional, you can set it clicking on "Set title". +Post title is optional, you can set it by clicking on "Set title". -Posts can optionally be in one or more categories. Write categories name separated by a comma to file your new post. +Posts can optionally be in one or more categories. Write category names separated by a comma to file your new post. The Big Empty Textarea is where you write your new post. -You can simply enter your text there and click "Share" button, and your new post will be public on your profile page and shared to your contact. +You can simply enter your text there and click the "Share" button, and your new post will be public on your profile page and shared to your contact. If plain text is not so exciting to you, Friendica understands BBCode to spice up your posts: bold, italic, images, links, lists.. See [BBCode tags reference](help/BBCode) page to see all what you can do. -The icons under the text area are there to help you to write posts quickly: +The icons under the text area are there to help you to write posts quickly, but vary depending on the theme: -editor Upload a picture from your computer. The image will be uploaded and correct bbcode tag will be added to your post.* +With the Frio theme, the Underline, Italics and Bold buttons should be self-explanatory. + +editor Upload a picture from your computer. The image will be uploaded and correct bbcode tag will be added to your post.* In the Frio theme, use the Browser tab instead to Upload and/or attach content to your post.

-paper_clip Add files from your computer. Same as picture, but for generic attachment to the post.* +paper_clip This depends on the theme: For Frio, this is to attach remote content - put in a URL to embed in your post, including video or audio content. For other themes: Add files from your computer. Same as picture, but for generic attachment to the post.*

-chain Add a web address (url). Enter an url and Friendica will add to your post a link to the url and an excerpt from the web site, if possible. +chain Add a web address (url). Enter a URL and Friendica will add to your post a link to the url and an excerpt from the web site, if possible.

-video Add a video. Enter the url to a video (ogg) or to a video page on youtube or vimeo, and it will be embedded in your post with a preview. Friendica is using [HTML5](http://en.wikipedia.org/wiki/HTML5_video) for embedding content. Therefore, the supported files are depending on your browser and operating system (OS). Some filetypes are WebM, MP4 and OGG.* +video Add a video. Enter the url to a video (ogg) or to a video page on youtube or vimeo, and it will be embedded in your post with a preview. (In the Frio theme, this is done with the paperclip as mentioned above.) Friendica is using [HTML5](http://en.wikipedia.org/wiki/HTML5_video) for embedding content. Therefore, the supported files are depending on your browser and operating system (OS). Some filetypes are WebM, MP4 and OGG.*

-mic Add an audio. Same as video, but for audio. Depending on your browser and operation system MP3, OGG and AAC are supported. Additionally, you are able to add URLs from audiohosters like Soundcloud. +mic Add an audio. Same as video, but for audio. Depending on your browser and operation system MP3, OGG and AAC are supported. Additionally, you are able to add URLs from audiohosters like Soundcloud.

-globe Set your geographic location. This location will be added into a Google Maps search. That's why a note like "New York" or "10004" is already enough. +globe Or location Set your geographic location. This location will be added into a Google Maps search. That's why a note like "New York" or "10004" is already enough. +

+
+

-* how to [upload](help/FAQ#upload) files - -Those icons can change with themes. Some examples: +These icons can change depending on the theme. Some examples: - + + + + + + - + - - - - -
Darkbubble: Vier: vier.png 
Smoothly: darkbubble.png(inkl. smoothly, testbubble) 
Frost: frost.png  
Vier: vier.png(inkl. dispy)
-

 

+* how to [upload](help/FAQ#upload) files

 

-**lock icon The lock** +**lock icon The Lock / Permissions** -The last button, the Lock, is the most important feature in Friendica. If the lock is open, your post will be public, and will shows up on your profile page when strangers visit it. +In Frio, the Permissions tab, or in other themes, the Lock button, is the most important feature in Friendica. If the lock is open, your post will be public, and will show up on your profile page when strangers visit it. Click on it and the *Permission settings* window (aka "*Access Control Selector*" or "*ACL Selector*") pops up. There you can select who can see the post. @@ -95,4 +109,4 @@ Click again on "show" or "don't show" to switch it off. You can search for contacts or groups with the search box. -See also [Group and Privacy](help/Groups-and-Privacy) \ No newline at end of file +See also [Group and Privacy](help/Groups-and-Privacy) diff --git a/doc/Two-Factor-Authentication.md b/doc/Two-Factor-Authentication.md new file mode 100644 index 000000000..32aa7308a --- /dev/null +++ b/doc/Two-Factor-Authentication.md @@ -0,0 +1,60 @@ +# Configuring two-factor authentication + +* [Home](help) + +You can configure two-factor authentication using a mobile app. +A time-based one-time password (TOTP) application automatically generates an authentication code that changes after a certain period of time. + +**Tip**: To configure authentication via TOTP on multiple devices, during setup, scan the QR code using each device at the same time. +If 2FA is already enabled and you want to add another device, you must re-configure 2FA from your security settings. + +## Enabling two-factor authentication + +### 1. Download an authenticator app + +Any authenticator app should work with Friendica. +Notheless, we recommend: + + - For iOS, [Matt Rubin's MIT-licensed Authenticator app](https://mattrubin.me/authenticator). + - For Android, [andOTP](https://github.com/andOTP/andOTP). + +### 2. Record your one-use recovery codes + +From your [two-factor authentication user settings](/settings/2fa), enter your password and click on "Enable two-factor authentication". + +You will be presented with a list of one-use recovery codes. +Please save those in the same place you are saving your Friendica password (ideally, in a password manager like [KeePass](https://keepass.info)). + +When you're done, click on "Next". + +### 3. Setup your authenticator app + +You have three methods to setup your authenticator app: + +1. Scan the QR Code with your device camera. + This will automatically configure your account on the app. +2. Click/tap on the provided **totp://** URl. + Ideally your authenticator app should be called with this URL and set up your account. +3. Enter your account settings manually. + Friendica is using default settings for token type, code digit count and hashing algorithm but you may be required to enter them in your app. + +**Tip**: If you have multiple devices, configure them all at this point. + +Then verify your app is correctly configured by submitting a code provided by your app. +This will conclude two-factor authentication configuration. + +**Note:** If you leave this screen at any point without having submitted a verification code, two-factor authentication won't be enabled on your account. +To complete the configuration, just come back to your [two-factor authentication user settings](/settings/2fa) and click on "Finish configuration" after entering your current password. + +## Disabling two-factor authentication + +You can disable two-factor authentication at any time by going to your [two-factor authentication user settings](/settings/2fa) and click on "Disable two-factor authentication" after entering your current password. + +You should remove your Friendica account from your authenticator app as it won't work again even if you reenable two-factor authentication. +In this case you will have to configure your authenticator app again using the process above. + +## Managing your one-time recovery codes + +When two-factor authentication is enabled, you can show your recovery codes, including the ones you've already used. + +You can freely regenerate a new set of fresh recovery codes, just be sure to replace the previous ones where you saved them as they won't be active anymore. diff --git a/doc/de/Addons.md b/doc/de/Addons.md index d85d4d07b..35ce0e28b 100644 --- a/doc/de/Addons.md +++ b/doc/de/Addons.md @@ -434,11 +434,6 @@ Eine komplette Liste aller Hook-Callbacks mit den zugehörigen Dateien (am 01-Ap Hook::callAll('notifier_end', $target_item); -### src/Worker/Queue.php - - Hook::callAll('queue_predeliver', $r); - Hook::callAll('queue_deliver', $params); - ### src/Module/Login.php Hook::callAll('authenticate', $addon_auth); diff --git a/doc/de/Install.md b/doc/de/Install.md index 209f78c60..b8a827ab1 100644 --- a/doc/de/Install.md +++ b/doc/de/Install.md @@ -28,12 +28,12 @@ Requirements --- * Apache mit einer aktiverten mod-rewrite-Funktion und dem Eintrag "Options All", so dass du die lokale .htaccess-Datei nutzen kannst -* PHP 5.6.1+ (PHP 7 ist aufgrund der Performance empfohlen) +* PHP 7+ (PHP 7.1+ wird für Performance und offiziellen Support empfohlen) * PHP *Kommandozeilen*-Zugang mit register_argc_argv auf "true" gesetzt in der php.ini-Datei * Curl, GD, PDO, MySQLi, xml, zip und OpenSSL-Erweiterung * Das POSIX Modul muss aktiviert sein ([CentOS, RHEL](http://www.bigsoft.co.uk/blog/index.php/2014/12/08/posix-php-commands-not-working-under-centos-7http://www.bigsoft.co.uk/blog/index.php/2014/12/08/posix-php-commands-not-working-under-centos-7) haben dies z.B. deaktiviert) * etwas in der Art eines Email-Servers oder eines Gateways wie PHP mail() -* Mysql 5.5.3+ (oder eine äquivalente Alternative: MariaDB, Percona Server etc.) +* Mysql 5.6+ (oder eine äquivalente Alternative: MariaDB, Percona Server etc.) * die Möglichkeit, wiederkehrende Aufgaben mit cron (Linux/Mac) oder "Scheduled Tasks" einzustellen (Windows) [Beachte: andere Optionen sind in Abschnitt 7 dieser Dokumentation zu finden] * Installation in einer Top-Level-Domain oder Subdomain (ohne eine Verzeichnis/Pfad-Komponente in der URL) wird bevorzugt. Verzeichnispfade sind für diesen Zweck nicht so günstig und wurden auch nicht ausführlich getestet. diff --git a/doc/de/Settings.md b/doc/de/Settings.md index 1234a3d15..abb381757 100644 --- a/doc/de/Settings.md +++ b/doc/de/Settings.md @@ -160,7 +160,7 @@ Angemeldete Nutzer des Knotens können grundsätzlich beide Seiten verwenden. #### Erlaubte Domains für Kontakte Kommagetrennte Liste von Domains, welche eine Freundschaft mit dieser Seite eingehen dürfen. -Wildcards werden akzeptiert (Wildcard-Unterstützung unter Windows benötigt PHP5.3) Standardmäßig sind alle gültigen Domains erlaubt. +Wildcards werden akzeptiert Standardmäßig sind alle gültigen Domains erlaubt. Mit dieser Option kann man einfach geschlossene Netzwerke, z.B. im schulischen Bereich aufbauen, aus denen nicht mit dem Rest des Netzwerks kommuniziert werden soll. @@ -168,7 +168,7 @@ Mit dieser Option kann man einfach geschlossene Netzwerke, z.B. im schulischen B Kommagetrennte Liste von Domains, welche bei der Registrierung als Part der Email-Adresse erlaubt sind. Das grenzt Leute aus, die nicht Teil der Gruppe oder Organisation sind. -Wildcards werden akzeptiert (Wildcard-Unterstützung unter Windows benötigt PHP5.3) Standardmäßig sind alle gültigen Email-Adressen erlaubt. +Wildcards werden akzeptiert Standardmäßig sind alle gültigen Email-Adressen erlaubt. #### Nutzern erlauben das remote_self Flag zu setzen diff --git a/doc/img/friendica_editor.png b/doc/img/editor_dpzero.png similarity index 100% rename from doc/img/friendica_editor.png rename to doc/img/editor_dpzero.png diff --git a/doc/img/editor_frio.png b/doc/img/editor_frio.png index cb5ad0bcc..d37081b83 100644 Binary files a/doc/img/editor_frio.png and b/doc/img/editor_frio.png differ diff --git a/doc/img/editor_vier.png b/doc/img/editor_vier.png index 217f67ac6..5278b0ff1 100644 Binary files a/doc/img/editor_vier.png and b/doc/img/editor_vier.png differ diff --git a/doc/img/frio_location.png b/doc/img/frio_location.png new file mode 100644 index 000000000..8850c8008 Binary files /dev/null and b/doc/img/frio_location.png differ diff --git a/doc/img/vier_icons.png b/doc/img/vier_icons.png new file mode 100644 index 000000000..4dc8ae03f Binary files /dev/null and b/doc/img/vier_icons.png differ diff --git a/images/friendica-404_svg_flexy-o-hare.png b/images/friendica-404_svg_flexy-o-hare.png new file mode 100644 index 000000000..36d6b5ca3 Binary files /dev/null and b/images/friendica-404_svg_flexy-o-hare.png differ diff --git a/images/friendica-404_svg_hare-bottom-light-inside.png b/images/friendica-404_svg_hare-bottom-light-inside.png new file mode 100644 index 000000000..6c9189e4e Binary files /dev/null and b/images/friendica-404_svg_hare-bottom-light-inside.png differ diff --git a/include/api.php b/include/api.php index b1ee607ab..0fab1f47c 100644 --- a/include/api.php +++ b/include/api.php @@ -11,7 +11,6 @@ use Friendica\Content\ContactSelector; use Friendica\Content\Feature; use Friendica\Content\Text\BBCode; use Friendica\Content\Text\HTML; -use Friendica\Core\Authentication; use Friendica\Core\Config; use Friendica\Core\Hook; use Friendica\Core\L10n; @@ -19,6 +18,7 @@ use Friendica\Core\Logger; use Friendica\Core\NotificationsManager; use Friendica\Core\PConfig; use Friendica\Core\Protocol; +use Friendica\Core\Session; use Friendica\Core\System; use Friendica\Core\Worker; use Friendica\Database\DBA; @@ -250,7 +250,7 @@ function api_login(App $a) throw new UnauthorizedException("This API requires login"); } - Authentication::setAuthenticatedSessionForUser($record); + Session::setAuthenticatedForUser($a, $record); $_SESSION["allow_api"] = true; @@ -361,10 +361,10 @@ function api_call(App $a) } } - Logger::warning(API_LOG_PREFIX . 'not implemented', ['module' => 'api', 'action' => 'call']); + Logger::warning(API_LOG_PREFIX . 'not implemented', ['module' => 'api', 'action' => 'call', 'query' => $a->query_string]); throw new NotImplementedException(); } catch (HTTPException $e) { - header("HTTP/1.1 {$e->httpcode} {$e->httpdesc}"); + header("HTTP/1.1 {$e->getCode()} {$e->httpdesc}"); return api_error($type, $e); } } @@ -384,7 +384,7 @@ function api_error($type, $e) /// @TODO: https://dev.twitter.com/overview/api/response-codes $error = ["error" => $error, - "code" => $e->httpcode . " " . $e->httpdesc, + "code" => $e->getCode() . " " . $e->httpdesc, "request" => $a->query_string]; $return = api_format_data('status', $type, ['status' => $error]); @@ -611,7 +611,7 @@ function api_get_user(App $a, $contact_id = null) 'name' => $contact["name"], 'screen_name' => (($contact['nick']) ? $contact['nick'] : $contact['name']), 'location' => ($contact["location"] != "") ? $contact["location"] : ContactSelector::networkToName($contact['network'], $contact['url']), - 'description' => $contact["about"], + 'description' => HTML::toPlaintext(BBCode::toPlaintext($contact["about"])), 'profile_image_url' => $contact["micro"], 'profile_image_url_https' => $contact["micro"], 'profile_image_url_profile_size' => $contact["thumb"], @@ -690,7 +690,7 @@ function api_get_user(App $a, $contact_id = null) 'name' => (($uinfo[0]['name']) ? $uinfo[0]['name'] : $uinfo[0]['nick']), 'screen_name' => (($uinfo[0]['nick']) ? $uinfo[0]['nick'] : $uinfo[0]['name']), 'location' => $location, - 'description' => $description, + 'description' => HTML::toPlaintext(BBCode::toPlaintext($description)), 'profile_image_url' => $uinfo[0]['micro'], 'profile_image_url_https' => $uinfo[0]['micro'], 'profile_image_url_profile_size' => $uinfo[0]["thumb"], @@ -932,7 +932,6 @@ function api_format_data($root_element, $type, $data) */ function api_account_verify_credentials($type) { - $a = \get_app(); if (api_user() === false) { @@ -954,13 +953,9 @@ function api_account_verify_credentials($type) // - Adding last status if (!$skip_status) { - $user_info["status"] = api_status_show("raw"); - if (isset($user_info["status"])) { - if (!is_array($user_info["status"]) || !count($user_info["status"])) { - unset($user_info["status"]); - } else { - unset($user_info["status"]["user"]); - } + $item = api_get_last_status($user_info['pid'], $user_info['uid']); + if ($item) { + $user_info['status'] = api_format_item($item, $type); } } @@ -1244,105 +1239,61 @@ function api_media_upload() api_register_func('api/media/upload', 'api_media_upload', true, API_METHOD_POST); /** - * - * @param string $type Return type (atom, rss, xml, json) - * + * @param string $type Return format (atom, rss, xml, json) * @param int $item_id - * @return array|string - * @throws BadRequestException - * @throws ImagickException - * @throws InternalServerErrorException - * @throws UnauthorizedException + * @return string + * @throws Exception */ -function api_status_show($type, $item_id = 0) +function api_status_show($type, $item_id) { - $a = \get_app(); + Logger::info(API_LOG_PREFIX . 'Start', ['action' => 'status_show', 'type' => $type, 'item_id' => $item_id]); - $user_info = api_get_user($a); + $status_info = []; - Logger::log('api_status_show: user_info: '.print_r($user_info, true), Logger::DEBUG); - - if (!empty($item_id)) { - // Get the item with the given id - $condition = ['id' => $item_id]; - } else { - // get last public wall message - $condition = ['owner-id' => $user_info['pid'], 'uid' => api_user(), - 'gravity' => [GRAVITY_PARENT, GRAVITY_COMMENT]]; + $item = api_get_item(['id' => $item_id]); + if ($item) { + $status_info = api_format_item($item, $type); } - if ($type == "raw") { - $condition['private'] = false; - } + Logger::info(API_LOG_PREFIX . 'End', ['action' => 'get_status', 'status_info' => $status_info]); - $lastwall = Item::selectFirst(Item::ITEM_FIELDLIST, $condition, ['order' => ['id' => true]]); + return api_format_data('statuses', $type, ['status' => $status_info]); +} - if (DBA::isResult($lastwall)) { - $in_reply_to = api_in_reply_to($lastwall); +/** + * Retrieves the last public status of the provided user info + * + * @param int $ownerId Public contact Id + * @param int $uid User Id + * @return array + * @throws Exception + */ +function api_get_last_status($ownerId, $uid) +{ + $condition = [ + 'author-id'=> $ownerId, + 'uid' => $uid, + 'gravity' => [GRAVITY_PARENT, GRAVITY_COMMENT], + 'private' => false + ]; - $converted = api_convert_item($lastwall); + $item = api_get_item($condition); - if ($type == "xml") { - $geo = "georss:point"; - } else { - $geo = "geo"; - } + return $item; +} - $status_info = [ - 'created_at' => api_date($lastwall['created']), - 'id' => intval($lastwall['id']), - 'id_str' => (string) $lastwall['id'], - 'text' => $converted["text"], - 'source' => (($lastwall['app']) ? $lastwall['app'] : 'web'), - 'truncated' => false, - 'in_reply_to_status_id' => $in_reply_to['status_id'], - 'in_reply_to_status_id_str' => $in_reply_to['status_id_str'], - 'in_reply_to_user_id' => $in_reply_to['user_id'], - 'in_reply_to_user_id_str' => $in_reply_to['user_id_str'], - 'in_reply_to_screen_name' => $in_reply_to['screen_name'], - 'user' => $user_info, - $geo => null, - 'coordinates' => '', - 'place' => '', - 'contributors' => '', - 'is_quote_status' => false, - 'retweet_count' => 0, - 'favorite_count' => 0, - 'favorited' => $lastwall['starred'] ? true : false, - 'retweeted' => false, - 'possibly_sensitive' => false, - 'lang' => '', - 'statusnet_html' => $converted["html"], - 'statusnet_conversation_id' => $lastwall['parent'], - 'external_url' => System::baseUrl() . '/display/' . $lastwall['guid'], - ]; +/** + * Retrieves a single item record based on the provided condition and converts it for API use. + * + * @param array $condition Item table condition array + * @return array + * @throws Exception + */ +function api_get_item(array $condition) +{ + $item = Item::selectFirst(Item::DISPLAY_FIELDLIST, $condition, ['order' => ['id' => true]]); - if (count($converted["attachments"]) > 0) { - $status_info["attachments"] = $converted["attachments"]; - } - - if (count($converted["entities"]) > 0) { - $status_info["entities"] = $converted["entities"]; - } - - if ($status_info["source"] == 'web') { - $status_info["source"] = ContactSelector::networkToName($lastwall['network'], $lastwall['author-link']); - } elseif (ContactSelector::networkToName($lastwall['network'], $lastwall['author-link']) != $status_info["source"]) { - $status_info["source"] = trim($status_info["source"].' ('.ContactSelector::networkToName($lastwall['network'], $lastwall['author-link']).')'); - } - - // "uid" and "self" are only needed for some internal stuff, so remove it from here - unset($status_info["user"]["uid"]); - unset($status_info["user"]["self"]); - - Logger::log('status_info: '.print_r($status_info, true), Logger::DEBUG); - - if ($type == "raw") { - return $status_info; - } - - return api_format_data("statuses", $type, ['status' => $status_info]); - } + return $item; } /** @@ -1359,66 +1310,20 @@ function api_status_show($type, $item_id = 0) */ function api_users_show($type) { - $a = \get_app(); + $a = \Friendica\BaseObject::getApp(); $user_info = api_get_user($a); - $condition = ['owner-id' => $user_info['pid'], 'uid' => api_user(), - 'gravity' => [GRAVITY_PARENT, GRAVITY_COMMENT], 'private' => false]; - $lastwall = Item::selectFirst(Item::ITEM_FIELDLIST, $condition, ['order' => ['id' => true]]); - - if (DBA::isResult($lastwall)) { - $in_reply_to = api_in_reply_to($lastwall); - - $converted = api_convert_item($lastwall); - - if ($type == "xml") { - $geo = "georss:point"; - } else { - $geo = "geo"; - } - - $user_info['status'] = [ - 'text' => $converted["text"], - 'truncated' => false, - 'created_at' => api_date($lastwall['created']), - 'in_reply_to_status_id' => $in_reply_to['status_id'], - 'in_reply_to_status_id_str' => $in_reply_to['status_id_str'], - 'source' => (($lastwall['app']) ? $lastwall['app'] : 'web'), - 'id' => intval($lastwall['contact-id']), - 'id_str' => (string) $lastwall['contact-id'], - 'in_reply_to_user_id' => $in_reply_to['user_id'], - 'in_reply_to_user_id_str' => $in_reply_to['user_id_str'], - 'in_reply_to_screen_name' => $in_reply_to['screen_name'], - $geo => null, - 'favorited' => $lastwall['starred'] ? true : false, - 'statusnet_html' => $converted["html"], - 'statusnet_conversation_id' => $lastwall['parent'], - 'external_url' => System::baseUrl() . "/display/" . $lastwall['guid'], - ]; - - if (count($converted["attachments"]) > 0) { - $user_info["status"]["attachments"] = $converted["attachments"]; - } - - if (count($converted["entities"]) > 0) { - $user_info["status"]["entities"] = $converted["entities"]; - } - - if ($user_info["status"]["source"] == 'web') { - $user_info["status"]["source"] = ContactSelector::networkToName($lastwall['network'], $lastwall['author-link']); - } - - if (ContactSelector::networkToName($lastwall['network'], $user_info['url']) != $user_info["status"]["source"]) { - $user_info["status"]["source"] = trim($user_info["status"]["source"] . ' (' . ContactSelector::networkToName($lastwall['network'], $lastwall['author-link']) . ')'); - } + $item = api_get_last_status($user_info['pid'], $user_info['uid']); + if ($item) { + $user_info['status'] = api_format_item($item, $type); } // "uid" and "self" are only needed for some internal stuff, so remove it from here - unset($user_info["uid"]); - unset($user_info["self"]); + unset($user_info['uid']); + unset($user_info['self']); - return api_format_data("user", $type, ['user' => $user_info]); + return api_format_data('user', $type, ['user' => $user_info]); } /// @TODO move to top of file or somewhere better @@ -1685,7 +1590,7 @@ function api_statuses_home_timeline($type) Item::update(['unseen' => false], ['unseen' => true, 'id' => $idarray]); } } - + bindComments($ret); $data = ['status' => $ret]; @@ -2972,7 +2877,7 @@ function api_format_items_profiles($profile_row) /** * @brief format items to be returned by api * - * @param array $r array of items + * @param array $items array of items * @param array $user_info * @param bool $filter_user filter items by $user_info * @param string $type Return type (atom, rss, xml, json) @@ -2982,14 +2887,13 @@ function api_format_items_profiles($profile_row) * @throws InternalServerErrorException * @throws UnauthorizedException */ -function api_format_items($r, $user_info, $filter_user = false, $type = "json") +function api_format_items($items, $user_info, $filter_user = false, $type = "json") { - $a = \get_app(); + $a = \Friendica\BaseObject::getApp(); $ret = []; - foreach ((array)$r as $item) { - localize_item($item); + foreach ((array)$items as $item) { list($status_user, $author_user, $owner_user) = api_item_get_user($a, $item); // Look if the posts are matching if they should be filtered by user id @@ -2997,100 +2901,181 @@ function api_format_items($r, $user_info, $filter_user = false, $type = "json") continue; } - $in_reply_to = api_in_reply_to($item); + $status = api_format_item($item, $type, $status_user, $author_user, $owner_user); - $converted = api_convert_item($item); - - if ($type == "xml") { - $geo = "georss:point"; - } else { - $geo = "geo"; - } - - $status = [ - 'text' => $converted["text"], - 'truncated' => false, - 'created_at'=> api_date($item['created']), - 'in_reply_to_status_id' => $in_reply_to['status_id'], - 'in_reply_to_status_id_str' => $in_reply_to['status_id_str'], - 'source' => (($item['app']) ? $item['app'] : 'web'), - 'id' => intval($item['id']), - 'id_str' => (string) intval($item['id']), - 'in_reply_to_user_id' => $in_reply_to['user_id'], - 'in_reply_to_user_id_str' => $in_reply_to['user_id_str'], - 'in_reply_to_screen_name' => $in_reply_to['screen_name'], - $geo => null, - 'favorited' => $item['starred'] ? true : false, - 'user' => $status_user, - 'friendica_author' => $author_user, - 'friendica_owner' => $owner_user, - 'friendica_private' => $item['private'] == 1, - //'entities' => NULL, - 'statusnet_html' => $converted["html"], - 'statusnet_conversation_id' => $item['parent'], - 'external_url' => System::baseUrl() . "/display/" . $item['guid'], - 'friendica_activities' => api_format_items_activities($item, $type), - ]; - - if (count($converted["attachments"]) > 0) { - $status["attachments"] = $converted["attachments"]; - } - - if (count($converted["entities"]) > 0) { - $status["entities"] = $converted["entities"]; - } - - if ($status["source"] == 'web') { - $status["source"] = ContactSelector::networkToName($item['network'], $item['author-link']); - } elseif (ContactSelector::networkToName($item['network'], $item['author-link']) != $status["source"]) { - $status["source"] = trim($status["source"].' ('.ContactSelector::networkToName($item['network'], $item['author-link']).')'); - } - - if ($item["id"] == $item["parent"]) { - $retweeted_item = api_share_as_retweet($item); - if ($retweeted_item !== false) { - $retweeted_status = $status; - $status['user'] = $status['friendica_owner']; - try { - $retweeted_status["user"] = api_get_user($a, $retweeted_item["author-id"]); - } catch (BadRequestException $e) { - // user not found. should be found? - /// @todo check if the user should be always found - $retweeted_status["user"] = []; - } - - $rt_converted = api_convert_item($retweeted_item); - - $retweeted_status['text'] = $rt_converted["text"]; - $retweeted_status['statusnet_html'] = $rt_converted["html"]; - $retweeted_status['friendica_activities'] = api_format_items_activities($retweeted_item, $type); - $retweeted_status['created_at'] = api_date($retweeted_item['created']); - $status['retweeted_status'] = $retweeted_status; - $status['friendica_author'] = $retweeted_status['friendica_author']; - } - } - - // "uid" and "self" are only needed for some internal stuff, so remove it from here - unset($status["user"]["uid"]); - unset($status["user"]["self"]); - - if ($item["coord"] != "") { - $coords = explode(' ', $item["coord"]); - if (count($coords) == 2) { - if ($type == "json") { - $status["geo"] = ['type' => 'Point', - 'coordinates' => [(float) $coords[0], - (float) $coords[1]]]; - } else {// Not sure if this is the official format - if someone founds a documentation we can check - $status["georss:point"] = $item["coord"]; - } - } - } $ret[] = $status; - }; + } + return $ret; } +/** + * @param array $item Item record + * @param string $type Return format (atom, rss, xml, json) + * @param array $status_user User record of the item author, can be provided by api_item_get_user() + * @param array $author_user User record of the item author, can be provided by api_item_get_user() + * @param array $owner_user User record of the item owner, can be provided by api_item_get_user() + * @return array API-formatted status + * @throws BadRequestException + * @throws ImagickException + * @throws InternalServerErrorException + * @throws UnauthorizedException + */ +function api_format_item($item, $type = "json", $status_user = null, $author_user = null, $owner_user = null) +{ + $a = \Friendica\BaseObject::getApp(); + + if (empty($status_user) || empty($author_user) || empty($owner_user)) { + list($status_user, $author_user, $owner_user) = api_item_get_user($a, $item); + } + + localize_item($item); + + $in_reply_to = api_in_reply_to($item); + + $converted = api_convert_item($item); + + if ($type == "xml") { + $geo = "georss:point"; + } else { + $geo = "geo"; + } + + $status = [ + 'text' => $converted["text"], + 'truncated' => false, + 'created_at'=> api_date($item['created']), + 'in_reply_to_status_id' => $in_reply_to['status_id'], + 'in_reply_to_status_id_str' => $in_reply_to['status_id_str'], + 'source' => (($item['app']) ? $item['app'] : 'web'), + 'id' => intval($item['id']), + 'id_str' => (string) intval($item['id']), + 'in_reply_to_user_id' => $in_reply_to['user_id'], + 'in_reply_to_user_id_str' => $in_reply_to['user_id_str'], + 'in_reply_to_screen_name' => $in_reply_to['screen_name'], + $geo => null, + 'favorited' => $item['starred'] ? true : false, + 'user' => $status_user, + 'friendica_author' => $author_user, + 'friendica_owner' => $owner_user, + 'friendica_private' => $item['private'] == 1, + //'entities' => NULL, + 'statusnet_html' => $converted["html"], + 'statusnet_conversation_id' => $item['parent'], + 'external_url' => System::baseUrl() . "/display/" . $item['guid'], + 'friendica_activities' => api_format_items_activities($item, $type), + ]; + + if (count($converted["attachments"]) > 0) { + $status["attachments"] = $converted["attachments"]; + } + + if (count($converted["entities"]) > 0) { + $status["entities"] = $converted["entities"]; + } + + if ($status["source"] == 'web') { + $status["source"] = ContactSelector::networkToName($item['network'], $item['author-link']); + } elseif (ContactSelector::networkToName($item['network'], $item['author-link']) != $status["source"]) { + $status["source"] = trim($status["source"].' ('.ContactSelector::networkToName($item['network'], $item['author-link']).')'); + } + + $retweeted_item = []; + $quoted_item = []; + + if ($item["id"] == $item["parent"]) { + $body = $item['body']; + $retweeted_item = api_share_as_retweet($item); + if ($body != $item['body']) { + $quoted_item = $retweeted_item; + $retweeted_item = []; + } + } + + if (empty($retweeted_item) && ($item['owner-id'] == $item['author-id'])) { + $announce = api_get_announce($item); + if (!empty($announce)) { + $retweeted_item = $item; + $item = $announce; + $status['friendica_owner'] = api_get_user($a, $announce['author-id']); + } + } + + if (!empty($quoted_item)) { + $conv_quoted = api_convert_item($quoted_item); + $quoted_status = $status; + unset($quoted_status['friendica_author']); + unset($quoted_status['friendica_owner']); + unset($quoted_status['friendica_activities']); + unset($quoted_status['friendica_private']); + unset($quoted_status['statusnet_conversation_id']); + $quoted_status['text'] = $conv_quoted['text']; + $quoted_status['statusnet_html'] = $conv_quoted['html']; + try { + $quoted_status["user"] = api_get_user($a, $quoted_item["author-id"]); + } catch (BadRequestException $e) { + // user not found. should be found? + /// @todo check if the user should be always found + $quoted_status["user"] = []; + } + } + + if (!empty($retweeted_item)) { + $retweeted_status = $status; + unset($retweeted_status['friendica_author']); + unset($retweeted_status['friendica_owner']); + unset($retweeted_status['friendica_activities']); + unset($retweeted_status['friendica_private']); + unset($retweeted_status['statusnet_conversation_id']); + $status['user'] = $status['friendica_owner']; + try { + $retweeted_status["user"] = api_get_user($a, $retweeted_item["author-id"]); + } catch (BadRequestException $e) { + // user not found. should be found? + /// @todo check if the user should be always found + $retweeted_status["user"] = []; + } + + $rt_converted = api_convert_item($retweeted_item); + + $retweeted_status['text'] = $rt_converted["text"]; + $retweeted_status['statusnet_html'] = $rt_converted["html"]; + $retweeted_status['created_at'] = api_date($retweeted_item['created']); + + if (!empty($quoted_status)) { + $retweeted_status['quoted_status'] = $quoted_status; + } + + $status['friendica_author'] = $retweeted_status['user']; + $status['retweeted_status'] = $retweeted_status; + } elseif (!empty($quoted_status)) { + $root_status = api_convert_item($item); + + $status['text'] = $root_status["text"]; + $status['statusnet_html'] = $root_status["html"]; + $status['quoted_status'] = $quoted_status; + } + + // "uid" and "self" are only needed for some internal stuff, so remove it from here + unset($status["user"]["uid"]); + unset($status["user"]["self"]); + + if ($item["coord"] != "") { + $coords = explode(' ', $item["coord"]); + if (count($coords) == 2) { + if ($type == "json") { + $status["geo"] = ['type' => 'Point', + 'coordinates' => [(float) $coords[0], + (float) $coords[1]]]; + } else {// Not sure if this is the official format - if someone founds a documentation we can check + $status["georss:point"] = $item["coord"]; + } + } + } + + return $status; +} + /** * Returns the remaining number of API requests available to the user before the API limit is reached. * @@ -5046,6 +5031,40 @@ function api_friendica_remoteauth() } api_register_func('api/friendica/remoteauth', 'api_friendica_remoteauth', true); +/** + * Return an item with announcer data if it had been announced + * + * @param array $item Item array + * @return array Item array with announce data + */ +function api_get_announce($item) +{ + // Quit if the item already has got a different owner and author + if ($item['owner-id'] != $item['author-id']) { + return []; + } + + // Don't change original or Diaspora posts + if ($item['origin'] || in_array($item['network'], [Protocol::DIASPORA])) { + return []; + } + + // Quit if we do now the original author and it had been a post from a native network + if (!empty($item['contact-uid']) && in_array($item['network'], Protocol::NATIVE_SUPPORT)) { + return []; + } + + $fields = ['author-id', 'author-name', 'author-link', 'author-avatar']; + $activity = Item::activityToIndex(ACTIVITY2_ANNOUNCE); + $condition = ['parent-uri' => $item['uri'], 'gravity' => GRAVITY_ACTIVITY, 'uid' => [0, $item['uid']], 'activity' => $activity]; + $announce = Item::selectFirstForUser($item['uid'], $fields, $condition, ['order' => ['created' => true]]); + if (!DBA::isResult($announce)) { + return []; + } + + return array_merge($item, $announce); +} + /** * @brief Return the item shared, if the item contains only the [share] tag * @@ -5141,7 +5160,12 @@ function api_share_as_retweet(&$item) $posted = $matches[1]; } - $shared_body = preg_replace("/\[share(.*?)\]\s?(.*?)\s?\[\/share\]\s?/ism", "$2", $body); + $pre_body = trim(preg_replace("/(.*?)\[share.*?\]\s?.*?\s?\[\/share\]\s?/ism", "$1", $body)); + if ($pre_body != '') { + $item['body'] = $pre_body; + } + + $shared_body = trim(preg_replace("/(.*?)\[share.*?\]\s?(.*?)\s?\[\/share\]\s?/ism", "$2", $body)); if (($shared_body == "") || ($profile == "") || ($author == "") || ($avatar == "") || ($posted == "")) { return false; @@ -5957,7 +5981,7 @@ function api_friendica_notification($type) } $nm = new NotificationsManager(); - $notes = $nm->getAll([], "+seen -date", 50); + $notes = $nm->getAll([], ['seen' => 'ASC', 'date' => 'DESC'], 50); if ($type == "xml") { $xmlnotes = []; diff --git a/include/conversation.php b/include/conversation.php index 9f4313a39..85938ca0f 100644 --- a/include/conversation.php +++ b/include/conversation.php @@ -396,7 +396,7 @@ function visible_activity($item) { * likes (etc.) can apply to other things besides posts. Check if they are post children, * in which case we handle them specially */ - $hidden_activities = [ACTIVITY_LIKE, ACTIVITY_DISLIKE, ACTIVITY_ATTEND, ACTIVITY_ATTENDNO, ACTIVITY_ATTENDMAYBE, ACTIVITY_FOLLOW]; + $hidden_activities = [ACTIVITY_LIKE, ACTIVITY_DISLIKE, ACTIVITY_ATTEND, ACTIVITY_ATTENDNO, ACTIVITY_ATTENDMAYBE, ACTIVITY_FOLLOW, ACTIVITY2_ANNOUNCE]; foreach ($hidden_activities as $act) { if (activity_match($item['verb'], $act)) { return false; @@ -565,8 +565,12 @@ function conversation(App $a, array $items, Pager $pager, $mode, $update, $previ $items = $cb['items']; $conv_responses = [ - 'like' => ['title' => L10n::t('Likes','title')], 'dislike' => ['title' => L10n::t('Dislikes','title')], - 'attendyes' => ['title' => L10n::t('Attending','title')], 'attendno' => ['title' => L10n::t('Not attending','title')], 'attendmaybe' => ['title' => L10n::t('Might attend','title')] + 'like' => ['title' => L10n::t('Likes','title')], + 'dislike' => ['title' => L10n::t('Dislikes','title')], + 'attendyes' => ['title' => L10n::t('Attending','title')], + 'attendno' => ['title' => L10n::t('Not attending','title')], + 'attendmaybe' => ['title' => L10n::t('Might attend','title')], + 'announce' => ['title' => L10n::t('Reshares','title')] ]; // array with html for each thread (parent+comments) @@ -784,6 +788,46 @@ function conversation(App $a, array $items, Pager $pager, $mode, $update, $previ return $o; } +/** + * Fetch all comments from a query. Additionally set the newest resharer as thread owner. + * + * @param $thread_items Database statement with thread posts + * @return array items with parents and comments + */ +function conversation_fetch_comments($thread_items) { + $comments = []; + $parentlines = []; + $lineno = 0; + $actor = []; + $created = ''; + + while ($row = Item::fetch($thread_items)) { + if (($row['verb'] == ACTIVITY2_ANNOUNCE) && !empty($row['contact-uid']) && ($row['created'] > $created) && ($row['thr-parent'] == $row['parent-uri'])) { + $actor = ['link' => $row['author-link'], 'avatar' => $row['author-avatar'], 'name' => $row['author-name']]; + $created = $row['created']; + } + + if ((($row['gravity'] == GRAVITY_PARENT) && !$row['origin'] && !in_array($row['network'], [Protocol::DIASPORA])) && + (empty($row['contact-uid']) || !in_array($row['network'], Protocol::NATIVE_SUPPORT))) { + $parentlines[] = $lineno; + } + + $comments[] = $row; + $lineno++; + } + + DBA::close($thread_items); + + if (!empty($actor)) { + foreach ($parentlines as $line) { + $comments[$line]['owner-link'] = $actor['link']; + $comments[$line]['owner-avatar'] = $actor['avatar']; + $comments[$line]['owner-name'] = $actor['name']; + } + } + return $comments; +} + /** * @brief Add comments to top level entries that had been fetched before * @@ -815,9 +859,10 @@ function conversation_add_children(array $parents, $block_authors, $order, $uid) if ($block_authors) { $condition[0] .= "AND NOT `author`.`hidden`"; } - $thread_items = Item::selectForUser(local_user(), [], $condition, $params); - $comments = Item::inArray($thread_items); + $thread_items = Item::selectForUser(local_user(), array_merge(Item::DISPLAY_FIELDLIST, ['contact-uid', 'gravity']), $condition, $params); + + $comments = conversation_fetch_comments($thread_items); if (count($comments) != 0) { $items = array_merge($items, $comments); @@ -963,6 +1008,9 @@ function builtin_activity_puller($item, &$conv_responses) { case 'attendmaybe': $verb = ACTIVITY_ATTENDMAYBE; break; + case 'announce': + $verb = ACTIVITY2_ANNOUNCE; + break; default: return; } @@ -1045,6 +1093,9 @@ function format_like($cnt, array $arr, $type, $id) { case 'attendmaybe' : $phrase = L10n::t('%s attends maybe.', $likers); break; + case 'announce' : + $phrase = L10n::t('%s reshared this.', $likers); + break; } } @@ -1084,6 +1135,10 @@ function format_like($cnt, array $arr, $type, $id) { $phrase = L10n::t('%2$d people attend maybe', $spanatts, $cnt); $explikers = L10n::t('%s attend maybe.', $likers); break; + case 'announce': + $phrase = L10n::t('%2$d people reshared this', $spanatts, $cnt); + $explikers = L10n::t('%s reshared this.', $likers); + break; } $expanded .= "\t" . ''; diff --git a/include/items.php b/include/items.php index a28e19a05..25c857f11 100644 --- a/include/items.php +++ b/include/items.php @@ -446,80 +446,3 @@ function drop_item($id, $return = '') //NOTREACHED } } - -/* arrange the list in years */ -function list_post_dates($uid, $wall) -{ - $dnow = DateTimeFormat::localNow('Y-m-d'); - - $dthen = Item::firstPostDate($uid, $wall); - if (!$dthen) { - return []; - } - - // Set the start and end date to the beginning of the month - $dnow = substr($dnow, 0, 8) . '01'; - $dthen = substr($dthen, 0, 8) . '01'; - - $ret = []; - - /* - * Starting with the current month, get the first and last days of every - * month down to and including the month of the first post - */ - while (substr($dnow, 0, 7) >= substr($dthen, 0, 7)) { - $dyear = intval(substr($dnow, 0, 4)); - $dstart = substr($dnow, 0, 8) . '01'; - $dend = substr($dnow, 0, 8) . Temporal::getDaysInMonth(intval($dnow), intval(substr($dnow, 5))); - $start_month = DateTimeFormat::utc($dstart, 'Y-m-d'); - $end_month = DateTimeFormat::utc($dend, 'Y-m-d'); - $str = L10n::getDay(DateTimeFormat::utc($dnow, 'F')); - - if (empty($ret[$dyear])) { - $ret[$dyear] = []; - } - - $ret[$dyear][] = [$str, $end_month, $start_month]; - $dnow = DateTimeFormat::utc($dnow . ' -1 month', 'Y-m-d'); - } - return $ret; -} - -function posted_date_widget($url, $uid, $wall) -{ - $o = ''; - - if (!Feature::isEnabled($uid, 'archives')) { - return $o; - } - - // For former Facebook folks that left because of "timeline" - /* - * @TODO old-lost code? - if ($wall && intval(PConfig::get($uid, 'system', 'no_wall_archive_widget'))) - return $o; - */ - - $visible_years = PConfig::get($uid, 'system', 'archive_visible_years', 5); - - $ret = list_post_dates($uid, $wall); - - if (!DBA::isResult($ret)) { - return $o; - } - - $cutoff_year = intval(DateTimeFormat::localNow('Y')) - $visible_years; - $cutoff = ((array_key_exists($cutoff_year, $ret))? true : false); - - $o = Renderer::replaceMacros(Renderer::getMarkupTemplate('posted_date_widget.tpl'),[ - '$title' => L10n::t('Archives'), - '$size' => $visible_years, - '$cutoff_year' => $cutoff_year, - '$cutoff' => $cutoff, - '$url' => $url, - '$dates' => $ret, - '$showmore' => L10n::t('show more') - - ]); - return $o; -} diff --git a/include/text.php b/include/text.php index c4249f86c..e4227cd7d 100644 --- a/include/text.php +++ b/include/text.php @@ -8,10 +8,8 @@ use Friendica\Content\Smilies; use Friendica\Content\Text\BBCode; use Friendica\Core\Protocol; use Friendica\Model\Contact; - use Friendica\Model\FileTag; use Friendica\Util\Strings; -use Friendica\Util\XML; /** * Turn user/group ACLs stored as angle bracketed text into arrays @@ -186,21 +184,17 @@ function get_cats_and_terms($item) { $categories = []; $folders = []; - - $matches = []; $first = true; - $cnt = preg_match_all('/<(.*?)>/', $item['file'], $matches, PREG_SET_ORDER); - if ($cnt) { - foreach ($matches as $mtch) { - $categories[] = [ - 'name' => XML::escape(FileTag::decode($mtch[1])), - 'url' => "#", - 'removeurl' => ((local_user() == $item['uid'])?'filerm/' . $item['id'] . '?f=&cat=' . XML::escape(FileTag::decode($mtch[1])):""), - 'first' => $first, - 'last' => false - ]; - $first = false; - } + + foreach (FileTag::fileToArray($item['file'] ?? '', 'category') as $savedFolderName) { + $categories[] = [ + 'name' => $savedFolderName, + 'url' => "#", + 'removeurl' => ((local_user() == $item['uid']) ? 'filerm/' . $item['id'] . '?f=&cat=' . rawurlencode($savedFolderName) : ""), + 'first' => $first, + 'last' => false + ]; + $first = false; } if (count($categories)) { @@ -208,20 +202,15 @@ function get_cats_and_terms($item) } if (local_user() == $item['uid']) { - $matches = []; - $first = true; - $cnt = preg_match_all('/\[(.*?)\]/', $item['file'], $matches, PREG_SET_ORDER); - if ($cnt) { - foreach ($matches as $mtch) { - $folders[] = [ - 'name' => XML::escape(FileTag::decode($mtch[1])), - 'url' => "#", - 'removeurl' => ((local_user() == $item['uid']) ? 'filerm/' . $item['id'] . '?f=&term=' . XML::escape(FileTag::decode($mtch[1])) : ""), - 'first' => $first, - 'last' => false - ]; - $first = false; - } + foreach (FileTag::fileToArray($item['file'] ?? '') as $savedFolderName) { + $folders[] = [ + 'name' => $savedFolderName, + 'url' => "#", + 'removeurl' => ((local_user() == $item['uid']) ? 'filerm/' . $item['id'] . '?f=&term=' . rawurlencode($savedFolderName) : ""), + 'first' => $first, + 'last' => false + ]; + $first = false; } } diff --git a/mod/_well_known.php b/mod/_well_known.php deleted file mode 100644 index f6a351bb2..000000000 --- a/mod/_well_known.php +++ /dev/null @@ -1,82 +0,0 @@ -argc > 1) { - switch ($a->argv[1]) { - case "host-meta": - hostxrd_init($a); - break; - case "x-social-relay": - wk_social_relay(); - break; - case "nodeinfo": - nodeinfo_wellknown($a); - break; - case "webfinger": - xrd_init($a); - break; - } - } - System::httpExit(404); -} - -function wk_social_relay() -{ - $subscribe = (bool) Config::get('system', 'relay_subscribe', false); - - if ($subscribe) { - $scope = Config::get('system', 'relay_scope', SR_SCOPE_ALL); - } else { - $scope = SR_SCOPE_NONE; - } - - $tags = []; - - if ($scope == SR_SCOPE_TAGS) { - $server_tags = Config::get('system', 'relay_server_tags'); - $tagitems = explode(",", $server_tags); - - /// @todo Check if it was better to use "strtolower" on the tags - foreach ($tagitems AS $tag) { - $tag = trim($tag, "# "); - $tags[$tag] = $tag; - } - - if (Config::get('system', 'relay_user_tags')) { - $terms = q("SELECT DISTINCT(`term`) FROM `search`"); - - foreach ($terms AS $term) { - $tag = trim($term["term"], "#"); - $tags[$tag] = $tag; - } - } - } - - $taglist = []; - foreach ($tags AS $tag) { - if (!empty($tag)) { - $taglist[] = $tag; - } - } - - $relay = [ - 'subscribe' => $subscribe, - 'scope' => $scope, - 'tags' => $taglist, - 'protocols' => ['diaspora' => ['receive' => System::baseUrl() . '/receive/public'], - 'dfrn' => ['receive' => System::baseUrl() . '/dfrn_notify']] - ]; - - header('Content-type: application/json; charset=utf-8'); - echo json_encode($relay, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); - exit; -} diff --git a/mod/admin.php b/mod/admin.php deleted file mode 100644 index f8a75b7a2..000000000 --- a/mod/admin.php +++ /dev/null @@ -1,2720 +0,0 @@ -argc > 2 && $a->argv[1] == 'themes') { - $theme = $a->argv[2]; - if (is_file("view/theme/$theme/config.php")) { - $a->setCurrentTheme($theme); - } - } -} - -/** - * @brief Process send data from the admin panels subpages - * - * This function acts as relay for processing the data send from the subpages - * of the admin panel. Depending on the 1st parameter of the url (argv[1]) - * specialized functions are called to process the data from the subpages. - * - * The function itself does not return anything, but the subsequently function - * return the HTML for the pages of the admin panel. - * - * @param App $a - * @throws ImagickException - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - */ -function admin_post(App $a) -{ - if (!is_site_admin()) { - return; - } - - // do not allow a page manager to access the admin panel at all. - - if (!empty($_SESSION['submanage'])) { - return; - } - - $return_path = 'admin'; - if ($a->argc > 1) { - switch ($a->argv[1]) { - case 'site': - admin_page_site_post($a); - break; - case 'users': - admin_page_users_post($a); - break; - case 'addons': - if ($a->argc > 2 && - is_file("addon/" . $a->argv[2] . "/" . $a->argv[2] . ".php")) { - include_once "addon/" . $a->argv[2] . "/" . $a->argv[2] . ".php"; - if (function_exists($a->argv[2] . '_addon_admin_post')) { - $func = $a->argv[2] . '_addon_admin_post'; - $func($a); - } - } - $return_path = 'admin/addons/' . $a->argv[2]; - break; - case 'themes': - if ($a->argc < 2) { - if ($a->isAjax()) { - return; - } - $a->internalRedirect('admin/'); - return; - } - - $theme = $a->argv[2]; - if (is_file("view/theme/$theme/config.php")) { - require_once "view/theme/$theme/config.php"; - - if (function_exists('theme_admin_post')) { - theme_admin_post($a); - } - } - - info(L10n::t('Theme settings updated.')); - if ($a->isAjax()) { - return; - } - $return_path = 'admin/themes/' . $theme . (!empty($_GET['mode']) ? '?mode=' . $_GET['mode'] : ''); - break; - case 'tos': - admin_page_tos_post($a); - break; - case 'features': - admin_page_features_post($a); - break; - case 'logs': - admin_page_logs_post($a); - break; - case 'contactblock': - admin_page_contactblock_post($a); - break; - case 'blocklist': - admin_page_blocklist_post($a); - break; - case 'deleteitem': - admin_page_deleteitem_post($a); - break; - } - } - - $a->internalRedirect($return_path); - return; // NOTREACHED -} - -/** - * @brief Generates content of the admin panel pages - * - * This function generates the content for the admin panel. It consists of the - * aside menu (same for the entire admin panel) and the code for the soecified - * subpage of the panel. - * - * The structure of the adress is: /admin/subpage/details though "details" is - * only necessary for some subpages, like themes or addons where it is the name - * of one theme resp. addon from which the details should be shown. Content for - * the subpages is generated in separate functions for each of the subpages. - * - * The returned string hold the generated HTML code of the page. - * - * @param App $a - * @return string - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - */ -function admin_content(App $a) -{ - if (!is_site_admin()) { - return Login::form(); - } - - if (!empty($_SESSION['submanage'])) { - return ""; - } - - // APC deactivated, since there are problems with PHP 5.5 - //if (function_exists("apc_delete")) { - // $toDelete = new APCIterator('user', APC_ITER_VALUE); - // apc_delete($toDelete); - //} - // Header stuff - $a->page['htmlhead'] .= Renderer::replaceMacros(Renderer::getMarkupTemplate('admin/settings_head.tpl'), []); - - /* - * Side bar links - */ - $aside_tools = []; - // array(url, name, extra css classes) - // not part of $aside to make the template more adjustable - $aside_sub = [ - 'information' => [L10n::t('Information'), [ - 'overview' => ['admin/', L10n::t('Overview'), 'overview'], - 'federation' => ['admin/federation/' , L10n::t('Federation Statistics'), 'federation']]], - 'configuration' => [L10n::t('Configuration'), [ - 'site' => ['admin/site/' , L10n::t('Site') , 'site'], - 'users' => ['admin/users/' , L10n::t('Users') , 'users'], - 'addons' => ['admin/addons/' , L10n::t('Addons') , 'addons'], - 'themes' => ['admin/themes/' , L10n::t('Themes') , 'themes'], - 'features' => ['admin/features/' , L10n::t('Additional features') , 'features'], - 'tos' => ['admin/tos/' , L10n::t('Terms of Service') , 'tos']]], - 'database' => [L10n::t('Database'), [ - 'dbsync' => ['admin/dbsync/' , L10n::t('DB updates') , 'dbsync'], - 'queue' => ['admin/queue/' , L10n::t('Inspect Queue') , 'queue'], - 'deferred' => ['admin/deferred/' , L10n::t('Inspect Deferred Workers'), 'deferred'], - 'workerqueue' => ['admin/workerqueue/' , L10n::t('Inspect worker Queue') , 'workerqueue']]], - 'tools' => [L10n::t('Tools'), [ - 'contactblock' => ['admin/contactblock/', L10n::t('Contact Blocklist') , 'contactblock'], - 'blocklist' => ['admin/blocklist/' , L10n::t('Server Blocklist') , 'blocklist'], - 'deleteitem' => ['admin/deleteitem/' , L10n::t('Delete Item') , 'deleteitem'],]], - 'logs' => [L10n::t('Logs'), [ - 'logsconfig' => ['admin/logs/', L10n::t('Logs'), 'logs'], - 'logsview' => ['admin/viewlogs/', L10n::t('View Logs'), 'viewlogs'] - ]], - 'diagnostics' => [L10n::t('Diagnostics'), [ - 'phpinfo' => ['phpinfo/', L10n::t('PHP Info'), 'phpinfo'], - 'probe' => ['probe/', L10n::t('probe address'), 'probe'], - 'webfinger' =>['webfinger/', L10n::t('check webfinger'), 'webfinger'] - ]] - ]; - - /* get addons admin page */ - - $r = q("SELECT `name` FROM `addon` WHERE `plugin_admin` = 1 ORDER BY `name`"); - $aside_tools['addons_admin'] = []; - $addons_admin = []; - foreach ($r as $h) { - $addon = $h['name']; - $aside_tools['addons_admin'][] = ["admin/addons/" . $addon, $addon, "addon"]; - // temp addons with admin - $addons_admin[] = $addon; - } - - $t = Renderer::getMarkupTemplate('admin/aside.tpl'); - $a->page['aside'] .= Renderer::replaceMacros($t, [ - '$admin' => $aside_tools, - '$subpages' => $aside_sub, - '$admtxt' => L10n::t('Admin'), - '$plugadmtxt' => L10n::t('Addon Features'), - '$h_pending' => L10n::t('User registrations waiting for confirmation'), - '$admurl' => "admin/" - ]); - - // Page content - $o = ''; - // urls - if ($a->argc > 1) { - switch ($a->argv[1]) { - case 'site': - $o = admin_page_site($a); - break; - case 'users': - $o = admin_page_users($a); - break; - case 'addons': - $o = admin_page_addons($a, $addons_admin); - break; - case 'themes': - $o = admin_page_themes($a); - break; - case 'features': - $o = admin_page_features($a); - break; - case 'logs': - $o = admin_page_logs($a); - break; - case 'viewlogs': - $o = admin_page_viewlogs($a); - break; - case 'dbsync': - $o = admin_page_dbsync($a); - break; - case 'queue': - $o = admin_page_queue($a); - break; - case 'deferred': - $o = admin_page_workerqueue($a, true); - break; - case 'workerqueue': - $o = admin_page_workerqueue($a, false); - break; - case 'federation': - $o = admin_page_federation($a); - break; - case 'contactblock': - $o = admin_page_contactblock($a); - break; - case 'blocklist': - $o = admin_page_blocklist($a); - break; - case 'deleteitem': - $o = admin_page_deleteitem($a); - break; - case 'tos': - $o = admin_page_tos($a); - break; - default: - notice(L10n::t("Item not found.")); - } - } else { - $o = admin_page_summary($a); - } - - if ($a->isAjax()) { - echo $o; - exit(); - } else { - return $o; - } -} - -/** - * @brief Subpage to define the display of a Terms of Usage page. - * - * @param App $a - * @return string - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - */ -function admin_page_tos(App $a) -{ - $tos = new Tos(); - $t = Renderer::getMarkupTemplate('admin/tos.tpl'); - return Renderer::replaceMacros($t, [ - '$title' => L10n::t('Administration'), - '$page' => L10n::t('Terms of Service'), - '$displaytos' => ['displaytos', L10n::t('Display Terms of Service'), Config::get('system', 'tosdisplay'), L10n::t('Enable the Terms of Service page. If this is enabled a link to the terms will be added to the registration form and the general information page.')], - '$displayprivstatement' => ['displayprivstatement', L10n::t('Display Privacy Statement'), Config::get('system', 'tosprivstatement'), L10n::t('Show some informations regarding the needed information to operate the node according e.g. to EU-GDPR.', 'https://en.wikipedia.org/wiki/General_Data_Protection_Regulation')], - '$preview' => L10n::t('Privacy Statement Preview'), - '$privtext' => $tos->privacy_complete, - '$tostext' => ['tostext', L10n::t('The Terms of Service'), Config::get('system', 'tostext'), L10n::t('Enter the Terms of Service for your node here. You can use BBCode. Headers of sections should be [h2] and below.')], - '$form_security_token' => BaseModule::getFormSecurityToken("admin_tos"), - '$submit' => L10n::t('Save Settings'), - ]); -} - -/** - * @brief Process send data from Admin TOS Page - * - * @param App $a - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - */ -function admin_page_tos_post(App $a) -{ - BaseModule::checkFormSecurityTokenRedirectOnError('/admin/tos', 'admin_tos'); - - if (empty($_POST['page_tos'])) { - return; - } - - $displaytos = !empty($_POST['displaytos']); - $displayprivstatement = !empty($_POST['displayprivstatement']); - $tostext = (!empty($_POST['tostext']) ? strip_tags(trim($_POST['tostext'])) : ''); - - Config::set('system', 'tosdisplay', $displaytos); - Config::set('system', 'tosprivstatement', $displayprivstatement); - Config::set('system', 'tostext', $tostext); - - $a->internalRedirect('admin/tos'); - - return; // NOTREACHED -} - -/** - * @brief Subpage to modify the server wide block list via the admin panel. - * - * This function generates the subpage of the admin panel to allow the - * modification of the node wide block/black list to block entire - * remote servers from communication with this node. The page allows - * adding, removing and editing of entries from the blocklist. - * - * @param App $a - * @return string - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - */ -function admin_page_blocklist(App $a) -{ - $blocklist = Config::get('system', 'blocklist'); - $blocklistform = []; - if (is_array($blocklist)) { - foreach ($blocklist as $id => $b) { - $blocklistform[] = [ - 'domain' => ["domain[$id]", L10n::t('Blocked domain'), $b['domain'], '', L10n::t('The blocked domain'), 'required', '', ''], - 'reason' => ["reason[$id]", L10n::t("Reason for the block"), $b['reason'], L10n::t('The reason why you blocked this domain.') . '(' . $b['domain'] . ')', 'required', '', ''], - 'delete' => ["delete[$id]", L10n::t("Delete domain") . ' (' . $b['domain'] . ')', false, L10n::t("Check to delete this entry from the blocklist")] - ]; - } - } - $t = Renderer::getMarkupTemplate('admin/blocklist.tpl'); - return Renderer::replaceMacros($t, [ - '$title' => L10n::t('Administration'), - '$page' => L10n::t('Server Blocklist'), - '$intro' => L10n::t('This page can be used to define a black list of servers from the federated network that are not allowed to interact with your node. For all entered domains you should also give a reason why you have blocked the remote server.'), - '$public' => L10n::t('The list of blocked servers will be made publically available on the /friendica page so that your users and people investigating communication problems can find the reason easily.'), - '$addtitle' => L10n::t('Add new entry to block list'), - '$newdomain' => ['newentry_domain', L10n::t('Server Domain'), '', L10n::t('The domain of the new server to add to the block list. Do not include the protocol.'), 'required', '', ''], - '$newreason' => ['newentry_reason', L10n::t('Block reason'), '', L10n::t('The reason why you blocked this domain.'), 'required', '', ''], - '$submit' => L10n::t('Add Entry'), - '$savechanges' => L10n::t('Save changes to the blocklist'), - '$currenttitle' => L10n::t('Current Entries in the Blocklist'), - '$thurl' => L10n::t('Blocked domain'), - '$threason' => L10n::t('Reason for the block'), - '$delentry' => L10n::t('Delete entry from blocklist'), - '$entries' => $blocklistform, - '$baseurl' => System::baseUrl(true), - '$confirm_delete' => L10n::t('Delete entry from blocklist?'), - '$form_security_token' => BaseModule::getFormSecurityToken("admin_blocklist") - ]); -} - -/** - * @brief Process send data from Admin Blocklist Page - * - * @param App $a - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - */ -function admin_page_blocklist_post(App $a) -{ - if (empty($_POST['page_blocklist_save']) && empty($_POST['page_blocklist_edit'])) { - return; - } - - BaseModule::checkFormSecurityTokenRedirectOnError('/admin/blocklist', 'admin_blocklist'); - - if (!empty($_POST['page_blocklist_save'])) { - // Add new item to blocklist - $blocklist = Config::get('system', 'blocklist'); - $blocklist[] = [ - 'domain' => Strings::escapeTags(trim($_POST['newentry_domain'])), - 'reason' => Strings::escapeTags(trim($_POST['newentry_reason'])) - ]; - Config::set('system', 'blocklist', $blocklist); - info(L10n::t('Server added to blocklist.') . EOL); - } else { - // Edit the entries from blocklist - $blocklist = []; - foreach ($_POST['domain'] as $id => $domain) { - // Trimming whitespaces as well as any lingering slashes - $domain = Strings::escapeTags(trim($domain, "\x00..\x1F/")); - $reason = Strings::escapeTags(trim($_POST['reason'][$id])); - if (empty($_POST['delete'][$id])) { - $blocklist[] = [ - 'domain' => $domain, - 'reason' => $reason - ]; - } - } - Config::set('system', 'blocklist', $blocklist); - info(L10n::t('Site blocklist updated.') . EOL); - } - $a->internalRedirect('admin/blocklist'); - - return; // NOTREACHED -} - -/** - * @brief Process data send by the contact block admin page - * - * @param App $a - * @throws ImagickException - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - */ -function admin_page_contactblock_post(App $a) -{ - $contact_url = defaults($_POST, 'contact_url', ''); - $contacts = defaults($_POST, 'contacts', []); - - BaseModule::checkFormSecurityTokenRedirectOnError('/admin/contactblock', 'admin_contactblock'); - - if (!empty($_POST['page_contactblock_block'])) { - $contact_id = Contact::getIdForURL($contact_url); - if ($contact_id) { - Contact::block($contact_id); - notice(L10n::t('The contact has been blocked from the node')); - } else { - notice(L10n::t("Could not find any contact entry for this URL \x28%s\x29", $contact_url)); - } - } - if (!empty($_POST['page_contactblock_unblock'])) { - foreach ($contacts as $uid) { - Contact::unblock($uid); - } - notice(L10n::tt("%s contact unblocked", "%s contacts unblocked", count($contacts))); - } - $a->internalRedirect('admin/contactblock'); - return; // NOTREACHED -} - -/** - * @brief Admin panel for server-wide contact block - * - * @param App $a - * @return string - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - */ -function admin_page_contactblock(App $a) -{ - $condition = ['uid' => 0, 'blocked' => true]; - - $total = DBA::count('contact', $condition); - - $pager = new Pager($a->query_string, 30); - - $statement = DBA::select('contact', [], $condition, ['limit' => [$pager->getStart(), $pager->getItemsPerPage()]]); - - $contacts = DBA::toArray($statement); - - $t = Renderer::getMarkupTemplate('admin/contactblock.tpl'); - $o = Renderer::replaceMacros($t, [ - // strings // - '$title' => L10n::t('Administration'), - '$page' => L10n::t('Remote Contact Blocklist'), - '$description' => L10n::t('This page allows you to prevent any message from a remote contact to reach your node.'), - '$submit' => L10n::t('Block Remote Contact'), - '$select_all' => L10n::t('select all'), - '$select_none' => L10n::t('select none'), - '$block' => L10n::t('Block'), - '$unblock' => L10n::t('Unblock'), - '$no_data' => L10n::t('No remote contact is blocked from this node.'), - - '$h_contacts' => L10n::t('Blocked Remote Contacts'), - '$h_newblock' => L10n::t('Block New Remote Contact'), - '$th_contacts' => [L10n::t('Photo'), L10n::t('Name'), L10n::t('Address'), L10n::t('Profile URL')], - - '$form_security_token' => BaseModule::getFormSecurityToken("admin_contactblock"), - - // values // - '$baseurl' => System::baseUrl(true), - - '$contacts' => $contacts, - '$total_contacts' => L10n::tt('%s total blocked contact', '%s total blocked contacts', $total), - '$paginate' => $pager->renderFull($total), - '$contacturl' => ['contact_url', L10n::t("Profile URL"), '', L10n::t("URL of the remote contact to block.")], - ]); - return $o; -} - -/** - * @brief Subpage where the admin can delete an item from their node given the GUID - * - * This subpage of the admin panel offers the nodes admin to delete an item from - * the node, given the GUID or the display URL such as http://example.com/display/123456. - * The item will then be marked as deleted in the database and processed accordingly. - * - * @param App $a - * @return string - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - */ -function admin_page_deleteitem(App $a) -{ - $t = Renderer::getMarkupTemplate('admin/deleteitem.tpl'); - - return Renderer::replaceMacros($t, [ - '$title' => L10n::t('Administration'), - '$page' => L10n::t('Delete Item'), - '$submit' => L10n::t('Delete this Item'), - '$intro1' => L10n::t('On this page you can delete an item from your node. If the item is a top level posting, the entire thread will be deleted.'), - '$intro2' => L10n::t('You need to know the GUID of the item. You can find it e.g. by looking at the display URL. The last part of http://example.com/display/123456 is the GUID, here 123456.'), - '$deleteitemguid' => ['deleteitemguid', L10n::t("GUID"), '', L10n::t("The GUID of the item you want to delete."), 'required', 'autofocus'], - '$baseurl' => System::baseUrl(), - '$form_security_token' => BaseModule::getFormSecurityToken("admin_deleteitem") - ]); -} - -/** - * @brief Process send data from Admin Delete Item Page - * - * The GUID passed through the form should be only the GUID. But we also parse - * URLs like the full /display URL to make the process more easy for the admin. - * - * @param App $a - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - */ -function admin_page_deleteitem_post(App $a) -{ - if (empty($_POST['page_deleteitem_submit'])) { - return; - } - - BaseModule::checkFormSecurityTokenRedirectOnError('/admin/deleteitem/', 'admin_deleteitem'); - - if (!empty($_POST['page_deleteitem_submit'])) { - $guid = trim(Strings::escapeTags($_POST['deleteitemguid'])); - // The GUID should not include a "/", so if there is one, we got an URL - // and the last part of it is most likely the GUID. - if (strpos($guid, '/')) { - $guid = substr($guid, strrpos($guid, '/') + 1); - } - // Now that we have the GUID, drop those items, which will also delete the - // associated threads. - Item::delete(['guid' => $guid]); - } - - info(L10n::t('Item marked for deletion.') . EOL); - $a->internalRedirect('admin/deleteitem'); - return; // NOTREACHED -} - -/** - * @brief Subpage with some stats about "the federation" network - * - * This function generates the "Federation Statistics" subpage for the admin - * panel. The page lists some numbers to the part of "The Federation" known to - * the node. This data includes the different connected networks (e.g. - * Diaspora, Hubzilla, GNU Social) and the used versions in the different - * networks. - * - * The returned string contains the HTML code of the subpage for display. - * - * @param App $a - * @return string - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - */ -function admin_page_federation(App $a) -{ - // get counts on active friendica, diaspora, redmatrix, hubzilla, gnu - // social and statusnet nodes this node is knowing - // - // We are looking for the following platforms in the DB, "Red" should find - // all variants of that platform ID string as the q() function is stripping - // off one % two of them are needed in the query - // Add more platforms if you like, when one returns 0 known nodes it is not - // displayed on the stats page. - $platforms = ['Friendi%%a', 'Diaspora', '%%red%%', 'Hubzilla', 'BlaBlaNet', 'GNU Social', 'StatusNet', 'Mastodon', 'Pleroma', 'socialhome', 'ganggo']; - $colors = [ - 'Friendi%%a' => '#ffc018', // orange from the logo - 'Diaspora' => '#a1a1a1', // logo is black and white, makes a gray - '%%red%%' => '#c50001', // fire red from the logo - 'Hubzilla' => '#43488a', // blue from the logo - 'BlaBlaNet' => '#3B5998', // blue from the navbar at blablanet-dot-com - 'GNU Social' => '#a22430', // dark red from the logo - 'StatusNet' => '#789240', // the green from the logo (red and blue have already others - 'Mastodon' => '#1a9df9', // blue from the Mastodon logo - 'Pleroma' => '#E46F0F', // Orange from the text that is used on Pleroma instances - 'socialhome' => '#52056b' , // lilac from the Django Image used at the Socialhome homepage - 'ganggo' => '#69d7e2' // from the favicon - ]; - $counts = []; - $total = 0; - $users = 0; - - foreach ($platforms as $p) { - // get a total count for the platform, the name and version of the - // highest version and the protocol tpe - $c = q('SELECT COUNT(*) AS `total`, SUM(`registered-users`) AS `users`, ANY_VALUE(`platform`) AS `platform`, - ANY_VALUE(`network`) AS `network`, MAX(`version`) AS `version` FROM `gserver` - WHERE `platform` LIKE "%s" AND `last_contact` >= `last_failure` - ORDER BY `version` ASC;', $p); - $total += $c[0]['total']; - $users += $c[0]['users']; - - // what versions for that platform do we know at all? - // again only the active nodes - $v = q('SELECT COUNT(*) AS `total`, `version` FROM `gserver` - WHERE `last_contact` >= `last_failure` AND `platform` LIKE "%s" - GROUP BY `version` - ORDER BY `version`;', $p); - - // - // clean up version numbers - // - // some platforms do not provide version information, add a unkown there - // to the version string for the displayed list. - foreach ($v as $key => $value) { - if ($v[$key]['version'] == '') { - $v[$key] = ['total' => $v[$key]['total'], 'version' => L10n::t('unknown')]; - } - } - - // Reformat and compact version numbers - if ($p == 'Pleroma') { - $compacted = []; - - foreach ($v as $key => $value) { - $version = $v[$key]['version']; - $parts = explode(' ', trim($version)); - do { - $part = array_pop($parts); - } while (!empty($parts) && ((strlen($part) >= 40) || (strlen($part) <= 3))); - - if (!empty($part)) { - if (empty($compacted[$part])) { - $compacted[$part] = $v[$key]['total']; - } else { - $compacted[$part] += $v[$key]['total']; - } - } - } - - $v = []; - foreach ($compacted as $version => $pl_total) { - $v[] = ['version' => $version, 'total' => $pl_total]; - } - } - - // in the DB the Diaspora versions have the format x.x.x.x-xx the last - // part (-xx) should be removed to clean up the versions from the "head - // commit" information and combined into a single entry for x.x.x.x - if ($p == 'Diaspora') { - $newV = []; - $newVv = []; - foreach ($v as $vv) { - $newVC = $vv['total']; - $newVV = $vv['version']; - $posDash = strpos($newVV, '-'); - if ($posDash) { - $newVV = substr($newVV, 0, $posDash); - } - if (isset($newV[$newVV])) { - $newV[$newVV] += $newVC; - } else { - $newV[$newVV] = $newVC; - } - } - foreach ($newV as $key => $value) { - array_push($newVv, ['total' => $value, 'version' => $key]); - } - $v = $newVv; - } - - // early friendica versions have the format x.x.xxxx where xxxx is the - // DB version stamp; those should be operated out and versions be - // conbined - if ($p == 'Friendi%%a') { - $newV = []; - $newVv = []; - foreach ($v as $vv) { - $newVC = $vv['total']; - $newVV = $vv['version']; - $lastDot = strrpos($newVV, '.'); - $len = strlen($newVV) - 1; - if (($lastDot == $len - 4) && (!strrpos($newVV, '-rc') == $len - 3)) { - $newVV = substr($newVV, 0, $lastDot); - } - if (isset($newV[$newVV])) { - $newV[$newVV] += $newVC; - } else { - $newV[$newVV] = $newVC; - } - } - foreach ($newV as $key => $value) { - array_push($newVv, ['total' => $value, 'version' => $key]); - } - $v = $newVv; - } - - // Assure that the versions are sorted correctly - $v2 = []; - $versions = []; - foreach ($v as $vv) { - $version = trim(strip_tags($vv["version"])); - $v2[$version] = $vv; - $versions[] = $version; - } - - usort($versions, 'version_compare'); - - $v = []; - foreach ($versions as $version) { - $v[] = $v2[$version]; - } - - // the 3rd array item is needed for the JavaScript graphs as JS does - // not like some characters in the names of variables... - $counts[$p] = [$c[0], $v, str_replace([' ', '%'], '', $p), $colors[$p]]; - } - - // some helpful text - $intro = L10n::t('This page offers you some numbers to the known part of the federated social network your Friendica node is part of. These numbers are not complete but only reflect the part of the network your node is aware of.'); - $hint = L10n::t('The Auto Discovered Contact Directory feature is not enabled, it will improve the data displayed here.'); - - // load the template, replace the macros and return the page content - $t = Renderer::getMarkupTemplate('admin/federation.tpl'); - return Renderer::replaceMacros($t, [ - '$title' => L10n::t('Administration'), - '$page' => L10n::t('Federation Statistics'), - '$intro' => $intro, - '$hint' => $hint, - '$autoactive' => Config::get('system', 'poco_completion'), - '$counts' => $counts, - '$version' => FRIENDICA_VERSION, - '$legendtext' => L10n::t('Currently this node is aware of %d nodes with %d registered users from the following platforms:', $total, $users), - '$baseurl' => System::baseUrl(), - ]); -} - -/** - * @brief Admin Inspect Queue Page - * - * Generates a page for the admin to have a look into the current queue of - * postings that are not deliverable. Shown are the name and url of the - * recipient, the delivery network and the dates when the posting was generated - * and the last time tried to deliver the posting. - * - * The returned string holds the content of the page. - * - * @param App $a - * @return string - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - */ -function admin_page_queue(App $a) -{ - // get content from the queue table - $entries = DBA::p("SELECT `contact`.`name`, `contact`.`nurl`, - `queue`.`id`, `queue`.`network`, `queue`.`created`, `queue`.`last` - FROM `queue` INNER JOIN `contact` ON `contact`.`id` = `queue`.`cid` - ORDER BY `queue`.`cid`, `queue`.`created`"); - - $r = []; - while ($entry = DBA::fetch($entries)) { - $entry['created'] = DateTimeFormat::local($entry['created']); - $entry['last'] = DateTimeFormat::local($entry['last']); - $r[] = $entry; - } - DBA::close($entries); - - $t = Renderer::getMarkupTemplate('admin/queue.tpl'); - return Renderer::replaceMacros($t, [ - '$title' => L10n::t('Administration'), - '$page' => L10n::t('Inspect Queue'), - '$count' => count($r), - 'id_header' => L10n::t('ID'), - '$to_header' => L10n::t('Recipient Name'), - '$url_header' => L10n::t('Recipient Profile'), - '$network_header' => L10n::t('Network'), - '$created_header' => L10n::t('Created'), - '$last_header' => L10n::t('Last Tried'), - '$info' => L10n::t('This page lists the content of the queue for outgoing postings. These are postings the initial delivery failed for. They will be resend later and eventually deleted if the delivery fails permanently.'), - '$entries' => $r, - ]); -} - -/** - * @brief Admin Inspect Worker Queue Page - * - * Generates a page for the admin to have a look into the current queue of - * worker jobs. Shown are the parameters for the job and its priority. - * - * The returned string holds the content of the page. - * - * @param App $a - * @param $deferred - * @return string - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - */ -function admin_page_workerqueue(App $a, $deferred) -{ - // get jobs from the workerqueue table - if ($deferred) { - $condition = ["NOT `done` AND `next_try` > ?", DateTimeFormat::utcNow()]; - $sub_title = L10n::t('Inspect Deferred Worker Queue'); - $info = L10n::t("This page lists the deferred worker jobs. This are jobs that couldn't be executed at the first time."); - } else { - $condition = ["NOT `done` AND `next_try` < ?", DateTimeFormat::utcNow()]; - $sub_title = L10n::t('Inspect Worker Queue'); - $info = L10n::t('This page lists the currently queued worker jobs. These jobs are handled by the worker cronjob you\'ve set up during install.'); - } - - $entries = DBA::select('workerqueue', ['id', 'parameter', 'created', 'priority'], $condition, ['order' => ['priority']]); - - $r = []; - while ($entry = DBA::fetch($entries)) { - // fix GH-5469. ref: src/Core/Worker.php:217 - $entry['parameter'] = Arrays::recursiveImplode(json_decode($entry['parameter'], true), ': '); - $entry['created'] = DateTimeFormat::local($entry['created']); - $r[] = $entry; - } - DBA::close($entries); - - $t = Renderer::getMarkupTemplate('admin/workerqueue.tpl'); - return Renderer::replaceMacros($t, [ - '$title' => L10n::t('Administration'), - '$page' => $sub_title, - '$count' => count($r), - '$id_header' => L10n::t('ID'), - '$param_header' => L10n::t('Job Parameters'), - '$created_header' => L10n::t('Created'), - '$prio_header' => L10n::t('Priority'), - '$info' => $info, - '$entries' => $r, - ]); -} - -/** - * @brief Admin Summary Page - * - * The summary page is the "start page" of the admin panel. It gives the admin - * a first overview of the open adminastrative tasks. - * - * The returned string contains the HTML content of the generated page. - * - * @param App $a - * @return string - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - */ -function admin_page_summary(App $a) -{ - // are there MyISAM tables in the DB? If so, trigger a warning message - $r = q("SELECT `engine` FROM `information_schema`.`tables` WHERE `engine` = 'myisam' AND `table_schema` = '%s' LIMIT 1", DBA::escape(DBA::databaseName())); - $showwarning = false; - $warningtext = []; - if (DBA::isResult($r)) { - $showwarning = true; - $warningtext[] = L10n::t('Your DB still runs with MyISAM tables. You should change the engine type to InnoDB. As Friendica will use InnoDB only features in the future, you should change this! See here for a guide that may be helpful converting the table engines. You may also use the command php bin/console.php dbstructure toinnodb of your Friendica installation for an automatic conversion.
', 'https://dev.mysql.com/doc/refman/5.7/en/converting-tables-to-innodb.html'); - } - // Check if github.com/friendica/master/VERSION is higher then - // the local version of Friendica. Check is opt-in, source may be master or devel branch - if (Config::get('system', 'check_new_version_url', 'none') != 'none') { - $gitversion = Config::get('system', 'git_friendica_version'); - if (version_compare(FRIENDICA_VERSION, $gitversion) < 0) { - $warningtext[] = L10n::t('There is a new version of Friendica available for download. Your current version is %1$s, upstream version is %2$s', FRIENDICA_VERSION, $gitversion); - $showwarning = true; - } - } - - if (Config::get('system', 'dbupdate', DBStructure::UPDATE_NOT_CHECKED) == DBStructure::UPDATE_NOT_CHECKED) { - DBStructure::update($a->getBasePath(), false, true); - } - if (Config::get('system', 'dbupdate') == DBStructure::UPDATE_FAILED) { - $showwarning = true; - $warningtext[] = L10n::t('The database update failed. Please run "php bin/console.php dbstructure update" from the command line and have a look at the errors that might appear.'); - } - - $last_worker_call = Config::get('system', 'last_worker_execution', false); - if (!$last_worker_call) { - $showwarning = true; - $warningtext[] = L10n::t('The worker was never executed. Please check your database structure!'); - } elseif ((strtotime(DateTimeFormat::utcNow()) - strtotime($last_worker_call)) > 60 * 60) { - $showwarning = true; - $warningtext[] = L10n::t('The last worker execution was on %s UTC. This is older than one hour. Please check your crontab settings.', $last_worker_call); - } - - // Legacy config file warning - if (file_exists('.htconfig.php')) { - $showwarning = true; - $warningtext[] = L10n::t('Friendica\'s configuration now is stored in config/local.config.php, please copy config/local-sample.config.php and move your config from .htconfig.php. See the Config help page for help with the transition.', $a->getBaseURL() . '/help/Config'); - } - if (file_exists('config/local.ini.php')) { - $showwarning = true; - $warningtext[] = L10n::t('Friendica\'s configuration now is stored in config/local.config.php, please copy config/local-sample.config.php and move your config from config/local.ini.php. See the Config help page for help with the transition.', $a->getBaseURL() . '/help/Config'); - } - - // Check server vitality - if (!admin_page_server_vital()) { - $showwarning = true; - $well_known = $a->getBaseURL() . '/.well-known/host-meta'; - $warningtext[] = L10n::t('%s is not reachable on your system. This is a severe configuration issue that prevents server to server communication. See the installation page for help.', - $well_known, $well_known, $a->getBaseURL() . '/help/Install'); - } - - $r = q("SELECT `page-flags`, COUNT(`uid`) AS `count` FROM `user` GROUP BY `page-flags`"); - $accounts = [ - [L10n::t('Normal Account'), 0], - [L10n::t('Automatic Follower Account'), 0], - [L10n::t('Public Forum Account'), 0], - [L10n::t('Automatic Friend Account'), 0], - [L10n::t('Blog Account'), 0], - [L10n::t('Private Forum Account'), 0] - ]; - - $users = 0; - foreach ($r as $u) { - $accounts[$u['page-flags']][1] = $u['count']; - $users += $u['count']; - } - - Logger::log('accounts: ' . print_r($accounts, true), Logger::DATA); - - $pending = Register::getPendingCount(); - - $queue = DBA::count('queue', []); - - $deferred = DBA::count('workerqueue', ["`executed` <= ? AND NOT `done` AND `next_try` > ?", - DBA::NULL_DATETIME, DateTimeFormat::utcNow()]); - - $workerqueue = DBA::count('workerqueue', ["`executed` <= ? AND NOT `done` AND `next_try` < ?", - DBA::NULL_DATETIME, DateTimeFormat::utcNow()]); - - // We can do better, but this is a quick queue status - - $queues = ['label' => L10n::t('Message queues'), 'queue' => $queue, 'deferred' => $deferred, 'workerq' => $workerqueue]; - - - $r = q("SHOW variables LIKE 'max_allowed_packet'"); - $max_allowed_packet = (($r) ? $r[0]['Value'] : 0); - - $server_settings = ['label' => L10n::t('Server Settings'), - 'php' => ['upload_max_filesize' => ini_get('upload_max_filesize'), - 'post_max_size' => ini_get('post_max_size'), - 'memory_limit' => ini_get('memory_limit')], - 'mysql' => ['max_allowed_packet' => $max_allowed_packet]]; - - $t = Renderer::getMarkupTemplate('admin/summary.tpl'); - return Renderer::replaceMacros($t, [ - '$title' => L10n::t('Administration'), - '$page' => L10n::t('Summary'), - '$queues' => $queues, - '$users' => [L10n::t('Registered users'), $users], - '$accounts' => $accounts, - '$pending' => [L10n::t('Pending registrations'), $pending], - '$version' => [L10n::t('Version'), FRIENDICA_VERSION], - '$baseurl' => System::baseUrl(), - '$platform' => FRIENDICA_PLATFORM, - '$codename' => FRIENDICA_CODENAME, - '$build' => Config::get('system', 'build'), - '$addons' => [L10n::t('Active addons'), Addon::getEnabledList()], - '$serversettings' => $server_settings, - '$showwarning' => $showwarning, - '$warningtext' => $warningtext - ]); -} - -/** - * @brief Process send data from Admin Site Page - * - * @param App $a - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - */ -function admin_page_site_post(App $a) -{ - BaseModule::checkFormSecurityTokenRedirectOnError('/admin/site', 'admin_site'); - - if (!empty($_POST['republish_directory'])) { - Worker::add(PRIORITY_LOW, 'Directory'); - return; - } - - if (empty($_POST['page_site'])) { - return; - } - - // relocate - if (!empty($_POST['relocate']) && !empty($_POST['relocate_url']) && $_POST['relocate_url'] != "") { - $new_url = $_POST['relocate_url']; - $new_url = rtrim($new_url, "/"); - - $parsed = @parse_url($new_url); - if (!is_array($parsed) || empty($parsed['host']) || empty($parsed['scheme'])) { - notice(L10n::t("Can not parse base url. Must have at least ://")); - $a->internalRedirect('admin/site'); - } - - /* steps: - * replace all "baseurl" to "new_url" in config, profile, term, items and contacts - * send relocate for every local user - * */ - - $old_url = $a->getBaseURL(true); - - // Generate host names for relocation the addresses in the format user@address.tld - $new_host = str_replace("http://", "@", Strings::normaliseLink($new_url)); - $old_host = str_replace("http://", "@", Strings::normaliseLink($old_url)); - - function update_table(App $a, $table_name, $fields, $old_url, $new_url) - { - $dbold = DBA::escape($old_url); - $dbnew = DBA::escape($new_url); - - $upd = []; - foreach ($fields as $f) { - $upd[] = "`$f` = REPLACE(`$f`, '$dbold', '$dbnew')"; - } - - $upds = implode(", ", $upd); - - $r = q("UPDATE %s SET %s;", $table_name, $upds); - - if (!DBA::isResult($r)) { - notice("Failed updating '$table_name': " . DBA::errorMessage()); - $a->internalRedirect('admin/site'); - } - } - - // update tables - // update profile links in the format "http://server.tld" - update_table($a, "profile", ['photo', 'thumb'], $old_url, $new_url); - update_table($a, "term", ['url'], $old_url, $new_url); - update_table($a, "contact", ['photo', 'thumb', 'micro', 'url', 'nurl', 'alias', 'request', 'notify', 'poll', 'confirm', 'poco', 'avatar'], $old_url, $new_url); - update_table($a, "gcontact", ['url', 'nurl', 'photo', 'server_url', 'notify', 'alias'], $old_url, $new_url); - update_table($a, "item", ['owner-link', 'author-link', 'body', 'plink', 'tag'], $old_url, $new_url); - - // update profile addresses in the format "user@server.tld" - update_table($a, "contact", ['addr'], $old_host, $new_host); - update_table($a, "gcontact", ['connect', 'addr'], $old_host, $new_host); - - // update config - Config::set('system', 'hostname', parse_url($new_url, PHP_URL_HOST)); - Config::set('system', 'url', $new_url); - $a->setBaseURL($new_url); - - // send relocate - $users = q("SELECT `uid` FROM `user` WHERE `account_removed` = 0 AND `account_expired` = 0"); - - foreach ($users as $user) { - Worker::add(PRIORITY_HIGH, 'Notifier', 'relocate', $user['uid']); - } - - info("Relocation started. Could take a while to complete."); - - $a->internalRedirect('admin/site'); - } - // end relocate - - $sitename = (!empty($_POST['sitename']) ? Strings::escapeTags(trim($_POST['sitename'])) : ''); - $hostname = (!empty($_POST['hostname']) ? Strings::escapeTags(trim($_POST['hostname'])) : ''); - $sender_email = (!empty($_POST['sender_email']) ? Strings::escapeTags(trim($_POST['sender_email'])) : ''); - $banner = (!empty($_POST['banner']) ? trim($_POST['banner']) : false); - $shortcut_icon = (!empty($_POST['shortcut_icon']) ? Strings::escapeTags(trim($_POST['shortcut_icon'])) : ''); - $touch_icon = (!empty($_POST['touch_icon']) ? Strings::escapeTags(trim($_POST['touch_icon'])) : ''); - $additional_info = (!empty($_POST['additional_info']) ? trim($_POST['additional_info']) : ''); - $language = (!empty($_POST['language']) ? Strings::escapeTags(trim($_POST['language'])) : ''); - $theme = (!empty($_POST['theme']) ? Strings::escapeTags(trim($_POST['theme'])) : ''); - $theme_mobile = (!empty($_POST['theme_mobile']) ? Strings::escapeTags(trim($_POST['theme_mobile'])) : ''); - $maximagesize = (!empty($_POST['maximagesize']) ? intval(trim($_POST['maximagesize'])) : 0); - $maximagelength = (!empty($_POST['maximagelength']) ? intval(trim($_POST['maximagelength'])) : MAX_IMAGE_LENGTH); - $jpegimagequality = (!empty($_POST['jpegimagequality']) ? intval(trim($_POST['jpegimagequality'])) : JPEG_QUALITY); - - $register_policy = (!empty($_POST['register_policy']) ? intval(trim($_POST['register_policy'])) : 0); - $daily_registrations = (!empty($_POST['max_daily_registrations']) ? intval(trim($_POST['max_daily_registrations'])) : 0); - $abandon_days = (!empty($_POST['abandon_days']) ? intval(trim($_POST['abandon_days'])) : 0); - - $register_text = (!empty($_POST['register_text']) ? strip_tags(trim($_POST['register_text'])) : ''); - - $allowed_sites = (!empty($_POST['allowed_sites']) ? Strings::escapeTags(trim($_POST['allowed_sites'])) : ''); - $allowed_email = (!empty($_POST['allowed_email']) ? Strings::escapeTags(trim($_POST['allowed_email'])) : ''); - $forbidden_nicknames = (!empty($_POST['forbidden_nicknames']) ? strtolower(Strings::escapeTags(trim($_POST['forbidden_nicknames']))) : ''); - $no_oembed_rich_content = !empty($_POST['no_oembed_rich_content']); - $allowed_oembed = (!empty($_POST['allowed_oembed']) ? Strings::escapeTags(trim($_POST['allowed_oembed'])) : ''); - $block_public = !empty($_POST['block_public']); - $force_publish = !empty($_POST['publish_all']); - $global_directory = (!empty($_POST['directory']) ? Strings::escapeTags(trim($_POST['directory'])) : ''); - $newuser_private = !empty($_POST['newuser_private']); - $enotify_no_content = !empty($_POST['enotify_no_content']); - $private_addons = !empty($_POST['private_addons']); - $disable_embedded = !empty($_POST['disable_embedded']); - $allow_users_remote_self = !empty($_POST['allow_users_remote_self']); - $explicit_content = !empty($_POST['explicit_content']); - - $no_multi_reg = !empty($_POST['no_multi_reg']); - $no_openid = !empty($_POST['no_openid']); - $no_regfullname = !empty($_POST['no_regfullname']); - $community_page_style = (!empty($_POST['community_page_style']) ? intval(trim($_POST['community_page_style'])) : 0); - $max_author_posts_community_page = (!empty($_POST['max_author_posts_community_page']) ? intval(trim($_POST['max_author_posts_community_page'])) : 0); - - $verifyssl = !empty($_POST['verifyssl']); - $proxyuser = (!empty($_POST['proxyuser']) ? Strings::escapeTags(trim($_POST['proxyuser'])) : ''); - $proxy = (!empty($_POST['proxy']) ? Strings::escapeTags(trim($_POST['proxy'])) : ''); - $timeout = (!empty($_POST['timeout']) ? intval(trim($_POST['timeout'])) : 60); - $maxloadavg = (!empty($_POST['maxloadavg']) ? intval(trim($_POST['maxloadavg'])) : 20); - $maxloadavg_frontend = (!empty($_POST['maxloadavg_frontend']) ? intval(trim($_POST['maxloadavg_frontend'])) : 50); - $min_memory = (!empty($_POST['min_memory']) ? intval(trim($_POST['min_memory'])) : 0); - $optimize_max_tablesize = (!empty($_POST['optimize_max_tablesize']) ? intval(trim($_POST['optimize_max_tablesize'])) : 100); - $optimize_fragmentation = (!empty($_POST['optimize_fragmentation']) ? intval(trim($_POST['optimize_fragmentation'])) : 30); - $poco_completion = (!empty($_POST['poco_completion']) ? intval(trim($_POST['poco_completion'])) : false); - $poco_requery_days = (!empty($_POST['poco_requery_days']) ? intval(trim($_POST['poco_requery_days'])) : 7); - $poco_discovery = (!empty($_POST['poco_discovery']) ? intval(trim($_POST['poco_discovery'])) : PortableContact::DISABLED); - $poco_discovery_since = (!empty($_POST['poco_discovery_since']) ? intval(trim($_POST['poco_discovery_since'])) : 30); - $poco_local_search = !empty($_POST['poco_local_search']); - $nodeinfo = !empty($_POST['nodeinfo']); - $dfrn_only = !empty($_POST['dfrn_only']); - $ostatus_disabled = !empty($_POST['ostatus_disabled']); - $ostatus_full_threads = !empty($_POST['ostatus_full_threads']); - $diaspora_enabled = !empty($_POST['diaspora_enabled']); - $ssl_policy = (!empty($_POST['ssl_policy']) ? intval($_POST['ssl_policy']) : 0); - $force_ssl = !empty($_POST['force_ssl']); - $hide_help = !empty($_POST['hide_help']); - $dbclean = !empty($_POST['dbclean']); - $dbclean_expire_days = (!empty($_POST['dbclean_expire_days']) ? intval($_POST['dbclean_expire_days']) : 0); - $dbclean_unclaimed = (!empty($_POST['dbclean_unclaimed']) ? intval($_POST['dbclean_unclaimed']) : 0); - $dbclean_expire_conv = (!empty($_POST['dbclean_expire_conv']) ? intval($_POST['dbclean_expire_conv']) : 0); - $suppress_tags = !empty($_POST['suppress_tags']); - $itemcache = (!empty($_POST['itemcache']) ? Strings::escapeTags(trim($_POST['itemcache'])) : ''); - $itemcache_duration = (!empty($_POST['itemcache_duration']) ? intval($_POST['itemcache_duration']) : 0); - $max_comments = (!empty($_POST['max_comments']) ? intval($_POST['max_comments']) : 0); - $temppath = (!empty($_POST['temppath']) ? Strings::escapeTags(trim($_POST['temppath'])) : ''); - $basepath = (!empty($_POST['basepath']) ? Strings::escapeTags(trim($_POST['basepath'])) : ''); - $singleuser = (!empty($_POST['singleuser']) ? Strings::escapeTags(trim($_POST['singleuser'])) : ''); - $proxy_disabled = !empty($_POST['proxy_disabled']); - $only_tag_search = !empty($_POST['only_tag_search']); - $rino = (!empty($_POST['rino']) ? intval($_POST['rino']) : 0); - $check_new_version_url = (!empty($_POST['check_new_version_url']) ? Strings::escapeTags(trim($_POST['check_new_version_url'])) : 'none'); - - $worker_queues = (!empty($_POST['worker_queues']) ? intval($_POST['worker_queues']) : 10); - $worker_dont_fork = !empty($_POST['worker_dont_fork']); - $worker_fastlane = !empty($_POST['worker_fastlane']); - $worker_frontend = !empty($_POST['worker_frontend']); - - $relay_directly = !empty($_POST['relay_directly']); - $relay_server = (!empty($_POST['relay_server']) ? Strings::escapeTags(trim($_POST['relay_server'])) : ''); - $relay_subscribe = !empty($_POST['relay_subscribe']); - $relay_scope = (!empty($_POST['relay_scope']) ? Strings::escapeTags(trim($_POST['relay_scope'])) : ''); - $relay_server_tags = (!empty($_POST['relay_server_tags']) ? Strings::escapeTags(trim($_POST['relay_server_tags'])) : ''); - $relay_user_tags = !empty($_POST['relay_user_tags']); - $active_panel = (!empty($_POST['active_panel']) ? "#" . Strings::escapeTags(trim($_POST['active_panel'])) : ''); - - /** - * @var $storagebackend \Friendica\Model\Storage\IStorage - */ - $storagebackend = Strings::escapeTags(trim(defaults($_POST, 'storagebackend', ''))); - if (!StorageManager::setBackend($storagebackend)) { - info(L10n::t('Invalid storage backend setting value.')); - } - - // save storage backend form - if (!is_null($storagebackend) && $storagebackend != "") { - $storage_opts = $storagebackend::getOptions(); - $storage_form_prefix=preg_replace('|[^a-zA-Z0-9]|' ,'', $storagebackend); - $storage_opts_data = []; - foreach($storage_opts as $name => $info) { - $fieldname = $storage_form_prefix . '_' . $name; - switch ($info[0]) { // type - case 'checkbox': - case 'yesno': - $value = !empty($_POST[$fieldname]); - break; - default: - $value = defaults($_POST, $fieldname, ''); - } - $storage_opts_data[$name] = $value; - } - unset($name); - unset($info); - - $storage_form_errors = $storagebackend::saveOptions($storage_opts_data); - if (count($storage_form_errors)) { - foreach($storage_form_errors as $name => $err) { - notice('Storage backend, ' . $storage_opts[$name][1] . ': ' . $err); - } - $a->internalRedirect('admin/site' . $active_panel); - } - } - - - - // Has the directory url changed? If yes, then resubmit the existing profiles there - if ($global_directory != Config::get('system', 'directory') && ($global_directory != '')) { - Config::set('system', 'directory', $global_directory); - Worker::add(PRIORITY_LOW, 'Directory'); - } - - if ($a->getURLPath() != "") { - $diaspora_enabled = false; - } - if ($ssl_policy != intval(Config::get('system', 'ssl_policy'))) { - if ($ssl_policy == SSL_POLICY_FULL) { - q("UPDATE `contact` SET - `url` = REPLACE(`url` , 'http:' , 'https:'), - `photo` = REPLACE(`photo` , 'http:' , 'https:'), - `thumb` = REPLACE(`thumb` , 'http:' , 'https:'), - `micro` = REPLACE(`micro` , 'http:' , 'https:'), - `request` = REPLACE(`request`, 'http:' , 'https:'), - `notify` = REPLACE(`notify` , 'http:' , 'https:'), - `poll` = REPLACE(`poll` , 'http:' , 'https:'), - `confirm` = REPLACE(`confirm`, 'http:' , 'https:'), - `poco` = REPLACE(`poco` , 'http:' , 'https:') - WHERE `self` = 1" - ); - q("UPDATE `profile` SET - `photo` = REPLACE(`photo` , 'http:' , 'https:'), - `thumb` = REPLACE(`thumb` , 'http:' , 'https:') - WHERE 1 " - ); - } elseif ($ssl_policy == SSL_POLICY_SELFSIGN) { - q("UPDATE `contact` SET - `url` = REPLACE(`url` , 'https:' , 'http:'), - `photo` = REPLACE(`photo` , 'https:' , 'http:'), - `thumb` = REPLACE(`thumb` , 'https:' , 'http:'), - `micro` = REPLACE(`micro` , 'https:' , 'http:'), - `request` = REPLACE(`request`, 'https:' , 'http:'), - `notify` = REPLACE(`notify` , 'https:' , 'http:'), - `poll` = REPLACE(`poll` , 'https:' , 'http:'), - `confirm` = REPLACE(`confirm`, 'https:' , 'http:'), - `poco` = REPLACE(`poco` , 'https:' , 'http:') - WHERE `self` = 1" - ); - q("UPDATE `profile` SET - `photo` = REPLACE(`photo` , 'https:' , 'http:'), - `thumb` = REPLACE(`thumb` , 'https:' , 'http:') - WHERE 1 " - ); - } - } - Config::set('system', 'ssl_policy' , $ssl_policy); - Config::set('system', 'maxloadavg' , $maxloadavg); - Config::set('system', 'maxloadavg_frontend' , $maxloadavg_frontend); - Config::set('system', 'min_memory' , $min_memory); - Config::set('system', 'optimize_max_tablesize', $optimize_max_tablesize); - Config::set('system', 'optimize_fragmentation', $optimize_fragmentation); - Config::set('system', 'poco_completion' , $poco_completion); - Config::set('system', 'poco_requery_days' , $poco_requery_days); - Config::set('system', 'poco_discovery' , $poco_discovery); - Config::set('system', 'poco_discovery_since' , $poco_discovery_since); - Config::set('system', 'poco_local_search' , $poco_local_search); - Config::set('system', 'nodeinfo' , $nodeinfo); - Config::set('config', 'sitename' , $sitename); - Config::set('config', 'hostname' , $hostname); - Config::set('config', 'sender_email' , $sender_email); - Config::set('system', 'suppress_tags' , $suppress_tags); - Config::set('system', 'shortcut_icon' , $shortcut_icon); - Config::set('system', 'touch_icon' , $touch_icon); - - if ($banner == "") { - Config::delete('system', 'banner'); - } else { - Config::set('system', 'banner', $banner); - } - - if (empty($additional_info)) { - Config::delete('config', 'info'); - } else { - Config::set('config', 'info', $additional_info); - } - Config::set('system', 'language', $language); - Config::set('system', 'theme', $theme); - Theme::install($theme); - - if ($theme_mobile == '---') { - Config::delete('system', 'mobile-theme'); - } else { - Config::set('system', 'mobile-theme', $theme_mobile); - } - if ($singleuser == '---') { - Config::delete('system', 'singleuser'); - } else { - Config::set('system', 'singleuser', $singleuser); - } - Config::set('system', 'maximagesize' , $maximagesize); - Config::set('system', 'max_image_length' , $maximagelength); - Config::set('system', 'jpeg_quality' , $jpegimagequality); - - Config::set('config', 'register_policy' , $register_policy); - Config::set('system', 'max_daily_registrations', $daily_registrations); - Config::set('system', 'account_abandon_days' , $abandon_days); - Config::set('config', 'register_text' , $register_text); - Config::set('system', 'allowed_sites' , $allowed_sites); - Config::set('system', 'allowed_email' , $allowed_email); - Config::set('system', 'forbidden_nicknames' , $forbidden_nicknames); - Config::set('system', 'no_oembed_rich_content' , $no_oembed_rich_content); - Config::set('system', 'allowed_oembed' , $allowed_oembed); - Config::set('system', 'block_public' , $block_public); - Config::set('system', 'publish_all' , $force_publish); - Config::set('system', 'newuser_private' , $newuser_private); - Config::set('system', 'enotify_no_content' , $enotify_no_content); - Config::set('system', 'disable_embedded' , $disable_embedded); - Config::set('system', 'allow_users_remote_self', $allow_users_remote_self); - Config::set('system', 'explicit_content' , $explicit_content); - Config::set('system', 'check_new_version_url' , $check_new_version_url); - - Config::set('system', 'block_extended_register', $no_multi_reg); - Config::set('system', 'no_openid' , $no_openid); - Config::set('system', 'no_regfullname' , $no_regfullname); - Config::set('system', 'community_page_style' , $community_page_style); - Config::set('system', 'max_author_posts_community_page', $max_author_posts_community_page); - Config::set('system', 'verifyssl' , $verifyssl); - Config::set('system', 'proxyuser' , $proxyuser); - Config::set('system', 'proxy' , $proxy); - Config::set('system', 'curl_timeout' , $timeout); - Config::set('system', 'dfrn_only' , $dfrn_only); - Config::set('system', 'ostatus_disabled' , $ostatus_disabled); - Config::set('system', 'ostatus_full_threads' , $ostatus_full_threads); - Config::set('system', 'diaspora_enabled' , $diaspora_enabled); - - Config::set('config', 'private_addons' , $private_addons); - - Config::set('system', 'force_ssl' , $force_ssl); - Config::set('system', 'hide_help' , $hide_help); - - Config::set('system', 'dbclean' , $dbclean); - Config::set('system', 'dbclean-expire-days' , $dbclean_expire_days); - Config::set('system', 'dbclean_expire_conversation', $dbclean_expire_conv); - - if ($dbclean_unclaimed == 0) { - $dbclean_unclaimed = $dbclean_expire_days; - } - - Config::set('system', 'dbclean-expire-unclaimed', $dbclean_unclaimed); - - if ($itemcache != '') { - $itemcache = BasePath::getRealPath($itemcache); - } - - Config::set('system', 'itemcache', $itemcache); - Config::set('system', 'itemcache_duration', $itemcache_duration); - Config::set('system', 'max_comments', $max_comments); - - if ($temppath != '') { - $temppath = BasePath::getRealPath($temppath); - } - - Config::set('system', 'temppath', $temppath); - - if ($basepath != '') { - $basepath = BasePath::getRealPath($basepath); - } - - Config::set('system', 'basepath' , $basepath); - Config::set('system', 'proxy_disabled' , $proxy_disabled); - Config::set('system', 'only_tag_search' , $only_tag_search); - - Config::set('system', 'worker_queues' , $worker_queues); - Config::set('system', 'worker_dont_fork' , $worker_dont_fork); - Config::set('system', 'worker_fastlane' , $worker_fastlane); - Config::set('system', 'frontend_worker' , $worker_frontend); - - Config::set('system', 'relay_directly' , $relay_directly); - Config::set('system', 'relay_server' , $relay_server); - Config::set('system', 'relay_subscribe' , $relay_subscribe); - Config::set('system', 'relay_scope' , $relay_scope); - Config::set('system', 'relay_server_tags', $relay_server_tags); - Config::set('system', 'relay_user_tags' , $relay_user_tags); - - Config::set('system', 'rino_encrypt' , $rino); - - info(L10n::t('Site settings updated.') . EOL); - - $a->internalRedirect('admin/site' . $active_panel); - return; // NOTREACHED -} - -/** - * @brief Generate Admin Site subpage - * - * This function generates the main configuration page of the admin panel. - * - * @param App $a - * @return string - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - */ -function admin_page_site(App $a) -{ - /* Installed langs */ - $lang_choices = L10n::getAvailableLanguages(); - - if (strlen(Config::get('system', 'directory_submit_url')) && - !strlen(Config::get('system', 'directory'))) { - Config::set('system', 'directory', dirname(Config::get('system', 'directory_submit_url'))); - Config::delete('system', 'directory_submit_url'); - } - - /* Installed themes */ - $theme_choices = []; - $theme_choices_mobile = []; - $theme_choices_mobile["---"] = L10n::t("No special theme for mobile devices"); - $files = glob('view/theme/*'); - if (is_array($files)) { - $allowed_theme_list = Config::get('system', 'allowed_themes'); - - foreach ($files as $file) { - if (intval(file_exists($file . '/unsupported'))) { - continue; - } - - $f = basename($file); - - // Only show allowed themes here - if (($allowed_theme_list != '') && !strstr($allowed_theme_list, $f)) { - continue; - } - - $theme_name = ((file_exists($file . '/experimental')) ? sprintf("%s - \x28Experimental\x29", $f) : $f); - - if (file_exists($file . '/mobile')) { - $theme_choices_mobile[$f] = $theme_name; - } else { - $theme_choices[$f] = $theme_name; - } - } - } - - /* Community page style */ - $community_page_style_choices = [ - CP_NO_INTERNAL_COMMUNITY => L10n::t("No community page for local users"), - CP_NO_COMMUNITY_PAGE => L10n::t("No community page"), - CP_USERS_ON_SERVER => L10n::t("Public postings from users of this site"), - CP_GLOBAL_COMMUNITY => L10n::t("Public postings from the federated network"), - CP_USERS_AND_GLOBAL => L10n::t("Public postings from local users and the federated network") - ]; - - $poco_discovery_choices = [ - PortableContact::DISABLED => L10n::t("Disabled"), - PortableContact::USERS => L10n::t("Users"), - PortableContact::USERS_GCONTACTS => L10n::t("Users, Global Contacts"), - PortableContact::USERS_GCONTACTS_FALLBACK => L10n::t("Users, Global Contacts/fallback"), - ]; - - $poco_discovery_since_choices = [ - "30" => L10n::t("One month"), - "91" => L10n::t("Three months"), - "182" => L10n::t("Half a year"), - "365" => L10n::t("One year"), - ]; - - /* get user names to make the install a personal install of X */ - $user_names = []; - $user_names['---'] = L10n::t('Multi user instance'); - $users = q("SELECT `username`, `nickname` FROM `user`"); - - foreach ($users as $user) { - $user_names[$user['nickname']] = $user['username']; - } - - /* Banner */ - $banner = Config::get('system', 'banner'); - - if ($banner == false) { - $banner = 'logoFriendica'; - } - - $additional_info = Config::get('config', 'info'); - - // Automatically create temporary paths - get_temppath(); - get_itemcachepath(); - - //echo "
"; var_dump($lang_choices); die("
"); - - /* Register policy */ - $register_choices = [ - Module\Register::CLOSED => L10n::t("Closed"), - Module\Register::APPROVE => L10n::t("Requires approval"), - Module\Register::OPEN => L10n::t("Open") - ]; - - $ssl_choices = [ - SSL_POLICY_NONE => L10n::t("No SSL policy, links will track page SSL state"), - SSL_POLICY_FULL => L10n::t("Force all links to use SSL"), - SSL_POLICY_SELFSIGN => L10n::t("Self-signed certificate, use SSL for local links only \x28discouraged\x29") - ]; - - $check_git_version_choices = [ - "none" => L10n::t("Don't check"), - "master" => L10n::t("check the stable version"), - "develop" => L10n::t("check the development version") - ]; - - if (empty(Config::get('config', 'hostname'))) { - Config::set('config', 'hostname', $a->getHostName()); - } - $diaspora_able = ($a->getURLPath() == ""); - - $optimize_max_tablesize = Config::get('system', 'optimize_max_tablesize', -1); - - if ($optimize_max_tablesize <= 0) { - $optimize_max_tablesize = -1; - } - - /* storage backend */ - $storage_backends = StorageManager::listBackends(); - /** - * @var $storage_current_backend \Friendica\Model\Storage\IStorage - */ - $storage_current_backend = StorageManager::getBackend(); - - $storage_backends_choices = [ - '' => L10n::t('Database (legacy)') - ]; - foreach($storage_backends as $name=>$class) { - $storage_backends_choices[$class] = $name; - } - unset($storage_backends); - - // build storage config form, - $storage_form_prefix=preg_replace('|[^a-zA-Z0-9]|' ,'', $storage_current_backend); - - $storage_form = []; - if (!is_null($storage_current_backend) && $storage_current_backend != "") { - foreach ($storage_current_backend::getOptions() as $name => $info) { - $type = $info[0]; - $info[0] = $storage_form_prefix . '_' . $name; - $info['type'] = $type; - $info['field'] = 'field_' . $type . '.tpl'; - $storage_form[$name] = $info; - } - } - - - $t = Renderer::getMarkupTemplate('admin/site.tpl'); - return Renderer::replaceMacros($t, [ - '$title' => L10n::t('Administration'), - '$page' => L10n::t('Site'), - '$submit' => L10n::t('Save Settings'), - '$republish' => L10n::t('Republish users to directory'), - '$registration' => L10n::t('Registration'), - '$upload' => L10n::t('File upload'), - '$corporate' => L10n::t('Policies'), - '$advanced' => L10n::t('Advanced'), - '$portable_contacts' => L10n::t('Auto Discovered Contact Directory'), - '$performance' => L10n::t('Performance'), - '$worker_title' => L10n::t('Worker'), - '$relay_title' => L10n::t('Message Relay'), - '$relocate' => L10n::t('Relocate Instance'), - '$relocate_warning' => L10n::t('Warning! Advanced function. Could make this server unreachable.'), - '$baseurl' => System::baseUrl(true), - - // name, label, value, help string, extra data... - '$sitename' => ['sitename', L10n::t("Site name"), Config::get('config', 'sitename'), ''], - '$hostname' => ['hostname', L10n::t("Host name"), Config::get('config', 'hostname'), ""], - '$sender_email' => ['sender_email', L10n::t("Sender Email"), Config::get('config', 'sender_email'), L10n::t("The email address your server shall use to send notification emails from."), "", "", "email"], - '$banner' => ['banner', L10n::t("Banner/Logo"), $banner, ""], - '$shortcut_icon' => ['shortcut_icon', L10n::t("Shortcut icon"), Config::get('system', 'shortcut_icon'), L10n::t("Link to an icon that will be used for browsers.")], - '$touch_icon' => ['touch_icon', L10n::t("Touch icon"), Config::get('system', 'touch_icon'), L10n::t("Link to an icon that will be used for tablets and mobiles.")], - '$additional_info' => ['additional_info', L10n::t('Additional Info'), $additional_info, L10n::t('For public servers: you can add additional information here that will be listed at %s/servers.', get_server())], - '$language' => ['language', L10n::t("System language"), Config::get('system', 'language'), "", $lang_choices], - '$theme' => ['theme', L10n::t("System theme"), Config::get('system', 'theme'), L10n::t("Default system theme - may be over-ridden by user profiles - change theme settings"), $theme_choices], - '$theme_mobile' => ['theme_mobile', L10n::t("Mobile system theme"), Config::get('system', 'mobile-theme', '---'), L10n::t("Theme for mobile devices"), $theme_choices_mobile], - '$ssl_policy' => ['ssl_policy', L10n::t("SSL link policy"), (string)intval(Config::get('system', 'ssl_policy')), L10n::t("Determines whether generated links should be forced to use SSL"), $ssl_choices], - '$force_ssl' => ['force_ssl', L10n::t("Force SSL"), Config::get('system', 'force_ssl'), L10n::t("Force all Non-SSL requests to SSL - Attention: on some systems it could lead to endless loops.")], - '$hide_help' => ['hide_help', L10n::t("Hide help entry from navigation menu"), Config::get('system', 'hide_help'), L10n::t("Hides the menu entry for the Help pages from the navigation menu. You can still access it calling /help directly.")], - '$singleuser' => ['singleuser', L10n::t("Single user instance"), Config::get('system', 'singleuser', '---'), L10n::t("Make this instance multi-user or single-user for the named user"), $user_names], - - '$storagebackend' => ['storagebackend', L10n::t("File storage backend"), $storage_current_backend, L10n::t('The backend used to store uploaded data. If you change the storage backend, you can manually move the existing files. If you do not do so, the files uploaded before the change will still be available at the old backend. Please see the settings documentation for more information about the choices and the moving procedure.'), $storage_backends_choices], - '$storageform' => $storage_form, - '$maximagesize' => ['maximagesize', L10n::t("Maximum image size"), Config::get('system', 'maximagesize'), L10n::t("Maximum size in bytes of uploaded images. Default is 0, which means no limits.")], - '$maximagelength' => ['maximagelength', L10n::t("Maximum image length"), Config::get('system', 'max_image_length'), L10n::t("Maximum length in pixels of the longest side of uploaded images. Default is -1, which means no limits.")], - '$jpegimagequality' => ['jpegimagequality', L10n::t("JPEG image quality"), Config::get('system', 'jpeg_quality'), L10n::t("Uploaded JPEGS will be saved at this quality setting [0-100]. Default is 100, which is full quality.")], - - '$register_policy' => ['register_policy', L10n::t("Register policy"), Config::get('config', 'register_policy'), "", $register_choices], - '$daily_registrations' => ['max_daily_registrations', L10n::t("Maximum Daily Registrations"), Config::get('system', 'max_daily_registrations'), L10n::t("If registration is permitted above, this sets the maximum number of new user registrations to accept per day. If register is set to closed, this setting has no effect.")], - '$register_text' => ['register_text', L10n::t("Register text"), Config::get('config', 'register_text'), L10n::t("Will be displayed prominently on the registration page. You can use BBCode here.")], - '$forbidden_nicknames' => ['forbidden_nicknames', L10n::t('Forbidden Nicknames'), Config::get('system', 'forbidden_nicknames'), L10n::t('Comma separated list of nicknames that are forbidden from registration. Preset is a list of role names according RFC 2142.')], - '$abandon_days' => ['abandon_days', L10n::t('Accounts abandoned after x days'), Config::get('system', 'account_abandon_days'), L10n::t('Will not waste system resources polling external sites for abandonded accounts. Enter 0 for no time limit.')], - '$allowed_sites' => ['allowed_sites', L10n::t("Allowed friend domains"), Config::get('system', 'allowed_sites'), L10n::t("Comma separated list of domains which are allowed to establish friendships with this site. Wildcards are accepted. Empty to allow any domains")], - '$allowed_email' => ['allowed_email', L10n::t("Allowed email domains"), Config::get('system', 'allowed_email'), L10n::t("Comma separated list of domains which are allowed in email addresses for registrations to this site. Wildcards are accepted. Empty to allow any domains")], - '$no_oembed_rich_content' => ['no_oembed_rich_content', L10n::t("No OEmbed rich content"), Config::get('system', 'no_oembed_rich_content'), L10n::t("Don't show the rich content \x28e.g. embedded PDF\x29, except from the domains listed below.")], - '$allowed_oembed' => ['allowed_oembed', L10n::t("Allowed OEmbed domains"), Config::get('system', 'allowed_oembed'), L10n::t("Comma separated list of domains which oembed content is allowed to be displayed. Wildcards are accepted.")], - '$block_public' => ['block_public', L10n::t("Block public"), Config::get('system', 'block_public'), L10n::t("Check to block public access to all otherwise public personal pages on this site unless you are currently logged in.")], - '$force_publish' => ['publish_all', L10n::t("Force publish"), Config::get('system', 'publish_all'), L10n::t("Check to force all profiles on this site to be listed in the site directory.") . '' . L10n::t('Enabling this may violate privacy laws like the GDPR') . ''], - '$global_directory' => ['directory', L10n::t("Global directory URL"), Config::get('system', 'directory', 'https://dir.friendica.social'), L10n::t("URL to the global directory. If this is not set, the global directory is completely unavailable to the application.")], - '$newuser_private' => ['newuser_private', L10n::t("Private posts by default for new users"), Config::get('system', 'newuser_private'), L10n::t("Set default post permissions for all new members to the default privacy group rather than public.")], - '$enotify_no_content' => ['enotify_no_content', L10n::t("Don't include post content in email notifications"), Config::get('system', 'enotify_no_content'), L10n::t("Don't include the content of a post/comment/private message/etc. in the email notifications that are sent out from this site, as a privacy measure.")], - '$private_addons' => ['private_addons', L10n::t("Disallow public access to addons listed in the apps menu."), Config::get('config', 'private_addons'), L10n::t("Checking this box will restrict addons listed in the apps menu to members only.")], - '$disable_embedded' => ['disable_embedded', L10n::t("Don't embed private images in posts"), Config::get('system', 'disable_embedded'), L10n::t("Don't replace locally-hosted private photos in posts with an embedded copy of the image. This means that contacts who receive posts containing private photos will have to authenticate and load each image, which may take a while.")], - '$explicit_content' => ['explicit_content', L10n::t('Explicit Content'), Config::get('system', 'explicit_content', false), L10n::t('Set this to announce that your node is used mostly for explicit content that might not be suited for minors. This information will be published in the node information and might be used, e.g. by the global directory, to filter your node from listings of nodes to join. Additionally a note about this will be shown at the user registration page.')], - '$allow_users_remote_self'=> ['allow_users_remote_self', L10n::t('Allow Users to set remote_self'), Config::get('system', 'allow_users_remote_self'), L10n::t('With checking this, every user is allowed to mark every contact as a remote_self in the repair contact dialog. Setting this flag on a contact causes mirroring every posting of that contact in the users stream.')], - '$no_multi_reg' => ['no_multi_reg', L10n::t("Block multiple registrations"), Config::get('system', 'block_extended_register'), L10n::t("Disallow users to register additional accounts for use as pages.")], - '$no_openid' => ['no_openid', L10n::t("Disable OpenID"), Config::get('system', 'no_openid'), L10n::t("Disable OpenID support for registration and logins.")], - '$no_regfullname' => ['no_regfullname', L10n::t("No Fullname check"), Config::get('system', 'no_regfullname'), L10n::t("Allow users to register without a space between the first name and the last name in their full name.")], - '$community_page_style' => ['community_page_style', L10n::t("Community pages for visitors"), Config::get('system', 'community_page_style'), L10n::t("Which community pages should be available for visitors. Local users always see both pages."), $community_page_style_choices], - '$max_author_posts_community_page' => ['max_author_posts_community_page', L10n::t("Posts per user on community page"), Config::get('system', 'max_author_posts_community_page'), L10n::t("The maximum number of posts per user on the community page. \x28Not valid for 'Global Community'\x29")], - '$ostatus_disabled' => ['ostatus_disabled', L10n::t("Disable OStatus support"), Config::get('system', 'ostatus_disabled'), L10n::t("Disable built-in OStatus (StatusNet, GNU Social etc.) compatibility. All communications in OStatus are public, so privacy warnings will be occasionally displayed.")], - '$ostatus_full_threads' => ['ostatus_full_threads', L10n::t("Only import OStatus/ActivityPub threads from our contacts"), Config::get('system', 'ostatus_full_threads'), L10n::t("Normally we import every content from our OStatus and ActivityPub contacts. With this option we only store threads that are started by a contact that is known on our system.")], - '$ostatus_not_able' => L10n::t("OStatus support can only be enabled if threading is enabled."), - '$diaspora_able' => $diaspora_able, - '$diaspora_not_able' => L10n::t("Diaspora support can't be enabled because Friendica was installed into a sub directory."), - '$diaspora_enabled' => ['diaspora_enabled', L10n::t("Enable Diaspora support"), Config::get('system', 'diaspora_enabled', $diaspora_able), L10n::t("Provide built-in Diaspora network compatibility.")], - '$dfrn_only' => ['dfrn_only', L10n::t('Only allow Friendica contacts'), Config::get('system', 'dfrn_only'), L10n::t("All contacts must use Friendica protocols. All other built-in communication protocols disabled.")], - '$verifyssl' => ['verifyssl', L10n::t("Verify SSL"), Config::get('system', 'verifyssl'), L10n::t("If you wish, you can turn on strict certificate checking. This will mean you cannot connect \x28at all\x29 to self-signed SSL sites.")], - '$proxyuser' => ['proxyuser', L10n::t("Proxy user"), Config::get('system', 'proxyuser'), ""], - '$proxy' => ['proxy', L10n::t("Proxy URL"), Config::get('system', 'proxy'), ""], - '$timeout' => ['timeout', L10n::t("Network timeout"), Config::get('system', 'curl_timeout', 60), L10n::t("Value is in seconds. Set to 0 for unlimited \x28not recommended\x29.")], - '$maxloadavg' => ['maxloadavg', L10n::t("Maximum Load Average"), Config::get('system', 'maxloadavg', 20), L10n::t("Maximum system load before delivery and poll processes are deferred - default %d.", 20)], - '$maxloadavg_frontend' => ['maxloadavg_frontend', L10n::t("Maximum Load Average \x28Frontend\x29"), Config::get('system', 'maxloadavg_frontend', 50), L10n::t("Maximum system load before the frontend quits service - default 50.")], - '$min_memory' => ['min_memory', L10n::t("Minimal Memory"), Config::get('system', 'min_memory', 0), L10n::t("Minimal free memory in MB for the worker. Needs access to /proc/meminfo - default 0 \x28deactivated\x29.")], - '$optimize_max_tablesize' => ['optimize_max_tablesize', L10n::t("Maximum table size for optimization"), $optimize_max_tablesize, L10n::t("Maximum table size \x28in MB\x29 for the automatic optimization. Enter -1 to disable it.")], - '$optimize_fragmentation' => ['optimize_fragmentation', L10n::t("Minimum level of fragmentation"), Config::get('system', 'optimize_fragmentation', 30), L10n::t("Minimum fragmenation level to start the automatic optimization - default value is 30%.")], - - '$poco_completion' => ['poco_completion', L10n::t("Periodical check of global contacts"), Config::get('system', 'poco_completion'), L10n::t("If enabled, the global contacts are checked periodically for missing or outdated data and the vitality of the contacts and servers.")], - '$poco_requery_days' => ['poco_requery_days', L10n::t("Days between requery"), Config::get('system', 'poco_requery_days'), L10n::t("Number of days after which a server is requeried for his contacts.")], - '$poco_discovery' => ['poco_discovery', L10n::t("Discover contacts from other servers"), (string)intval(Config::get('system', 'poco_discovery')), L10n::t("Periodically query other servers for contacts. You can choose between 'users': the users on the remote system, 'Global Contacts': active contacts that are known on the system. The fallback is meant for Redmatrix servers and older friendica servers, where global contacts weren't available. The fallback increases the server load, so the recommended setting is 'Users, Global Contacts'."), $poco_discovery_choices], - '$poco_discovery_since' => ['poco_discovery_since', L10n::t("Timeframe for fetching global contacts"), (string)intval(Config::get('system', 'poco_discovery_since')), L10n::t("When the discovery is activated, this value defines the timeframe for the activity of the global contacts that are fetched from other servers."), $poco_discovery_since_choices], - '$poco_local_search' => ['poco_local_search', L10n::t("Search the local directory"), Config::get('system', 'poco_local_search'), L10n::t("Search the local directory instead of the global directory. When searching locally, every search will be executed on the global directory in the background. This improves the search results when the search is repeated.")], - - '$nodeinfo' => ['nodeinfo', L10n::t("Publish server information"), Config::get('system', 'nodeinfo'), L10n::t("If enabled, general server and usage data will be published. The data contains the name and version of the server, number of users with public profiles, number of posts and the activated protocols and connectors. See the-federation.info for details.")], - - '$check_new_version_url' => ['check_new_version_url', L10n::t("Check upstream version"), Config::get('system', 'check_new_version_url'), L10n::t("Enables checking for new Friendica versions at github. If there is a new version, you will be informed in the admin panel overview."), $check_git_version_choices], - '$suppress_tags' => ['suppress_tags', L10n::t("Suppress Tags"), Config::get('system', 'suppress_tags'), L10n::t("Suppress showing a list of hashtags at the end of the posting.")], - '$dbclean' => ['dbclean', L10n::t("Clean database"), Config::get('system', 'dbclean', false), L10n::t("Remove old remote items, orphaned database records and old content from some other helper tables.")], - '$dbclean_expire_days' => ['dbclean_expire_days', L10n::t("Lifespan of remote items"), Config::get('system', 'dbclean-expire-days', 0), L10n::t("When the database cleanup is enabled, this defines the days after which remote items will be deleted. Own items, and marked or filed items are always kept. 0 disables this behaviour.")], - '$dbclean_unclaimed' => ['dbclean_unclaimed', L10n::t("Lifespan of unclaimed items"), Config::get('system', 'dbclean-expire-unclaimed', 90), L10n::t("When the database cleanup is enabled, this defines the days after which unclaimed remote items (mostly content from the relay) will be deleted. Default value is 90 days. Defaults to the general lifespan value of remote items if set to 0.")], - '$dbclean_expire_conv' => ['dbclean_expire_conv', L10n::t("Lifespan of raw conversation data"), Config::get('system', 'dbclean_expire_conversation', 90), L10n::t("The conversation data is used for ActivityPub and OStatus, as well as for debug purposes. It should be safe to remove it after 14 days, default is 90 days.")], - '$itemcache' => ['itemcache', L10n::t("Path to item cache"), Config::get('system', 'itemcache'), L10n::t("The item caches buffers generated bbcode and external images.")], - '$itemcache_duration' => ['itemcache_duration', L10n::t("Cache duration in seconds"), Config::get('system', 'itemcache_duration'), L10n::t("How long should the cache files be hold? Default value is 86400 seconds \x28One day\x29. To disable the item cache, set the value to -1.")], - '$max_comments' => ['max_comments', L10n::t("Maximum numbers of comments per post"), Config::get('system', 'max_comments'), L10n::t("How much comments should be shown for each post? Default value is 100.")], - '$temppath' => ['temppath', L10n::t("Temp path"), Config::get('system', 'temppath'), L10n::t("If you have a restricted system where the webserver can't access the system temp path, enter another path here.")], - '$basepath' => ['basepath', L10n::t("Base path to installation"), Config::get('system', 'basepath'), L10n::t("If the system cannot detect the correct path to your installation, enter the correct path here. This setting should only be set if you are using a restricted system and symbolic links to your webroot.")], - '$proxy_disabled' => ['proxy_disabled', L10n::t("Disable picture proxy"), Config::get('system', 'proxy_disabled'), L10n::t("The picture proxy increases performance and privacy. It shouldn't be used on systems with very low bandwidth.")], - '$only_tag_search' => ['only_tag_search', L10n::t("Only search in tags"), Config::get('system', 'only_tag_search'), L10n::t("On large systems the text search can slow down the system extremely.")], - - '$relocate_url' => ['relocate_url', L10n::t("New base url"), System::baseUrl(), L10n::t("Change base url for this server. Sends relocate message to all Friendica and Diaspora* contacts of all users.")], - - '$rino' => ['rino', L10n::t("RINO Encryption"), intval(Config::get('system', 'rino_encrypt')), L10n::t("Encryption layer between nodes."), [0 => L10n::t("Disabled"), 1 => L10n::t("Enabled")]], - - '$worker_queues' => ['worker_queues', L10n::t("Maximum number of parallel workers"), Config::get('system', 'worker_queues'), L10n::t("On shared hosters set this to %d. On larger systems, values of %d are great. Default value is %d.", 5, 20, 10)], - '$worker_dont_fork' => ['worker_dont_fork', L10n::t("Don't use 'proc_open' with the worker"), Config::get('system', 'worker_dont_fork'), L10n::t("Enable this if your system doesn't allow the use of 'proc_open'. This can happen on shared hosters. If this is enabled you should increase the frequency of worker calls in your crontab.")], - '$worker_fastlane' => ['worker_fastlane', L10n::t("Enable fastlane"), Config::get('system', 'worker_fastlane'), L10n::t("When enabed, the fastlane mechanism starts an additional worker if processes with higher priority are blocked by processes of lower priority.")], - '$worker_frontend' => ['worker_frontend', L10n::t('Enable frontend worker'), Config::get('system', 'frontend_worker'), L10n::t('When enabled the Worker process is triggered when backend access is performed \x28e.g. messages being delivered\x29. On smaller sites you might want to call %s/worker on a regular basis via an external cron job. You should only enable this option if you cannot utilize cron/scheduled jobs on your server.', System::baseUrl())], - - '$relay_subscribe' => ['relay_subscribe', L10n::t("Subscribe to relay"), Config::get('system', 'relay_subscribe'), L10n::t("Enables the receiving of public posts from the relay. They will be included in the search, subscribed tags and on the global community page.")], - '$relay_server' => ['relay_server', L10n::t("Relay server"), Config::get('system', 'relay_server', 'https://relay.diasp.org'), L10n::t("Address of the relay server where public posts should be send to. For example https://relay.diasp.org")], - '$relay_directly' => ['relay_directly', L10n::t("Direct relay transfer"), Config::get('system', 'relay_directly'), L10n::t("Enables the direct transfer to other servers without using the relay servers")], - '$relay_scope' => ['relay_scope', L10n::t("Relay scope"), Config::get('system', 'relay_scope'), L10n::t("Can be 'all' or 'tags'. 'all' means that every public post should be received. 'tags' means that only posts with selected tags should be received."), ['' => L10n::t('Disabled'), 'all' => L10n::t('all'), 'tags' => L10n::t('tags')]], - '$relay_server_tags' => ['relay_server_tags', L10n::t("Server tags"), Config::get('system', 'relay_server_tags'), L10n::t("Comma separated list of tags for the 'tags' subscription.")], - '$relay_user_tags' => ['relay_user_tags', L10n::t("Allow user tags"), Config::get('system', 'relay_user_tags', true), L10n::t("If enabled, the tags from the saved searches will used for the 'tags' subscription in addition to the 'relay_server_tags'.")], - - '$form_security_token' => BaseModule::getFormSecurityToken("admin_site"), - '$relocate_button' => L10n::t('Start Relocation'), - ]); -} - -/** - * @brief Generates admin panel subpage for DB syncronization - * - * This page checks if the database of friendica is in sync with the specs. - * Should this not be the case, it attemps to sync the structure and notifies - * the admin if the automatic process was failing. - * - * The returned string holds the HTML code of the page. - * - * @param App $a - * @return string - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - */ -function admin_page_dbsync(App $a) -{ - $o = ''; - - if ($a->argc > 3 && intval($a->argv[3]) && $a->argv[2] === 'mark') { - Config::set('database', 'update_' . intval($a->argv[3]), 'success'); - $curr = Config::get('system', 'build'); - if (intval($curr) == intval($a->argv[3])) { - Config::set('system', 'build', intval($curr) + 1); - } - info(L10n::t('Update has been marked successful') . EOL); - $a->internalRedirect('admin/dbsync'); - } - - if (($a->argc > 2) && (intval($a->argv[2]) || ($a->argv[2] === 'check'))) { - $retval = DBStructure::update($a->getBasePath(), false, true); - if ($retval === '') { - $o .= L10n::t("Database structure update %s was successfully applied.", DB_UPDATE_VERSION) . "
"; - Config::set('database', 'last_successful_update', DB_UPDATE_VERSION); - Config::set('database', 'last_successful_update_time', time()); - } else { - $o .= L10n::t("Executing of database structure update %s failed with error: %s", DB_UPDATE_VERSION, $retval) . "
"; - } - if ($a->argv[2] === 'check') { - return $o; - } - } - - if ($a->argc > 2 && intval($a->argv[2])) { - require_once 'update.php'; - - $func = 'update_' . intval($a->argv[2]); - - if (function_exists($func)) { - $retval = $func(); - - if ($retval === Update::FAILED) { - $o .= L10n::t("Executing %s failed with error: %s", $func, $retval); - } elseif ($retval === Update::SUCCESS) { - $o .= L10n::t('Update %s was successfully applied.', $func); - Config::set('database', $func, 'success'); - } else { - $o .= L10n::t('Update %s did not return a status. Unknown if it succeeded.', $func); - } - } else { - $o .= L10n::t('There was no additional update function %s that needed to be called.', $func) . "
"; - Config::set('database', $func, 'success'); - } - - return $o; - } - - $failed = []; - $r = q("SELECT `k`, `v` FROM `config` WHERE `cat` = 'database' "); - - if (DBA::isResult($r)) { - foreach ($r as $rr) { - $upd = intval(substr($rr['k'], 7)); - if ($upd < 1139 || $rr['v'] === 'success') { - continue; - } - $failed[] = $upd; - } - } - - if (!count($failed)) { - $o = Renderer::replaceMacros(Renderer::getMarkupTemplate('structure_check.tpl'), [ - '$base' => System::baseUrl(true), - '$banner' => L10n::t('No failed updates.'), - '$check' => L10n::t('Check database structure'), - ]); - } else { - $o = Renderer::replaceMacros(Renderer::getMarkupTemplate('failed_updates.tpl'), [ - '$base' => System::baseUrl(true), - '$banner' => L10n::t('Failed Updates'), - '$desc' => L10n::t('This does not include updates prior to 1139, which did not return a status.'), - '$mark' => L10n::t("Mark success \x28if update was manually applied\x29"), - '$apply' => L10n::t('Attempt to execute this update step automatically'), - '$failed' => $failed - ]); - } - - return $o; -} - -/** - * @brief Process data send by Users admin page - * - * @param App $a - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - */ -function admin_page_users_post(App $a) -{ - $pending = defaults($_POST, 'pending' , []); - $users = defaults($_POST, 'user' , []); - $nu_name = defaults($_POST, 'new_user_name' , ''); - $nu_nickname = defaults($_POST, 'new_user_nickname', ''); - $nu_email = defaults($_POST, 'new_user_email' , ''); - $nu_language = Config::get('system', 'language'); - - BaseModule::checkFormSecurityTokenRedirectOnError('/admin/users', 'admin_users'); - - if (!($nu_name === "") && !($nu_email === "") && !($nu_nickname === "")) { - try { - $result = User::create([ - 'username' => $nu_name, - 'email' => $nu_email, - 'nickname' => $nu_nickname, - 'verified' => 1, - 'language' => $nu_language - ]); - } catch (Exception $ex) { - notice($ex->getMessage()); - return; - } - - $user = $result['user']; - $preamble = Strings::deindent(L10n::t(' - Dear %1$s, - the administrator of %2$s has set up an account for you.')); - $body = Strings::deindent(L10n::t(' - The login details are as follows: - - Site Location: %1$s - Login Name: %2$s - Password: %3$s - - You may change your password from your account "Settings" page after logging - in. - - Please take a few moments to review the other account settings on that page. - - You may also wish to add some basic information to your default profile - ' . "\x28" . 'on the "Profiles" page' . "\x29" . ' so that other people can easily find you. - - We recommend setting your full name, adding a profile photo, - adding some profile "keywords" ' . "\x28" . 'very useful in making new friends' . "\x29" . ' - and - perhaps what country you live in; if you do not wish to be more specific - than that. - - We fully respect your right to privacy, and none of these items are necessary. - If you are new and do not know anybody here, they may help - you to make some new and interesting friends. - - If you ever want to delete your account, you can do so at %1$s/removeme - - Thank you and welcome to %4$s.')); - - $preamble = sprintf($preamble, $user['username'], Config::get('config', 'sitename')); - $body = sprintf($body, System::baseUrl(), $user['nickname'], $result['password'], Config::get('config', 'sitename')); - - notification([ - 'type' => SYSTEM_EMAIL, - 'language' => $user['language'], - 'to_name' => $user['username'], - 'to_email' => $user['email'], - 'uid' => $user['uid'], - 'subject' => L10n::t('Registration details for %s', Config::get('config', 'sitename')), - 'preamble' => $preamble, - 'body' => $body]); - } - - if (!empty($_POST['page_users_block'])) { - foreach ($users as $uid) { - q("UPDATE `user` SET `blocked` = 1-`blocked` WHERE `uid` = %s", intval($uid) - ); - } - notice(L10n::tt("%s user blocked/unblocked", "%s users blocked/unblocked", count($users))); - } - if (!empty($_POST['page_users_delete'])) { - foreach ($users as $uid) { - if (local_user() != $uid) { - User::remove($uid); - } else { - notice(L10n::t('You can\'t remove yourself')); - } - } - notice(L10n::tt("%s user deleted", "%s users deleted", count($users))); - } - - if (!empty($_POST['page_users_approve'])) { - require_once "mod/regmod.php"; - foreach ($pending as $hash) { - user_allow($hash); - } - } - if (!empty($_POST['page_users_deny'])) { - require_once "mod/regmod.php"; - foreach ($pending as $hash) { - user_deny($hash); - } - } - $a->internalRedirect('admin/users'); - return; // NOTREACHED -} - -/** - * @brief Admin panel subpage for User management - * - * This function generates the admin panel page for user management of the - * node. It offers functionality to add/block/delete users and offers some - * statistics about the userbase. - * - * The returned string holds the HTML code of the page. - * - * @param App $a - * @return string - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - */ -function admin_page_users(App $a) -{ - if ($a->argc > 2) { - $uid = $a->argv[3]; - $user = DBA::selectFirst('user', ['username', 'blocked'], ['uid' => $uid]); - if (!DBA::isResult($user)) { - notice('User not found' . EOL); - $a->internalRedirect('admin/users'); - return ''; // NOTREACHED - } - switch ($a->argv[2]) { - case "delete": - if (local_user() != $uid) { - BaseModule::checkFormSecurityTokenRedirectOnError('/admin/users', 'admin_users', 't'); - // delete user - User::remove($uid); - - notice(L10n::t("User '%s' deleted", $user['username'])); - } else { - notice(L10n::t('You can\'t remove yourself')); - } - break; - case "block": - BaseModule::checkFormSecurityTokenRedirectOnError('/admin/users', 'admin_users', 't'); - q("UPDATE `user` SET `blocked` = %d WHERE `uid` = %s", - intval(1 - $user['blocked']), - intval($uid) - ); - notice(sprintf(($user['blocked'] ? L10n::t("User '%s' unblocked") : L10n::t("User '%s' blocked")), $user['username']) . EOL); - break; - } - $a->internalRedirect('admin/users'); - return ''; // NOTREACHED - } - - /* get pending */ - $pending = Register::getPending(); - - $pager = new Pager($a->query_string, 100); - - /* ordering */ - $valid_orders = [ - 'contact.name', - 'user.email', - 'user.register_date', - 'user.login_date', - 'lastitem_date', - 'user.page-flags' - ]; - - $order = "contact.name"; - $order_direction = "+"; - if (!empty($_GET['o'])) { - $new_order = $_GET['o']; - if ($new_order[0] === "-") { - $order_direction = "-"; - $new_order = substr($new_order, 1); - } - - if (in_array($new_order, $valid_orders)) { - $order = $new_order; - } - } - $sql_order = "`" . str_replace('.', '`.`', $order) . "`"; - $sql_order_direction = ($order_direction === "+") ? "ASC" : "DESC"; - - $users = q("SELECT `user`.*, `contact`.`name`, `contact`.`url`, `contact`.`micro`, `user`.`account_expired`, `contact`.`last-item` AS `lastitem_date` - FROM `user` - INNER JOIN `contact` ON `contact`.`uid` = `user`.`uid` AND `contact`.`self` - WHERE `user`.`verified` - ORDER BY $sql_order $sql_order_direction LIMIT %d, %d", $pager->getStart(), $pager->getItemsPerPage() - ); - - $adminlist = explode(",", str_replace(" ", "", Config::get('config', 'admin_email'))); - $_setup_users = function ($e) use ($adminlist) { - $page_types = [ - User::PAGE_FLAGS_NORMAL => L10n::t('Normal Account Page'), - User::PAGE_FLAGS_SOAPBOX => L10n::t('Soapbox Page'), - User::PAGE_FLAGS_COMMUNITY => L10n::t('Public Forum'), - User::PAGE_FLAGS_FREELOVE => L10n::t('Automatic Friend Page'), - User::PAGE_FLAGS_PRVGROUP => L10n::t('Private Forum') - ]; - $account_types = [ - User::ACCOUNT_TYPE_PERSON => L10n::t('Personal Page'), - User::ACCOUNT_TYPE_ORGANISATION => L10n::t('Organisation Page'), - User::ACCOUNT_TYPE_NEWS => L10n::t('News Page'), - User::ACCOUNT_TYPE_COMMUNITY => L10n::t('Community Forum'), - User::ACCOUNT_TYPE_RELAY => L10n::t('Relay'), - ]; - - $e['page_flags_raw'] = $e['page-flags']; - $e['page-flags'] = $page_types[$e['page-flags']]; - - $e['account_type_raw'] = ($e['page_flags_raw'] == 0) ? $e['account-type'] : -1; - $e['account-type'] = ($e['page_flags_raw'] == 0) ? $account_types[$e['account-type']] : ""; - - $e['register_date'] = Temporal::getRelativeDate($e['register_date']); - $e['login_date'] = Temporal::getRelativeDate($e['login_date']); - $e['lastitem_date'] = Temporal::getRelativeDate($e['lastitem_date']); - $e['is_admin'] = in_array($e['email'], $adminlist); - $e['is_deletable'] = (intval($e['uid']) != local_user()); - $e['deleted'] = ($e['account_removed'] ? Temporal::getRelativeDate($e['account_expires_on']) : False); - - return $e; - }; - - $users = array_map($_setup_users, $users); - - - // Get rid of dashes in key names, Smarty3 can't handle them - // and extracting deleted users - - $tmp_users = []; - $deleted = []; - - while (count($users)) { - $new_user = []; - foreach (array_pop($users) as $k => $v) { - $k = str_replace('-', '_', $k); - $new_user[$k] = $v; - } - if ($new_user['deleted']) { - array_push($deleted, $new_user); - } else { - array_push($tmp_users, $new_user); - } - } - //Reversing the two array, and moving $tmp_users to $users - array_reverse($deleted); - while (count($tmp_users)) { - array_push($users, array_pop($tmp_users)); - } - - $th_users = array_map(null, [L10n::t('Name'), L10n::t('Email'), L10n::t('Register date'), L10n::t('Last login'), L10n::t('Last item'), L10n::t('Type')], $valid_orders); - - $t = Renderer::getMarkupTemplate('admin/users.tpl'); - $o = Renderer::replaceMacros($t, [ - // strings // - '$title' => L10n::t('Administration'), - '$page' => L10n::t('Users'), - '$submit' => L10n::t('Add User'), - '$select_all' => L10n::t('select all'), - '$h_pending' => L10n::t('User registrations waiting for confirm'), - '$h_deleted' => L10n::t('User waiting for permanent deletion'), - '$th_pending' => [L10n::t('Request date'), L10n::t('Name'), L10n::t('Email')], - '$no_pending' => L10n::t('No registrations.'), - '$pendingnotetext' => L10n::t('Note from the user'), - '$approve' => L10n::t('Approve'), - '$deny' => L10n::t('Deny'), - '$delete' => L10n::t('Delete'), - '$block' => L10n::t('Block'), - '$blocked' => L10n::t('User blocked'), - '$unblock' => L10n::t('Unblock'), - '$siteadmin' => L10n::t('Site admin'), - '$accountexpired' => L10n::t('Account expired'), - - '$h_users' => L10n::t('Users'), - '$h_newuser' => L10n::t('New User'), - '$th_deleted' => [L10n::t('Name'), L10n::t('Email'), L10n::t('Register date'), L10n::t('Last login'), L10n::t('Last item'), L10n::t('Permanent deletion')], - '$th_users' => $th_users, - '$order_users' => $order, - '$order_direction_users' => $order_direction, - - '$confirm_delete_multi' => L10n::t('Selected users will be deleted!\n\nEverything these users had posted on this site will be permanently deleted!\n\nAre you sure?'), - '$confirm_delete' => L10n::t('The user {0} will be deleted!\n\nEverything this user has posted on this site will be permanently deleted!\n\nAre you sure?'), - - '$form_security_token' => BaseModule::getFormSecurityToken("admin_users"), - - // values // - '$baseurl' => $a->getBaseURL(true), - - '$pending' => $pending, - 'deleted' => $deleted, - '$users' => $users, - '$newusername' => ['new_user_name', L10n::t("Name"), '', L10n::t("Name of the new user.")], - '$newusernickname' => ['new_user_nickname', L10n::t("Nickname"), '', L10n::t("Nickname of the new user.")], - '$newuseremail' => ['new_user_email', L10n::t("Email"), '', L10n::t("Email address of the new user."), '', '', 'email'], - ]); - $o .= $pager->renderFull(DBA::count('user')); - return $o; -} - -/** - * @brief Addons admin page - * - * This function generates the admin panel page for managing addons on the - * friendica node. If an addon name is given a single page showing the details - * for this addon is generated. If no name is given, a list of available - * addons is shown. - * - * The template used for displaying the list of addons and the details of the - * addon are the same as used for the templates. - * - * The returned string returned hulds the HTML code of the page. - * - * @param App $a - * @param array $addons_admin A list of admin addon names - * @return string - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - */ -function admin_page_addons(App $a, array $addons_admin) -{ - /* - * Single addon - */ - if ($a->argc == 3) { - $addon = $a->argv[2]; - if (!is_file("addon/$addon/$addon.php")) { - notice(L10n::t("Item not found.")); - return ''; - } - - if (defaults($_GET, 'a', '') == "t") { - BaseModule::checkFormSecurityTokenRedirectOnError('/admin/addons', 'admin_themes', 't'); - - // Toggle addon status - if (Addon::isEnabled($addon)) { - Addon::uninstall($addon); - info(L10n::t("Addon %s disabled.", $addon)); - } else { - Addon::install($addon); - info(L10n::t("Addon %s enabled.", $addon)); - } - - Addon::saveEnabledList(); - $a->internalRedirect('admin/addons'); - return ''; // NOTREACHED - } - - // display addon details - if (Addon::isEnabled($addon)) { - $status = "on"; - $action = L10n::t("Disable"); - } else { - $status = "off"; - $action = L10n::t("Enable"); - } - - $readme = null; - if (is_file("addon/$addon/README.md")) { - $readme = Markdown::convert(file_get_contents("addon/$addon/README.md"), false); - } elseif (is_file("addon/$addon/README")) { - $readme = "
" . file_get_contents("addon/$addon/README") . "
"; - } - - $admin_form = ""; - if (in_array($addon, $addons_admin)) { - require_once "addon/$addon/$addon.php"; - $func = $addon . '_addon_admin'; - $func($a, $admin_form); - } - - $t = Renderer::getMarkupTemplate('admin/addon_details.tpl'); - - return Renderer::replaceMacros($t, [ - '$title' => L10n::t('Administration'), - '$page' => L10n::t('Addons'), - '$toggle' => L10n::t('Toggle'), - '$settings' => L10n::t('Settings'), - '$baseurl' => $a->getBaseURL(true), - - '$addon' => $addon, - '$status' => $status, - '$action' => $action, - '$info' => Addon::getInfo($addon), - '$str_author' => L10n::t('Author: '), - '$str_maintainer' => L10n::t('Maintainer: '), - - '$admin_form' => $admin_form, - '$function' => 'addons', - '$screenshot' => '', - '$readme' => $readme, - - '$form_security_token' => BaseModule::getFormSecurityToken("admin_themes"), - ]); - } - - /* - * List addons - */ - if (!empty($_GET['a']) && $_GET['a'] == "r") { - BaseModule::checkFormSecurityTokenRedirectOnError($a->getBaseURL() . '/admin/addons', 'admin_themes', 't'); - Addon::reload(); - info("Addons reloaded"); - $a->internalRedirect('admin/addons'); - } - - $addons = []; - $files = glob("addon/*/"); - if (is_array($files)) { - foreach ($files as $file) { - if (is_dir($file)) { - list($tmp, $id) = array_map("trim", explode("/", $file)); - $info = Addon::getInfo($id); - $show_addon = true; - - // If the addon is unsupported, then only show it, when it is enabled - if ((strtolower($info["status"]) == "unsupported") && !Addon::isEnabled($id)) { - $show_addon = false; - } - - // Override the above szenario, when the admin really wants to see outdated stuff - if (Config::get("system", "show_unsupported_addons")) { - $show_addon = true; - } - - if ($show_addon) { - $addons[] = [$id, (Addon::isEnabled($id) ? "on" : "off"), $info]; - } - } - } - } - - $t = Renderer::getMarkupTemplate('admin/addons.tpl'); - return Renderer::replaceMacros($t, [ - '$title' => L10n::t('Administration'), - '$page' => L10n::t('Addons'), - '$submit' => L10n::t('Save Settings'), - '$reload' => L10n::t('Reload active addons'), - '$baseurl' => System::baseUrl(true), - '$function' => 'addons', - '$addons' => $addons, - '$pcount' => count($addons), - '$noplugshint' => L10n::t('There are currently no addons available on your node. You can find the official addon repository at %1$s and might find other interesting addons in the open addon registry at %2$s', 'https://github.com/friendica/friendica-addons', 'http://addons.friendi.ca'), - '$form_security_token' => BaseModule::getFormSecurityToken("admin_themes"), - ]); -} - -/** - * @param array $themes - * @param string $th - * @param int $result - */ -function toggle_theme(&$themes, $th, &$result) -{ - $count = count($themes); - for ($x = 0; $x < $count; $x++) { - if ($themes[$x]['name'] === $th) { - if ($themes[$x]['allowed']) { - $themes[$x]['allowed'] = 0; - $result = 0; - } else { - $themes[$x]['allowed'] = 1; - $result = 1; - } - } - } -} - -/** - * @param array $themes - * @param string $th - * @return int - */ -function theme_status($themes, $th) -{ - $count = count($themes); - for ($x = 0; $x < $count; $x++) { - if ($themes[$x]['name'] === $th) { - if ($themes[$x]['allowed']) { - return 1; - } else { - return 0; - } - } - } - return 0; -} - -/** - * @param array $themes - * @return string - */ -function rebuild_theme_table($themes) -{ - $o = ''; - if (count($themes)) { - foreach ($themes as $th) { - if ($th['allowed']) { - if (strlen($o)) { - $o .= ','; - } - $o .= $th['name']; - } - } - } - return $o; -} - -/** - * @brief Themes admin page - * - * This function generates the admin panel page to control the themes available - * on the friendica node. If the name of a theme is given as parameter a page - * with the details for the theme is shown. Otherwise a list of available - * themes is generated. - * - * The template used for displaying the list of themes and the details of the - * themes are the same as used for the addons. - * - * The returned string contains the HTML code of the admin panel page. - * - * @param App $a - * @return string - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - */ -function admin_page_themes(App $a) -{ - $allowed_themes_str = Config::get('system', 'allowed_themes'); - $allowed_themes_raw = explode(',', $allowed_themes_str); - $allowed_themes = []; - if (count($allowed_themes_raw)) { - foreach ($allowed_themes_raw as $x) { - if (strlen(trim($x))) { - $allowed_themes[] = trim($x); - } - } - } - - $themes = []; - $files = glob('view/theme/*'); - if (is_array($files)) { - foreach ($files as $file) { - $f = basename($file); - - // Is there a style file? - $theme_files = glob('view/theme/' . $f . '/style.*'); - - // If not then quit - if (count($theme_files) == 0) { - continue; - } - - $is_experimental = intval(file_exists($file . '/experimental')); - $is_supported = 1 - (intval(file_exists($file . '/unsupported'))); - $is_allowed = intval(in_array($f, $allowed_themes)); - - if ($is_allowed || $is_supported || Config::get("system", "show_unsupported_themes")) { - $themes[] = ['name' => $f, 'experimental' => $is_experimental, 'supported' => $is_supported, 'allowed' => $is_allowed]; - } - } - } - - if (!count($themes)) { - notice(L10n::t('No themes found.')); - return ''; - } - - /* - * Single theme - */ - - if ($a->argc == 3) { - $theme = $a->argv[2]; - if (!is_dir("view/theme/$theme")) { - notice(L10n::t("Item not found.")); - return ''; - } - - if (!empty($_GET['a']) && $_GET['a'] == "t") { - BaseModule::checkFormSecurityTokenRedirectOnError('/admin/themes', 'admin_themes', 't'); - - // Toggle theme status - - toggle_theme($themes, $theme, $result); - $s = rebuild_theme_table($themes); - if ($result) { - Theme::install($theme); - info(sprintf('Theme %s enabled.', $theme)); - } else { - Theme::uninstall($theme); - info(sprintf('Theme %s disabled.', $theme)); - } - - Config::set('system', 'allowed_themes', $s); - $a->internalRedirect('admin/themes'); - return ''; // NOTREACHED - } - - // display theme details - if (theme_status($themes, $theme)) { - $status = "on"; - $action = L10n::t("Disable"); - } else { - $status = "off"; - $action = L10n::t("Enable"); - } - - $readme = null; - - if (is_file("view/theme/$theme/README.md")) { - $readme = Markdown::convert(file_get_contents("view/theme/$theme/README.md"), false); - } elseif (is_file("view/theme/$theme/README")) { - $readme = "
" . file_get_contents("view/theme/$theme/README") . "
"; - } - - $admin_form = ''; - if (is_file("view/theme/$theme/config.php")) { - require_once "view/theme/$theme/config.php"; - - if (function_exists('theme_admin')) { - $admin_form = theme_admin($a); - } - } - - $screenshot = [Theme::getScreenshot($theme), L10n::t('Screenshot')]; - if (!stristr($screenshot[0], $theme)) { - $screenshot = null; - } - - $t = Renderer::getMarkupTemplate('admin/addon_details.tpl'); - return Renderer::replaceMacros($t, [ - '$title' => L10n::t('Administration'), - '$page' => L10n::t('Themes'), - '$toggle' => L10n::t('Toggle'), - '$settings' => L10n::t('Settings'), - '$baseurl' => System::baseUrl(true), - '$addon' => $theme . (!empty($_GET['mode']) ? '?mode=' . $_GET['mode'] : ''), - '$status' => $status, - '$action' => $action, - '$info' => Theme::getInfo($theme), - '$function' => 'themes', - '$admin_form' => $admin_form, - '$str_author' => L10n::t('Author: '), - '$str_maintainer' => L10n::t('Maintainer: '), - '$screenshot' => $screenshot, - '$readme' => $readme, - - '$form_security_token' => BaseModule::getFormSecurityToken("admin_themes"), - ]); - } - - // reload active themes - if (!empty($_GET['a']) && $_GET['a'] == "r") { - BaseModule::checkFormSecurityTokenRedirectOnError(System::baseUrl() . '/admin/themes', 'admin_themes', 't'); - foreach ($themes as $th) { - if ($th['allowed']) { - Theme::uninstall($th['name']); - Theme::install($th['name']); - } - } - info("Themes reloaded"); - $a->internalRedirect('admin/themes'); - } - - /* - * List themes - */ - - $addons = []; - foreach ($themes as $th) { - $addons[] = [$th['name'], (($th['allowed']) ? "on" : "off"), Theme::getInfo($th['name'])]; - } - - $t = Renderer::getMarkupTemplate('admin/addons.tpl'); - return Renderer::replaceMacros($t, [ - '$title' => L10n::t('Administration'), - '$page' => L10n::t('Themes'), - '$submit' => L10n::t('Save Settings'), - '$reload' => L10n::t('Reload active themes'), - '$baseurl' => System::baseUrl(true), - '$function' => 'themes', - '$addons' => $addons, - '$pcount' => count($themes), - '$noplugshint' => L10n::t('No themes found on the system. They should be placed in %1$s', '/view/themes'), - '$experimental' => L10n::t('[Experimental]'), - '$unsupported' => L10n::t('[Unsupported]'), - '$form_security_token' => BaseModule::getFormSecurityToken("admin_themes"), - ]); -} - -/** - * @brief Prosesses data send by Logs admin page - * - * @param App $a - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - */ -function admin_page_logs_post(App $a) -{ - if (!empty($_POST['page_logs'])) { - BaseModule::checkFormSecurityTokenRedirectOnError('/admin/logs', 'admin_logs'); - - $logfile = (!empty($_POST['logfile']) ? Strings::escapeTags(trim($_POST['logfile'])) : ''); - $debugging = !empty($_POST['debugging']); - $loglevel = defaults($_POST, 'loglevel', LogLevel::ERROR); - - Config::set('system', 'logfile', $logfile); - Config::set('system', 'debugging', $debugging); - Config::set('system', 'loglevel', $loglevel); - } - - info(L10n::t("Log settings updated.")); - $a->internalRedirect('admin/logs'); - return; // NOTREACHED -} - -/** - * @brief Generates admin panel subpage for configuration of the logs - * - * This function take the view/templates/admin_logs.tpl file and generates a - * page where admin can configure the logging of friendica. - * - * Displaying the log is separated from the log config as the logfile can get - * big depending on the settings and changing settings regarding the logs can - * thus waste bandwidth. - * - * The string returned contains the content of the template file with replaced - * macros. - * - * @param App $a - * @return string - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - */ -function admin_page_logs(App $a) -{ - $log_choices = [ - LogLevel::ERROR => 'Error', - LogLevel::WARNING => 'Warning', - LogLevel::NOTICE => 'Notice', - LogLevel::INFO => 'Info', - LogLevel::DEBUG => 'Debug', - ]; - - if (ini_get('log_errors')) { - $phplogenabled = L10n::t('PHP log currently enabled.'); - } else { - $phplogenabled = L10n::t('PHP log currently disabled.'); - } - - $t = Renderer::getMarkupTemplate('admin/logs.tpl'); - - return Renderer::replaceMacros($t, [ - '$title' => L10n::t('Administration'), - '$page' => L10n::t('Logs'), - '$submit' => L10n::t('Save Settings'), - '$clear' => L10n::t('Clear'), - '$baseurl' => System::baseUrl(true), - '$logname' => Config::get('system', 'logfile'), - // name, label, value, help string, extra data... - '$debugging' => ['debugging', L10n::t("Enable Debugging"), Config::get('system', 'debugging'), ""], - '$logfile' => ['logfile', L10n::t("Log file"), Config::get('system', 'logfile'), L10n::t("Must be writable by web server. Relative to your Friendica top-level directory.")], - '$loglevel' => ['loglevel', L10n::t("Log level"), Config::get('system', 'loglevel'), "", $log_choices], - '$form_security_token' => BaseModule::getFormSecurityToken("admin_logs"), - '$phpheader' => L10n::t("PHP logging"), - '$phphint' => L10n::t("To temporarily enable logging of PHP errors and warnings you can prepend the following to the index.php file of your installation. The filename set in the 'error_log' line is relative to the friendica top-level directory and must be writeable by the web server. The option '1' for 'log_errors' and 'display_errors' is to enable these options, set to '0' to disable them."), - '$phplogcode' => "error_reporting(E_ERROR | E_WARNING | E_PARSE);\nini_set('error_log','php.out');\nini_set('log_errors','1');\nini_set('display_errors', '1');", - '$phplogenabled' => $phplogenabled, - ]); -} - -/** - * @brief Generates admin panel subpage to view the Friendica log - * - * This function loads the template view/templates/admin_viewlogs.tpl to - * display the systemlog content. The filename for the systemlog of friendica - * is relative to the base directory and taken from the config entry 'logfile' - * in the 'system' category. - * - * Displaying the log is separated from the log config as the logfile can get - * big depending on the settings and changing settings regarding the logs can - * thus waste bandwidth. - * - * The string returned contains the content of the template file with replaced - * macros. - * - * @param App $a - * @return string - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - */ -function admin_page_viewlogs(App $a) -{ - $t = Renderer::getMarkupTemplate('admin/viewlogs.tpl'); - $f = Config::get('system', 'logfile'); - $data = ''; - - if (!file_exists($f)) { - $data = L10n::t('Error trying to open %1$s log file.\r\n
Check to see if file %1$s exist and is readable.', $f); - } else { - $fp = fopen($f, 'r'); - if (!$fp) { - $data = L10n::t('Couldn\'t open %1$s log file.\r\n
Check to see if file %1$s is readable.', $f); - } else { - $fstat = fstat($fp); - $size = $fstat['size']; - if ($size != 0) { - if ($size > 5000000 || $size < 0) { - $size = 5000000; - } - $seek = fseek($fp, 0 - $size, SEEK_END); - if ($seek === 0) { - $data = Strings::escapeHtml(fread($fp, $size)); - while (!feof($fp)) { - $data .= Strings::escapeHtml(fread($fp, 4096)); - } - } - } - fclose($fp); - } - } - return Renderer::replaceMacros($t, [ - '$title' => L10n::t('Administration'), - '$page' => L10n::t('View Logs'), - '$data' => $data, - '$logname' => Config::get('system', 'logfile') - ]); -} - -/** - * @brief Prosesses data send by the features admin page - * - * @param App $a - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - */ -function admin_page_features_post(App $a) -{ - BaseModule::checkFormSecurityTokenRedirectOnError('/admin/features', 'admin_manage_features'); - - Logger::log('postvars: ' . print_r($_POST, true), Logger::DATA); - - $features = Feature::get(false); - - foreach ($features as $fname => $fdata) { - foreach (array_slice($fdata, 1) as $f) { - $feature = $f[0]; - $feature_state = 'feature_' . $feature; - $featurelock = 'featurelock_' . $feature; - - if (!empty($_POST[$feature_state])) { - $val = intval($_POST[$feature_state]); - } else { - $val = 0; - } - Config::set('feature', $feature, $val); - - if (!empty($_POST[$featurelock])) { - Config::set('feature_lock', $feature, $val); - } else { - Config::delete('feature_lock', $feature); - } - } - } - - $a->internalRedirect('admin/features'); - return; // NOTREACHED -} - -/** - * @brief Subpage for global additional feature management - * - * This functin generates the subpage 'Manage Additional Features' - * for the admin panel. At this page the admin can set preferences - * for the user settings of the 'additional features'. If needed this - * preferences can be locked through the admin. - * - * The returned string contains the HTML code of the subpage 'Manage - * Additional Features' - * - * @param App $a - * @return string - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - */ -function admin_page_features(App $a) -{ - if (($a->argc > 1) && ($a->getArgumentValue(1) === 'features')) { - $arr = []; - $features = Feature::get(false); - - foreach ($features as $fname => $fdata) { - $arr[$fname] = []; - $arr[$fname][0] = $fdata[0]; - foreach (array_slice($fdata, 1) as $f) { - $set = Config::get('feature', $f[0], $f[3]); - $arr[$fname][1][] = [ - ['feature_' . $f[0], $f[1], $set, $f[2], [L10n::t('Off'), L10n::t('On')]], - ['featurelock_' . $f[0], L10n::t('Lock feature %s', $f[1]), (($f[4] !== false) ? "1" : ''), '', [L10n::t('Off'), L10n::t('On')]] - ]; - } - } - - $tpl = Renderer::getMarkupTemplate('admin/settings_features.tpl'); - $o = Renderer::replaceMacros($tpl, [ - '$form_security_token' => BaseModule::getFormSecurityToken("admin_manage_features"), - '$title' => L10n::t('Manage Additional Features'), - '$features' => $arr, - '$submit' => L10n::t('Save Settings'), - ]); - - return $o; - } -} - -function admin_page_server_vital() -{ - // Fetch the host-meta to check if this really is a vital server - return Network::curl(System::baseUrl() . '/.well-known/host-meta')->isSuccess(); -} diff --git a/mod/allfriends.php b/mod/allfriends.php deleted file mode 100644 index ce648cc25..000000000 --- a/mod/allfriends.php +++ /dev/null @@ -1,106 +0,0 @@ -argc > 1) { - $cid = intval($a->argv[1]); - } - - if (!$cid) { - return; - } - - $uid = $a->user['uid']; - - $contact = DBA::selectFirst('contact', ['name', 'url', 'photo', 'uid', 'id'], ['id' => $cid, 'uid' => local_user()]); - - if (!DBA::isResult($contact)) { - return; - } - - $a->page['aside'] = ""; - Model\Profile::load($a, "", 0, Model\Contact::getDetailsByURL($contact["url"])); - - $total = Model\GContact::countAllFriends(local_user(), $cid); - - $pager = new Pager($a->query_string); - - $r = Model\GContact::allFriends(local_user(), $cid, $pager->getStart(), $pager->getItemsPerPage()); - if (!DBA::isResult($r)) { - $o .= L10n::t('No friends to display.'); - return $o; - } - - $id = 0; - - $entries = []; - foreach ($r as $rr) { - //get further details of the contact - $contact_details = Model\Contact::getDetailsByURL($rr['url'], $uid, $rr); - - $connlnk = ''; - // $rr[cid] is only available for common contacts. So if the contact is a common one, use contact_photo_menu to generate the photo_menu - // If the contact is not common to the user, Connect/Follow' will be added to the photo menu - if ($rr['cid']) { - $rr['id'] = $rr['cid']; - $photo_menu = Model\Contact::photoMenu($rr); - } else { - $connlnk = System::baseUrl() . '/follow/?url=' . $rr['url']; - $photo_menu = [ - 'profile' => [L10n::t("View Profile"), Model\Contact::magicLink($rr['url'])], - 'follow' => [L10n::t("Connect/Follow"), $connlnk] - ]; - } - - $entry = [ - 'url' => Model\Contact::magicLink($rr['url']), - 'itemurl' => defaults($contact_details, 'addr', $rr['url']), - 'name' => $contact_details['name'], - 'thumb' => ProxyUtils::proxifyUrl($contact_details['thumb'], false, ProxyUtils::SIZE_THUMB), - 'img_hover' => $contact_details['name'], - 'details' => $contact_details['location'], - 'tags' => $contact_details['keywords'], - 'about' => $contact_details['about'], - 'account_type' => Model\Contact::getAccountType($contact_details), - 'network' => ContactSelector::networkToName($contact_details['network'], $contact_details['url']), - 'photo_menu' => $photo_menu, - 'conntxt' => L10n::t('Connect'), - 'connlnk' => $connlnk, - 'id' => ++$id, - ]; - $entries[] = $entry; - } - - $tab_str = Module\Contact::getTabsHTML($a, $contact, 4); - - $tpl = Renderer::getMarkupTemplate('viewcontact_template.tpl'); - $o .= Renderer::replaceMacros($tpl, [ - '$tab_str' => $tab_str, - '$contacts' => $entries, - '$paginate' => $pager->renderFull($total), - ]); - - return $o; -} diff --git a/mod/amcd.php b/mod/amcd.php deleted file mode 100644 index ca5aa07aa..000000000 --- a/mod/amcd.php +++ /dev/null @@ -1,49 +0,0 @@ - $title, - '$apps' => $apps, - ]); -} diff --git a/mod/babel.php b/mod/babel.php deleted file mode 100644 index 51bbf7c8a..000000000 --- a/mod/babel.php +++ /dev/null @@ -1,168 +0,0 @@ -', $s); -} - -function babel_content() -{ - $results = []; - if (!empty($_REQUEST['text'])) { - switch (defaults($_REQUEST, 'type', 'bbcode')) { - case 'bbcode': - $bbcode = trim($_REQUEST['text']); - $results[] = [ - 'title' => L10n::t('Source input'), - 'content' => visible_whitespace($bbcode) - ]; - - $plain = Text\BBCode::toPlaintext($bbcode, false); - $results[] = [ - 'title' => L10n::t('BBCode::toPlaintext'), - 'content' => visible_whitespace($plain) - ]; - - $html = Text\BBCode::convert($bbcode); - $results[] = [ - 'title' => L10n::t('BBCode::convert (raw HTML)'), - 'content' => visible_whitespace(htmlspecialchars($html)) - ]; - - $results[] = [ - 'title' => L10n::t('BBCode::convert'), - 'content' => $html - ]; - - $bbcode2 = Text\HTML::toBBCode($html); - $results[] = [ - 'title' => L10n::t('BBCode::convert => HTML::toBBCode'), - 'content' => visible_whitespace($bbcode2) - ]; - - $markdown = Text\BBCode::toMarkdown($bbcode); - $results[] = [ - 'title' => L10n::t('BBCode::toMarkdown'), - 'content' => visible_whitespace($markdown) - ]; - - $html2 = Text\Markdown::convert($markdown); - $results[] = [ - 'title' => L10n::t('BBCode::toMarkdown => Markdown::convert'), - 'content' => $html2 - ]; - - $bbcode3 = Text\Markdown::toBBCode($markdown); - $results[] = [ - 'title' => L10n::t('BBCode::toMarkdown => Markdown::toBBCode'), - 'content' => visible_whitespace($bbcode3) - ]; - - $bbcode4 = Text\HTML::toBBCode($html2); - $results[] = [ - 'title' => L10n::t('BBCode::toMarkdown => Markdown::convert => HTML::toBBCode'), - 'content' => visible_whitespace($bbcode4) - ]; - - $item = [ - 'body' => $bbcode, - 'tag' => '', - ]; - - \Friendica\Model\Item::setHashtags($item); - $results[] = [ - 'title' => L10n::t('Item Body'), - 'content' => visible_whitespace($item['body']) - ]; - $results[] = [ - 'title' => L10n::t('Item Tags'), - 'content' => $item['tag'] - ]; - break; - case 'markdown': - $markdown = trim($_REQUEST['text']); - $results[] = [ - 'title' => L10n::t('Source input (Diaspora format)'), - 'content' => '
' . $markdown . '
' - ]; - - $html = Text\Markdown::convert($markdown); - $results[] = [ - 'title' => L10n::t('Markdown::convert (raw HTML)'), - 'content' => visible_whitespace(htmlspecialchars($html)) - ]; - - $results[] = [ - 'title' => L10n::t('Markdown::convert'), - 'content' => $html - ]; - - $bbcode = Text\Markdown::toBBCode($markdown); - $results[] = [ - 'title' => L10n::t('Markdown::toBBCode'), - 'content' => '
' . $bbcode . '
' - ]; - break; - case 'html' : - $html = trim($_REQUEST['text']); - $results[] = [ - 'title' => L10n::t('Raw HTML input'), - 'content' => htmlspecialchars($html) - ]; - - $results[] = [ - 'title' => L10n::t('HTML Input'), - 'content' => $html - ]; - - $bbcode = Text\HTML::toBBCode($html); - $results[] = [ - 'title' => L10n::t('HTML::toBBCode'), - 'content' => visible_whitespace($bbcode) - ]; - - $html2 = Text\BBCode::convert($bbcode); - $results[] = [ - 'title' => L10n::t('HTML::toBBCode => BBCode::convert'), - 'content' => $html2 - ]; - - $results[] = [ - 'title' => L10n::t('HTML::toBBCode => BBCode::convert (raw HTML)'), - 'content' => htmlspecialchars($html2) - ]; - - $markdown = Text\HTML::toMarkdown($html); - $results[] = [ - 'title' => L10n::t('HTML::toMarkdown'), - 'content' => visible_whitespace($markdown) - ]; - - $text = Text\HTML::toPlaintext($html); - $results[] = [ - 'title' => L10n::t('HTML::toPlaintext'), - 'content' => '
' . $text . '
' - ]; - } - } - - $tpl = Renderer::getMarkupTemplate('babel.tpl'); - $o = Renderer::replaceMacros($tpl, [ - '$text' => ['text', L10n::t('Source text'), defaults($_REQUEST, 'text', ''), ''], - '$type_bbcode' => ['type', L10n::t('BBCode'), 'bbcode', '', defaults($_REQUEST, 'type', 'bbcode') == 'bbcode'], - '$type_markdown' => ['type', L10n::t('Markdown'), 'markdown', '', defaults($_REQUEST, 'type', 'bbcode') == 'markdown'], - '$type_html' => ['type', L10n::t('HTML'), 'html', '', defaults($_REQUEST, 'type', 'bbcode') == 'html'], - '$results' => $results - ]); - - return $o; -} diff --git a/mod/bookmarklet.php b/mod/bookmarklet.php deleted file mode 100644 index 1c4d191c4..000000000 --- a/mod/bookmarklet.php +++ /dev/null @@ -1,59 +0,0 @@ -' . L10n::t('Login') . ''; - $o .= Login::form($a->query_string, intval(Config::get('config', 'register_policy')) === \Friendica\Module\Register::CLOSED ? false : true); - return $o; - } - - $referer = Strings::normaliseLink(defaults($_SERVER, 'HTTP_REFERER', '')); - $page = Strings::normaliseLink(System::baseUrl() . "/bookmarklet"); - - if (!strstr($referer, $page)) { - if (empty($_REQUEST["url"])) { - System::httpExit(400, ["title" => L10n::t('Bad Request')]); - } - - $content = add_page_info($_REQUEST["url"]); - - $x = [ - 'is_owner' => true, - 'allow_location' => $a->user['allow_location'], - 'default_location' => $a->user['default-location'], - 'nickname' => $a->user['nickname'], - 'lockstate' => ((is_array($a->user) && ((strlen($a->user['allow_cid'])) || (strlen($a->user['allow_gid'])) || (strlen($a->user['deny_cid'])) || (strlen($a->user['deny_gid'])))) ? 'lock' : 'unlock'), - 'default_perms' => ACL::getDefaultUserPermissions($a->user), - 'acl' => ACL::getFullSelectorHTML($a->user, true), - 'bang' => '', - 'visitor' => 'block', - 'profile_uid' => local_user(), - 'title' => trim(defaults($_REQUEST, 'title', ''), "*"), - 'content' => $content - ]; - $o = status_editor($a, $x, 0, false); - $o .= ""; - } else { - $o = '

' . L10n::t('The post was created') . '

'; - $o .= ""; - } - - return $o; -} diff --git a/mod/cal.php b/mod/cal.php index 5baffec8a..0a2a02e53 100644 --- a/mod/cal.php +++ b/mod/cal.php @@ -31,11 +31,11 @@ function cal_init(App $a) } if (Config::get('system', 'block_public') && !local_user() && !remote_user()) { - System::httpExit(403, ['title' => L10n::t('Access denied.')]); + throw new \Friendica\Network\HTTPException\ForbiddenException(L10n::t('Access denied.')); } if ($a->argc < 2) { - System::httpExit(403, ['title' => L10n::t('Access denied.')]); + throw new \Friendica\Network\HTTPException\ForbiddenException(L10n::t('Access denied.')); } Nav::setSelected('events'); @@ -43,7 +43,7 @@ function cal_init(App $a) $nick = $a->argv[1]; $user = DBA::selectFirst('user', [], ['nickname' => $nick, 'blocked' => false]); if (!DBA::isResult($user)) { - System::httpExit(404, ['title' => L10n::t('Page not found.')]); + throw new \Friendica\Network\HTTPException\NotFoundException(); } $a->data['user'] = $user; @@ -59,7 +59,7 @@ function cal_init(App $a) $account_type = Contact::getAccountType($profile); - $tpl = Renderer::getMarkupTemplate("vcard-widget.tpl"); + $tpl = Renderer::getMarkupTemplate("widget/vcard.tpl"); $vcard_widget = Renderer::replaceMacros($tpl, [ '$name' => $profile['name'], @@ -90,7 +90,6 @@ function cal_content(App $a) $htpl = Renderer::getMarkupTemplate('event_head.tpl'); $a->page['htmlhead'] .= Renderer::replaceMacros($htpl, [ - '$baseurl' => System::baseUrl(), '$module_url' => '/cal/' . $a->data['user']['nickname'], '$modparams' => 2, '$i18n' => $i18n, @@ -148,7 +147,7 @@ function cal_content(App $a) $sql_extra = " AND `event`.`cid` = 0 " . $sql_perms; // get the tab navigation bar - $tabs = Profile::getTabs($a, false, $a->data['user']['nickname']); + $tabs = Profile::getTabs($a, 'cal', false, $a->data['user']['nickname']); // The view mode part is similiar to /mod/events.php if ($mode == 'view') { @@ -268,7 +267,6 @@ function cal_content(App $a) } $o = Renderer::replaceMacros($tpl, [ - '$baseurl' => System::baseUrl(), '$tabs' => $tabs, '$title' => L10n::t('Events'), '$view' => L10n::t('View'), diff --git a/mod/common.php b/mod/common.php index 6b6090e19..a2821921c 100644 --- a/mod/common.php +++ b/mod/common.php @@ -47,7 +47,7 @@ function common_content(App $a) $contact = DBA::selectFirst('contact', ['name', 'url', 'photo', 'uid', 'id'], ['self' => true, 'uid' => $uid]); if (DBA::isResult($contact)) { - $vcard_widget = Renderer::replaceMacros(Renderer::getMarkupTemplate("vcard-widget.tpl"), [ + $vcard_widget = Renderer::replaceMacros(Renderer::getMarkupTemplate("widget/vcard.tpl"), [ '$name' => $contact['name'], '$photo' => $contact['photo'], 'url' => 'contact/' . $cid diff --git a/mod/credits.php b/mod/credits.php deleted file mode 100644 index 3d71c54b3..000000000 --- a/mod/credits.php +++ /dev/null @@ -1,23 +0,0 @@ - L10n::t('Credits'), - '$thanks' => L10n::t('Friendica is a community project, that would not be possible without the help of many people. Here is a list of those who have contributed to the code or the translation of Friendica. Thank you all!'), - '$names' => $names, - ]); -} diff --git a/mod/dfrn_confirm.php b/mod/dfrn_confirm.php index 770c7070b..9f9684e09 100644 --- a/mod/dfrn_confirm.php +++ b/mod/dfrn_confirm.php @@ -28,8 +28,6 @@ use Friendica\Model\Contact; use Friendica\Model\Group; use Friendica\Model\User; use Friendica\Network\Probe; -use Friendica\Protocol\Diaspora; -use Friendica\Protocol\ActivityPub; use Friendica\Util\Crypto; use Friendica\Util\DateTimeFormat; use Friendica\Util\Network; @@ -136,163 +134,153 @@ function dfrn_confirm_post(App $a, $handsfree = null) $site_pubkey = $contact['site-pubkey']; $dfrn_confirm = $contact['confirm']; $aes_allow = $contact['aes_allow']; + $protocol = $contact['network']; - $network = ((strlen($contact['issued-id'])) ? Protocol::DFRN : Protocol::OSTATUS); + /* + * Generate a key pair for all further communications with this person. + * We have a keypair for every contact, and a site key for unknown people. + * This provides a means to carry on relationships with other people if + * any single key is compromised. It is a robust key. We're much more + * worried about key leakage than anybody cracking it. + */ + $res = Crypto::newKeypair(4096); - if ($contact['network']) { - $network = $contact['network']; + $private_key = $res['prvkey']; + $public_key = $res['pubkey']; + + // Save the private key. Send them the public key. + $fields = ['prvkey' => $private_key, 'protocol' => Protocol::DFRN]; + DBA::update('contact', $fields, ['id' => $contact_id]); + + $params = []; + + /* + * Per the DFRN protocol, we will verify both ends by encrypting the dfrn_id with our + * site private key (person on the other end can decrypt it with our site public key). + * Then encrypt our profile URL with the other person's site public key. They can decrypt + * it with their site private key. If the decryption on the other end fails for either + * item, it indicates tampering or key failure on at least one site and we will not be + * able to provide a secure communication pathway. + * + * If other site is willing to accept full encryption, (aes_allow is 1 AND we have php5.3 + * or later) then we encrypt the personal public key we send them using AES-256-CBC and a + * random key which is encrypted with their site public key. + */ + + $src_aes_key = openssl_random_pseudo_bytes(64); + + $result = ''; + openssl_private_encrypt($dfrn_id, $result, $user['prvkey']); + + $params['dfrn_id'] = bin2hex($result); + $params['public_key'] = $public_key; + + $my_url = System::baseUrl() . '/profile/' . $user['nickname']; + + openssl_public_encrypt($my_url, $params['source_url'], $site_pubkey); + $params['source_url'] = bin2hex($params['source_url']); + + if ($aes_allow && function_exists('openssl_encrypt')) { + openssl_public_encrypt($src_aes_key, $params['aes_key'], $site_pubkey); + $params['aes_key'] = bin2hex($params['aes_key']); + $params['public_key'] = bin2hex(openssl_encrypt($public_key, 'AES-256-CBC', $src_aes_key)); } - if ($network === Protocol::DFRN) { - /* - * Generate a key pair for all further communications with this person. - * We have a keypair for every contact, and a site key for unknown people. - * This provides a means to carry on relationships with other people if - * any single key is compromised. It is a robust key. We're much more - * worried about key leakage than anybody cracking it. - */ - $res = Crypto::newKeypair(4096); + $params['dfrn_version'] = DFRN_PROTOCOL_VERSION; + if ($duplex == 1) { + $params['duplex'] = 1; + } - $private_key = $res['prvkey']; - $public_key = $res['pubkey']; + if ($user['page-flags'] == User::PAGE_FLAGS_COMMUNITY) { + $params['page'] = 1; + } - // Save the private key. Send them the public key. - q("UPDATE `contact` SET `prvkey` = '%s' WHERE `id` = %d AND `uid` = %d", - DBA::escape($private_key), - intval($contact_id), - intval($uid) - ); + if ($user['page-flags'] == User::PAGE_FLAGS_PRVGROUP) { + $params['page'] = 2; + } - $params = []; + Logger::log('Confirm: posting data to ' . $dfrn_confirm . ': ' . print_r($params, true), Logger::DATA); - /* - * Per the DFRN protocol, we will verify both ends by encrypting the dfrn_id with our - * site private key (person on the other end can decrypt it with our site public key). - * Then encrypt our profile URL with the other person's site public key. They can decrypt - * it with their site private key. If the decryption on the other end fails for either - * item, it indicates tampering or key failure on at least one site and we will not be - * able to provide a secure communication pathway. - * - * If other site is willing to accept full encryption, (aes_allow is 1 AND we have php5.3 - * or later) then we encrypt the personal public key we send them using AES-256-CBC and a - * random key which is encrypted with their site public key. - */ + /* + * + * POST all this stuff to the other site. + * Temporarily raise the network timeout to 120 seconds because the default 60 + * doesn't always give the other side quite enough time to decrypt everything. + * + */ - $src_aes_key = openssl_random_pseudo_bytes(64); + $res = Network::post($dfrn_confirm, $params, [], 120)->getBody(); - $result = ''; - openssl_private_encrypt($dfrn_id, $result, $user['prvkey']); + Logger::log(' Confirm: received data: ' . $res, Logger::DATA); - $params['dfrn_id'] = bin2hex($result); - $params['public_key'] = $public_key; + // Now figure out what they responded. Try to be robust if the remote site is + // having difficulty and throwing up errors of some kind. - $my_url = System::baseUrl() . '/profile/' . $user['nickname']; + $leading_junk = substr($res, 0, strpos($res, 'status; + $message = XML::unescape($xml->message); // human readable text of what may have gone wrong. + switch ($status) { + case 0: + info(L10n::t("Confirmation completed successfully.") . EOL); + break; + case 1: + // birthday paradox - generate new dfrn-id and fall through. + $new_dfrn_id = Strings::getRandomHex(); + q("UPDATE contact SET `issued-id` = '%s' WHERE `id` = %d AND `uid` = %d", + DBA::escape($new_dfrn_id), + intval($contact_id), + intval($uid) + ); + + case 2: + notice(L10n::t("Temporary failure. Please wait and try again.") . EOL); + break; + case 3: + notice(L10n::t("Introduction failed or was revoked.") . EOL); + break; + } + + if (strlen($message)) { + notice(L10n::t('Remote site reported: ') . $message . EOL); + } + + if (($status == 0) && $intro_id) { + $intro = DBA::selectFirst('intro', ['note'], ['id' => $intro_id]); + if (DBA::isResult($intro)) { + DBA::update('contact', ['reason' => $intro['note']], ['id' => $contact_id]); } - $params['dfrn_version'] = DFRN_PROTOCOL_VERSION; - if ($duplex == 1) { - $params['duplex'] = 1; - } + // Success. Delete the notification. + DBA::delete('intro', ['id' => $intro_id]); + } - if ($user['page-flags'] == User::PAGE_FLAGS_COMMUNITY) { - $params['page'] = 1; - } - - if ($user['page-flags'] == User::PAGE_FLAGS_PRVGROUP) { - $params['page'] = 2; - } - - Logger::log('Confirm: posting data to ' . $dfrn_confirm . ': ' . print_r($params, true), Logger::DATA); - - /* - * - * POST all this stuff to the other site. - * Temporarily raise the network timeout to 120 seconds because the default 60 - * doesn't always give the other side quite enough time to decrypt everything. - * - */ - - $res = Network::post($dfrn_confirm, $params, null, $redirects, 120)->getBody(); - - Logger::log(' Confirm: received data: ' . $res, Logger::DATA); - - // Now figure out what they responded. Try to be robust if the remote site is - // having difficulty and throwing up errors of some kind. - - $leading_junk = substr($res, 0, strpos($res, 'status; - $message = XML::unescape($xml->message); // human readable text of what may have gone wrong. - switch ($status) { - case 0: - info(L10n::t("Confirmation completed successfully.") . EOL); - break; - case 1: - // birthday paradox - generate new dfrn-id and fall through. - $new_dfrn_id = Strings::getRandomHex(); - q("UPDATE contact SET `issued-id` = '%s' WHERE `id` = %d AND `uid` = %d", - DBA::escape($new_dfrn_id), - intval($contact_id), - intval($uid) - ); - - case 2: - notice(L10n::t("Temporary failure. Please wait and try again.") . EOL); - break; - case 3: - notice(L10n::t("Introduction failed or was revoked.") . EOL); - break; - } - - if (strlen($message)) { - notice(L10n::t('Remote site reported: ') . $message . EOL); - } - - if (($status == 0) && $intro_id) { - $intro = DBA::selectFirst('intro', ['note'], ['id' => $intro_id]); - if (DBA::isResult($intro)) { - DBA::update('contact', ['reason' => $intro['note']], ['id' => $contact_id]); - } - - // Success. Delete the notification. - DBA::delete('intro', ['id' => $intro_id]); - } - - if ($status != 0) { - return; - } + if ($status != 0) { + return; } /* @@ -306,101 +294,39 @@ function dfrn_confirm_post(App $a, $handsfree = null) Logger::log('dfrn_confirm: confirm - imported photos'); - if ($network === Protocol::DFRN) { - $new_relation = Contact::FOLLOWER; + $new_relation = Contact::FOLLOWER; - if (($relation == Contact::SHARING) || ($duplex)) { - $new_relation = Contact::FRIEND; - } - - if (($relation == Contact::SHARING) && ($duplex)) { - $duplex = 0; - } - - $r = q("UPDATE `contact` SET `rel` = %d, - `name-date` = '%s', - `uri-date` = '%s', - `blocked` = 0, - `pending` = 0, - `duplex` = %d, - `hidden` = %d, - `network` = '%s' WHERE `id` = %d - ", - intval($new_relation), - DBA::escape(DateTimeFormat::utcNow()), - DBA::escape(DateTimeFormat::utcNow()), - intval($duplex), - intval($hidden), - DBA::escape(Protocol::DFRN), - intval($contact_id) - ); - } else { - if ($network == Protocol::ACTIVITYPUB) { - ActivityPub\Transmitter::sendContactAccept($contact['url'], $contact['hub-verify'], $uid); - // Setting "pending" to true on a bidirectional contact request could create a problem when it isn't accepted on the other side - // Then we have got a situation where - although one direction is accepted - the contact still appears as pending. - // Possibly we need two different "pending" fields, one for incoming, one for outgoing? - // This has to be thought over, but for now this here is a better solution. - // $pending = $duplex; - $pending = false; - } else { - $pending = false; - } - - // $network !== Protocol::DFRN - $network = defaults($contact, 'network', Protocol::OSTATUS); - - $arr = Probe::uri($contact['url'], $network); - - $notify = defaults($contact, 'notify' , $arr['notify']); - $poll = defaults($contact, 'poll' , $arr['poll']); - - $addr = $arr['addr']; - - $new_relation = $contact['rel']; - $writable = $contact['writable']; - - if (in_array($network, [Protocol::DIASPORA, Protocol::ACTIVITYPUB])) { - if ($duplex) { - $new_relation = Contact::FRIEND; - } else { - $new_relation = Contact::FOLLOWER; - } - - if ($new_relation != Contact::FOLLOWER) { - $writable = 1; - } - } - - DBA::delete('intro', ['id' => $intro_id]); - - $fields = ['name-date' => DateTimeFormat::utcNow(), - 'uri-date' => DateTimeFormat::utcNow(), 'addr' => $addr, - 'notify' => $notify, 'poll' => $poll, 'blocked' => false, - 'pending' => $pending, 'network' => $network, - 'writable' => $writable, 'hidden' => $hidden, 'rel' => $new_relation]; - DBA::update('contact', $fields, ['id' => $contact_id]); + if (($relation == Contact::SHARING) || ($duplex)) { + $new_relation = Contact::FRIEND; } - if (!DBA::isResult($r)) { - notice(L10n::t('Unable to set contact photo.') . EOL); + if (($relation == Contact::SHARING) && ($duplex)) { + $duplex = 0; } + $r = q("UPDATE `contact` SET `rel` = %d, + `name-date` = '%s', + `uri-date` = '%s', + `blocked` = 0, + `pending` = 0, + `duplex` = %d, + `hidden` = %d, + `network` = '%s' WHERE `id` = %d + ", + intval($new_relation), + DBA::escape(DateTimeFormat::utcNow()), + DBA::escape(DateTimeFormat::utcNow()), + intval($duplex), + intval($hidden), + DBA::escape(Protocol::DFRN), + intval($contact_id) + ); + // reload contact info $contact = DBA::selectFirst('contact', [], ['id' => $contact_id]); - if ((isset($new_relation) && $new_relation == Contact::FRIEND)) { - if (DBA::isResult($contact) && ($contact['network'] === Protocol::DIASPORA)) { - $ret = Diaspora::sendShare($user, $contact); - Logger::log('share returns: ' . $ret); - } - } Group::addMember(User::getDefaultGroup($uid, $contact["network"]), $contact['id']); - if ($network == Protocol::ACTIVITYPUB && $duplex) { - ActivityPub\Transmitter::sendActivity('Follow', $contact['url'], $uid); - } - // Let's send our user to the contact editor in case they want to // do anything special with this new friend. if ($handsfree === null) { diff --git a/mod/dfrn_notify.php b/mod/dfrn_notify.php index 745411a8e..e75d975a8 100644 --- a/mod/dfrn_notify.php +++ b/mod/dfrn_notify.php @@ -29,7 +29,7 @@ function dfrn_notify_post(App $a) { $user = DBA::selectFirst('user', [], ['nickname' => $nick, 'account_expired' => false, 'account_removed' => false]); if (!DBA::isResult($user)) { - System::httpExit(500); + throw new \Friendica\Network\HTTPException\InternalServerErrorException(); } dfrn_dispatch_private($user, $postdata); } elseif (!dfrn_dispatch_public($postdata)) { @@ -190,13 +190,13 @@ function dfrn_dispatch_public($postdata) } // Fetch the corresponding public contact - $contact = Contact::getDetailsByAddr($msg['author'], 0); - if (!$contact) { + $contact_id = Contact::getIdForURL($msg['author']); + if (empty($contact_id)) { Logger::log('Contact not found for address ' . $msg['author']); System::xmlExit(3, 'Contact ' . $msg['author'] . ' not found'); } - $importer = DFRN::getImporter($contact['id']); + $importer = DFRN::getImporter($contact_id); // This should never fail if (empty($importer)) { diff --git a/mod/dfrn_poll.php b/mod/dfrn_poll.php index 5b72c0bf9..6c849cb80 100644 --- a/mod/dfrn_poll.php +++ b/mod/dfrn_poll.php @@ -50,7 +50,7 @@ function dfrn_poll_init(App $a) if (($dfrn_id === '') && empty($_POST['dfrn_id'])) { if (Config::get('system', 'block_public') && !local_user() && !remote_user()) { - System::httpExit(403); + throw new \Friendica\Network\HTTPException\ForbiddenException(); } $user = ''; @@ -59,7 +59,7 @@ function dfrn_poll_init(App $a) DBA::escape($a->argv[1]) ); if (!$r) { - System::httpExit(404); + throw new \Friendica\Network\HTTPException\NotFoundException(); } $hidewall = ($r[0]['hidewall'] && !local_user()); @@ -483,7 +483,7 @@ function dfrn_poll_content(App $a) // heluecht: I don't know why we don't fail immediately when the user or contact hadn't been found. // Since it doesn't make sense to continue from this point on, we now fail here. This should be safe. if (!DBA::isResult($r)) { - System::httpExit(404, ["title" => L10n::t('Page not found.')]); + throw new \Friendica\Network\HTTPException\NotFoundException(); } // URL reply diff --git a/mod/dfrn_request.php b/mod/dfrn_request.php index 780e0afee..19879c21b 100644 --- a/mod/dfrn_request.php +++ b/mod/dfrn_request.php @@ -476,7 +476,7 @@ function dfrn_request_post(App $a) function dfrn_request_content(App $a) { - if (($a->argc != 2) || (!count($a->profile))) { + if ($a->argc != 2 || empty($a->profile)) { return ""; } diff --git a/mod/directory.php b/mod/directory.php deleted file mode 100644 index 256c9bbbd..000000000 --- a/mod/directory.php +++ /dev/null @@ -1,230 +0,0 @@ -page['aside'] .= Widget::findPeople(); - $a->page['aside'] .= Widget::follow(); - } else { - unset($_SESSION['theme']); - unset($_SESSION['mobile-theme']); - } -} - -function directory_post(App $a) -{ - if (!empty($_POST['search'])) { - $a->data['search'] = $_POST['search']; - } -} - -function directory_content(App $a) -{ - if ((Config::get('system', 'block_public') && !local_user() && !remote_user()) - || (Config::get('system', 'block_local_dir') && !local_user() && !remote_user()) - ) { - notice(L10n::t('Public access denied.') . EOL); - return; - } - - $o = ''; - $entries = []; - - Nav::setSelected('directory'); - - if (!empty($a->data['search'])) { - $search = Strings::escapeTags(trim($a->data['search'])); - } else { - $search = (!empty($_GET['search']) ? Strings::escapeTags(trim(rawurldecode($_GET['search']))) : ''); - } - - $gdirpath = ''; - $dirurl = Config::get('system', 'directory'); - if (strlen($dirurl)) { - $gdirpath = Profile::zrl($dirurl, true); - } - - if ($search) { - $search = DBA::escape($search); - - $sql_extra = " AND ((`profile`.`name` LIKE '%$search%') OR - (`user`.`nickname` LIKE '%$search%') OR - (`profile`.`pdesc` LIKE '%$search%') OR - (`profile`.`locality` LIKE '%$search%') OR - (`profile`.`region` LIKE '%$search%') OR - (`profile`.`country-name` LIKE '%$search%') OR - (`profile`.`gender` LIKE '%$search%') OR - (`profile`.`marital` LIKE '%$search%') OR - (`profile`.`sexual` LIKE '%$search%') OR - (`profile`.`about` LIKE '%$search%') OR - (`profile`.`romance` LIKE '%$search%') OR - (`profile`.`work` LIKE '%$search%') OR - (`profile`.`education` LIKE '%$search%') OR - (`profile`.`pub_keywords` LIKE '%$search%') OR - (`profile`.`prv_keywords` LIKE '%$search%'))"; - } else { - $sql_extra = ''; - } - - $publish = (Config::get('system', 'publish_all') ? '' : " AND `publish` = 1 " ); - - - $total = 0; - $cnt = DBA::fetchFirst("SELECT COUNT(*) AS `total` FROM `profile` - LEFT JOIN `user` ON `user`.`uid` = `profile`.`uid` - WHERE `is-default` $publish AND NOT `user`.`blocked` AND NOT `user`.`account_removed` $sql_extra"); - if (DBA::isResult($cnt)) { - $total = $cnt['total']; - } - $pager = new Pager($a->query_string, 60); - - $order = " ORDER BY `name` ASC "; - - $limit = $pager->getStart()."," . $pager->getItemsPerPage(); - - $r = DBA::p("SELECT `profile`.*, `profile`.`uid` AS `profile_uid`, `user`.`nickname`, `user`.`timezone` , `user`.`page-flags`, - `contact`.`addr`, `contact`.`url` AS `profile_url` FROM `profile` - LEFT JOIN `user` ON `user`.`uid` = `profile`.`uid` - LEFT JOIN `contact` ON `contact`.`uid` = `user`.`uid` - WHERE `is-default` $publish AND NOT `user`.`blocked` AND NOT `user`.`account_removed` AND `contact`.`self` - $sql_extra $order LIMIT $limit" - ); - if (DBA::isResult($r)) { - if (in_array('small', $a->argv)) { - $photo = 'thumb'; - } else { - $photo = 'photo'; - } - - while ($rr = DBA::fetch($r)) { - $entries[] = format_directory_entry($rr, $photo); - } - DBA::close($r); - } else { - info(L10n::t("No entries \x28some entries may be hidden\x29.") . EOL); - } - - $tpl = Renderer::getMarkupTemplate('directory_header.tpl'); - - $o .= Renderer::replaceMacros($tpl, [ - '$search' => $search, - '$globaldir' => L10n::t('Global Directory'), - '$gdirpath' => $gdirpath, - '$desc' => L10n::t('Find on this site'), - '$contacts' => $entries, - '$finding' => L10n::t('Results for:'), - '$findterm' => (strlen($search) ? $search : ""), - '$title' => L10n::t('Site Directory'), - '$search_mod' => 'directory', - '$submit' => L10n::t('Find'), - '$paginate' => $pager->renderFull($total), - ]); - - return $o; -} - -/** - * Format contact/profile/user data from the database into an usable - * array for displaying directory entries. - * - * @param array $arr The directory entry from the database. - * @param string $photo_size Avatar size (thumb, photo or micro). - * - * @return array - */ -function format_directory_entry(array $arr, $photo_size = 'photo') -{ - $itemurl = (($arr['addr'] != "") ? $arr['addr'] : $arr['profile_url']); - - $profile_link = $arr['profile_url']; - - $pdesc = (($arr['pdesc']) ? $arr['pdesc'] . '
' : ''); - - $details = ''; - if (strlen($arr['locality'])) { - $details .= $arr['locality']; - } - if (strlen($arr['region'])) { - if (strlen($arr['locality'])) { - $details .= ', '; - } - $details .= $arr['region']; - } - if (strlen($arr['country-name'])) { - if (strlen($details)) { - $details .= ', '; - } - $details .= $arr['country-name']; - } - - $profile = $arr; - - if (!empty($profile['address']) - || !empty($profile['locality']) - || !empty($profile['region']) - || !empty($profile['postal-code']) - || !empty($profile['country-name']) - ) { - $location = L10n::t('Location:'); - } else { - $location = ''; - } - - $gender = (!empty($profile['gender']) ? L10n::t('Gender:') : false); - $marital = (!empty($profile['marital']) ? L10n::t('Status:') : false); - $homepage = (!empty($profile['homepage']) ? L10n::t('Homepage:') : false); - $about = (!empty($profile['about']) ? L10n::t('About:') : false); - - $location_e = $location; - - $photo_menu = [ - 'profile' => [L10n::t("View Profile"), Contact::magicLink($profile_link)] - ]; - - $entry = [ - 'id' => $arr['id'], - 'url' => Contact::magicLInk($profile_link), - 'itemurl' => $itemurl, - 'thumb' => ProxyUtils::proxifyUrl($arr[$photo_size], false, ProxyUtils::SIZE_THUMB), - 'img_hover' => $arr['name'], - 'name' => $arr['name'], - 'details' => $details, - 'account_type' => Contact::getAccountType($arr), - 'profile' => $profile, - 'location' => $location_e, - 'tags' => $arr['pub_keywords'], - 'gender' => $gender, - 'pdesc' => $pdesc, - 'marital' => $marital, - 'homepage' => $homepage, - 'about' => $about, - 'photo_menu' => $photo_menu, - - ]; - - $hook = ['contact' => $arr, 'entry' => $entry]; - - Hook::callAll('directory_item', $hook); - - unset($profile); - unset($location); - - return $hook['entry']; -} diff --git a/mod/dirfind.php b/mod/dirfind.php deleted file mode 100644 index a5b77312f..000000000 --- a/mod/dirfind.php +++ /dev/null @@ -1,265 +0,0 @@ -page['aside'])) { - $a->page['aside'] = ''; - } - - $a->page['aside'] .= Widget::findPeople(); - - $a->page['aside'] .= Widget::follow(); -} - -function dirfind_content(App $a, $prefix = "") { - - $community = false; - $discover_user = false; - - $local = Config::get('system','poco_local_search'); - - $search = $prefix.Strings::escapeTags(trim(defaults($_REQUEST, 'search', ''))); - - $header = ''; - - if (strpos($search,'@') === 0) { - $search = substr($search,1); - $header = L10n::t('People Search - %s', $search); - if ((filter_var($search, FILTER_VALIDATE_EMAIL) && Network::isEmailDomainValid($search)) || - (substr(Strings::normaliseLink($search), 0, 7) == "http://")) { - $user_data = Probe::uri($search); - $discover_user = (in_array($user_data["network"], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::OSTATUS, Protocol::DIASPORA])); - } - } - - if (strpos($search,'!') === 0) { - $search = substr($search,1); - $community = true; - $header = L10n::t('Forum Search - %s', $search); - } - - $o = ''; - - if ($search) { - $pager = new Pager($a->query_string); - - if ($discover_user) { - $j = new stdClass(); - $j->total = 1; - $j->items_page = 1; - $j->page = $pager->getPage(); - - $objresult = new stdClass(); - $objresult->cid = 0; - $objresult->name = $user_data["name"]; - $objresult->addr = $user_data["addr"]; - $objresult->url = $user_data["url"]; - $objresult->photo = $user_data["photo"]; - $objresult->tags = ""; - $objresult->network = $user_data["network"]; - - $contact = Model\Contact::getDetailsByURL($user_data["url"], local_user()); - $objresult->cid = $contact["cid"]; - $objresult->pcid = $contact["zid"]; - - $j->results[] = $objresult; - - // Add the contact to the global contacts if it isn't already in our system - if (($contact["cid"] == 0) && ($contact["zid"] == 0) && ($contact["gid"] == 0)) { - Model\GContact::update($user_data); - } - } elseif ($local) { - if ($community) { - $extra_sql = " AND `community`"; - } else { - $extra_sql = ""; - } - - $pager->setItemsPerPage(80); - - if (Config::get('system','diaspora_enabled')) { - $diaspora = Protocol::DIASPORA; - } else { - $diaspora = Protocol::DFRN; - } - - if (!Config::get('system','ostatus_disabled')) { - $ostatus = Protocol::OSTATUS; - } else { - $ostatus = Protocol::DFRN; - } - - $search2 = "%".$search."%"; - - /// @TODO These 2 SELECTs are not checked on validity with DBA::isResult() - $count = q("SELECT count(*) AS `total` FROM `gcontact` - WHERE NOT `hide` AND `network` IN ('%s', '%s', '%s', '%s') AND - ((`last_contact` >= `last_failure`) OR (`updated` >= `last_failure`)) AND - (`url` LIKE '%s' OR `name` LIKE '%s' OR `location` LIKE '%s' OR - `addr` LIKE '%s' OR `about` LIKE '%s' OR `keywords` LIKE '%s') $extra_sql", - DBA::escape(Protocol::ACTIVITYPUB), DBA::escape(Protocol::DFRN), DBA::escape($ostatus), DBA::escape($diaspora), - DBA::escape(Strings::escapeHtml($search2)), DBA::escape(Strings::escapeHtml($search2)), DBA::escape(Strings::escapeHtml($search2)), - DBA::escape(Strings::escapeHtml($search2)), DBA::escape(Strings::escapeHtml($search2)), DBA::escape(Strings::escapeHtml($search2))); - - $results = q("SELECT `nurl` - FROM `gcontact` - WHERE NOT `hide` AND `network` IN ('%s', '%s', '%s', '%s') AND - ((`last_contact` >= `last_failure`) OR (`updated` >= `last_failure`)) AND - (`url` LIKE '%s' OR `name` LIKE '%s' OR `location` LIKE '%s' OR - `addr` LIKE '%s' OR `about` LIKE '%s' OR `keywords` LIKE '%s') $extra_sql - GROUP BY `nurl` - ORDER BY `updated` DESC LIMIT %d, %d", - DBA::escape(Protocol::ACTIVITYPUB), DBA::escape(Protocol::DFRN), DBA::escape($ostatus), DBA::escape($diaspora), - DBA::escape(Strings::escapeHtml($search2)), DBA::escape(Strings::escapeHtml($search2)), DBA::escape(Strings::escapeHtml($search2)), - DBA::escape(Strings::escapeHtml($search2)), DBA::escape(Strings::escapeHtml($search2)), DBA::escape(Strings::escapeHtml($search2)), - $pager->getStart(), $pager->getItemsPerPage()); - $j = new stdClass(); - $j->total = $count[0]["total"]; - $j->items_page = $pager->getItemsPerPage(); - $j->page = $pager->getPage(); - foreach ($results AS $result) { - if (PortableContact::alternateOStatusUrl($result["nurl"])) { - continue; - } - - $urlparts = parse_url($result["nurl"]); - - // Ignore results that look strange. - // For historic reasons the gcontact table does contain some garbage. - if (!empty($urlparts['query']) || !empty($urlparts['fragment'])) { - continue; - } - - $result = Model\Contact::getDetailsByURL($result["nurl"], local_user()); - - if ($result["name"] == "") { - $result["name"] = end(explode("/", $urlparts["path"])); - } - - $objresult = new stdClass(); - $objresult->cid = $result["cid"]; - $objresult->pcid = $result["zid"]; - $objresult->name = $result["name"]; - $objresult->addr = $result["addr"]; - $objresult->url = $result["url"]; - $objresult->photo = $result["photo"]; - $objresult->tags = $result["keywords"]; - $objresult->network = $result["network"]; - - $j->results[] = $objresult; - } - - // Add found profiles from the global directory to the local directory - Worker::add(PRIORITY_LOW, 'DiscoverPoCo', "dirsearch", urlencode($search)); - } elseif (strlen(Config::get('system','directory'))) { - $p = (($pager->getPage() != 1) ? '&p=' . $pager->getPage() : ''); - - $x = Network::fetchUrl(get_server() . '/lsearch?f=' . $p . '&search=' . urlencode($search)); - - $j = json_decode($x); - $pager->setItemsPerPage($j->items_page); - } - - if (!empty($j->results)) { - $id = 0; - - $entries = []; - foreach ($j->results as $jj) { - - $alt_text = ""; - - $contact_details = Model\Contact::getDetailsByURL($jj->url, local_user()); - - $itemurl = (($contact_details["addr"] != "") ? $contact_details["addr"] : $jj->url); - - // If We already know this contact then don't show the "connect" button - if ($jj->cid > 0) { - $connlnk = ""; - $conntxt = ""; - $contact = DBA::selectFirst('contact', [], ['id' => $jj->cid]); - if (DBA::isResult($contact)) { - $photo_menu = Model\Contact::photoMenu($contact); - $details = Module\Contact::getContactTemplateVars($contact); - $alt_text = $details['alt_text']; - } else { - $photo_menu = []; - } - } else { - $connlnk = System::baseUrl().'/follow/?url='.(!empty($jj->connect) ? $jj->connect : $jj->url); - $conntxt = L10n::t('Connect'); - - $contact = DBA::selectFirst('contact', [], ['id' => $jj->pcid]); - if (DBA::isResult($contact)) { - $photo_menu = Model\Contact::photoMenu($contact); - } else { - $photo_menu = []; - } - - $photo_menu['profile'] = [L10n::t("View Profile"), Model\Contact::magicLink($jj->url)]; - $photo_menu['follow'] = [L10n::t("Connect/Follow"), $connlnk]; - } - - $jj->photo = str_replace("http:///photo/", get_server()."/photo/", $jj->photo); - - $entry = [ - 'alt_text' => $alt_text, - 'url' => Model\Contact::magicLink($jj->url), - 'itemurl' => $itemurl, - 'name' => $jj->name, - 'thumb' => ProxyUtils::proxifyUrl($jj->photo, false, ProxyUtils::SIZE_THUMB), - 'img_hover' => $jj->tags, - 'conntxt' => $conntxt, - 'connlnk' => $connlnk, - 'photo_menu' => $photo_menu, - 'details' => $contact_details['location'], - 'tags' => $contact_details['keywords'], - 'about' => $contact_details['about'], - 'account_type' => Model\Contact::getAccountType($contact_details), - 'network' => ContactSelector::networkToName($jj->network, $jj->url), - 'id' => ++$id, - ]; - $entries[] = $entry; - } - - $tpl = Renderer::getMarkupTemplate('viewcontact_template.tpl'); - $o .= Renderer::replaceMacros($tpl,[ - 'title' => $header, - '$contacts' => $entries, - '$paginate' => $pager->renderFull($j->total), - ]); - } else { - info(L10n::t('No matches') . EOL); - } - - } - - return $o; -} diff --git a/mod/display.php b/mod/display.php index acc2a5b09..54d479259 100644 --- a/mod/display.php +++ b/mod/display.php @@ -20,6 +20,7 @@ use Friendica\Model\Group; use Friendica\Model\Item; use Friendica\Model\Profile; use Friendica\Module\Objects; +use Friendica\Network\HTTPException; use Friendica\Protocol\ActivityPub; use Friendica\Protocol\DFRN; use Friendica\Util\Strings; @@ -76,7 +77,11 @@ function display_init(App $a) } if (!DBA::isResult($item)) { - System::httpExit(404); + return; + } + + if ($a->argc >= 3 && $nick == 'feed-item') { + displayShowFeed($item['id'], $a->argc > 3 && $a->argv[3] == 'conversation.atom'); } if ($a->argc >= 3 && $nick == 'feed-item') { @@ -185,23 +190,13 @@ function display_fetchauthor($a, $item) $profiledata["photo"] = System::removedBaseUrl($profiledata["photo"]); - if (local_user()) { - if (in_array($profiledata["network"], [Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS])) { - $profiledata["remoteconnect"] = System::baseUrl()."/follow?url=".urlencode($profiledata["url"]); - } - } elseif ($profiledata["network"] == Protocol::DFRN) { - $connect = str_replace("/profile/", "/dfrn_request/", $profiledata["url"]); - $profiledata["remoteconnect"] = $connect; - } - - return($profiledata); + return $profiledata; } function display_content(App $a, $update = false, $update_uid = 0) { if (Config::get('system','block_public') && !local_user() && !remote_user()) { - notice(L10n::t('Public access denied.') . EOL); - return; + throw new HTTPException\ForbiddenException(L10n::t('Public access denied.')); } $o = ''; @@ -254,7 +249,7 @@ function display_content(App $a, $update = false, $update_uid = 0) } if (!$item_id) { - System::httpExit(404); + throw new HTTPException\NotFoundException(L10n::t('The requested item doesn\'t exist or has been deleted.')); } // We are displaying an "alternate" link if that post was public. See issue 2864 @@ -303,8 +298,7 @@ function display_content(App $a, $update = false, $update_uid = 0) $is_owner = (local_user() && (in_array($a->profile['profile_uid'], [local_user(), 0])) ? true : false); if (!empty($a->profile['hidewall']) && !$is_owner && !$is_remote_contact) { - notice(L10n::t('Access to this profile has been restricted.') . EOL); - return; + throw new HTTPException\ForbiddenException(L10n::t('Access to this profile has been restricted.')); } // We need the editor here to be able to reshare an item. @@ -340,7 +334,7 @@ function display_content(App $a, $update = false, $update_uid = 0) $item = Item::selectFirstForUser(local_user(), $fields, $condition); if (!DBA::isResult($item)) { - System::httpExit(404); + throw new HTTPException\NotFoundException(L10n::t('The requested item doesn\'t exist or has been deleted.')); } $item['uri'] = $item['parent-uri']; @@ -415,7 +409,7 @@ function displayShowFeed($item_id, $conversation) { $xml = DFRN::itemFeed($item_id, $conversation); if ($xml == '') { - System::httpExit(500); + throw new HTTPException\InternalServerErrorException(L10n::t('The feed for this item is unavailable.')); } header("Content-type: application/atom+xml"); echo $xml; diff --git a/mod/editpost.php b/mod/editpost.php index c1c0d16d7..e14baffa2 100644 --- a/mod/editpost.php +++ b/mod/editpost.php @@ -8,7 +8,6 @@ use Friendica\Content\Feature; use Friendica\Core\Hook; use Friendica\Core\L10n; use Friendica\Core\Renderer; -use Friendica\Core\System; use Friendica\Model\FileTag; use Friendica\Model\Item; use Friendica\Database\DBA; @@ -48,7 +47,6 @@ function editpost_content(App $a) $tpl = Renderer::getMarkupTemplate('jot-header.tpl'); $a->page['htmlhead'] .= Renderer::replaceMacros($tpl, [ - '$baseurl' => System::baseUrl(), '$ispublic' => ' ', // L10n::t('Visible to everybody'), '$geotag' => $geotag, '$nickname' => $a->user['nickname'] @@ -91,7 +89,6 @@ function editpost_content(App $a) '$posttype' => $item['post-type'], '$content' => undo_post_tagging($item['body']), '$post_id' => $post_id, - '$baseurl' => System::baseUrl(), '$defloc' => $a->user['default-location'], '$visitor' => 'none', '$pvisit' => 'none', diff --git a/mod/events.php b/mod/events.php index 008cf643c..86cec9a7d 100644 --- a/mod/events.php +++ b/mod/events.php @@ -21,6 +21,7 @@ use Friendica\Module\Login; use Friendica\Util\DateTimeFormat; use Friendica\Util\Strings; use Friendica\Util\Temporal; +use Friendica\Worker\Delivery; function events_init(App $a) { @@ -195,7 +196,7 @@ function events_post(App $a) $item_id = Event::store($datarray); if (!$cid) { - Worker::add(PRIORITY_HIGH, "Notifier", "event", $item_id); + Worker::add(PRIORITY_HIGH, "Notifier", Delivery::POST, $item_id); } $a->internalRedirect('events'); @@ -237,7 +238,6 @@ function events_content(App $a) $htpl = Renderer::getMarkupTemplate('event_head.tpl'); $a->page['htmlhead'] .= Renderer::replaceMacros($htpl, [ - '$baseurl' => System::baseUrl(), '$module_url' => '/events', '$modparams' => 1, '$i18n' => $i18n, @@ -247,7 +247,7 @@ function events_content(App $a) $tabs = ''; // tabs if ($a->theme_events_in_profile) { - $tabs = Profile::getTabs($a, true); + $tabs = Profile::getTabs($a, 'events', true); } $mode = 'view'; @@ -379,7 +379,6 @@ function events_content(App $a) } $o = Renderer::replaceMacros($tpl, [ - '$baseurl' => System::baseUrl(), '$tabs' => $tabs, '$title' => L10n::t('Events'), '$view' => L10n::t('View'), diff --git a/mod/fbrowser.php b/mod/fbrowser.php index 559896acb..f2bccb085 100644 --- a/mod/fbrowser.php +++ b/mod/fbrowser.php @@ -11,6 +11,7 @@ use Friendica\Core\Renderer; use Friendica\Core\System; use Friendica\Database\DBA; use Friendica\Object\Image; +use Friendica\Util\Strings; /** * @param App $a @@ -27,6 +28,12 @@ function fbrowser_content(App $a) exit(); } + // Needed to match the correct template in a module that uses a different theme than the user/site/default + $theme = Strings::sanitizeFilePathItem(defaults($_GET, 'theme', null)); + if ($theme && is_file("view/theme/$theme/config.php")) { + $a->setCurrentTheme($theme); + } + $template_file = "filebrowser.tpl"; $o = ''; @@ -97,7 +104,6 @@ function fbrowser_content(App $a) $o = Renderer::replaceMacros($tpl, [ '$type' => 'image', - '$baseurl' => System::baseUrl(), '$path' => $path, '$folders' => $albums, '$files' => $files, @@ -127,7 +133,6 @@ function fbrowser_content(App $a) $tpl = Renderer::getMarkupTemplate($template_file); $o = Renderer::replaceMacros($tpl, [ '$type' => 'file', - '$baseurl' => System::baseUrl(), '$path' => [ [ "", L10n::t("Files")] ], '$folders' => false, '$files' => $files, diff --git a/mod/feedtest.php b/mod/feedtest.php deleted file mode 100644 index cffc1f345..000000000 --- a/mod/feedtest.php +++ /dev/null @@ -1,50 +0,0 @@ - local_user()]); - - $contact_id = Contact::getIdForURL($url, local_user(), true); - - $contact = DBA::selectFirst('contact', [], ['id' => $contact_id]); - - $xml = Network::fetchUrl($contact['poll']); - - $dummy = null; - $import_result = Feed::import($xml, $importer, $contact, $dummy, true); - - $result = [ - 'input' => $xml, - 'output' => var_export($import_result, true), - ]; - } - - $tpl = Renderer::getMarkupTemplate('feedtest.tpl'); - $o = Renderer::replaceMacros($tpl, [ - '$url' => ['url', L10n::t('Source URL'), defaults($_REQUEST, 'url', ''), ''], - '$result' => $result - ]); - - return $o; -} diff --git a/mod/fetch.php b/mod/fetch.php deleted file mode 100644 index 3e9c4e662..000000000 --- a/mod/fetch.php +++ /dev/null @@ -1,62 +0,0 @@ -argc != 3) || (!in_array($a->argv[1], ["post", "status_message", "reshare"]))) { - System::httpExit(404); - } - - $guid = $a->argv[2]; - - // Fetch the item - $fields = ['uid', 'title', 'body', 'guid', 'contact-id', 'private', 'created', 'app', 'location', 'coord', 'network', - 'event-id', 'resource-id', 'author-link', 'author-avatar', 'author-name', 'plink', 'owner-link', 'attach']; - $condition = ['wall' => true, 'private' => false, 'guid' => $guid, 'network' => [Protocol::DFRN, Protocol::DIASPORA]]; - $item = Item::selectFirst($fields, $condition); - if (!DBA::isResult($item)) { - $condition = ['guid' => $guid, 'network' => [Protocol::DFRN, Protocol::DIASPORA]]; - $item = Item::selectFirst(['author-link'], $condition); - if (DBA::isResult($item)) { - $parts = parse_url($item["author-link"]); - $host = $parts["scheme"]."://".$parts["host"]; - - if (Strings::normaliseLink($host) != Strings::normaliseLink(System::baseUrl())) { - $location = $host."/fetch/".$a->argv[1]."/".urlencode($guid); - - header("HTTP/1.1 301 Moved Permanently"); - header("Location:".$location); - exit(); - } - } - - System::httpExit(404); - } - - // Fetch some data from the author (We could combine both queries - but I think this is more readable) - $user = User::getOwnerDataById($item["uid"]); - if (!$user) { - System::httpExit(404); - } - - $status = Diaspora::buildStatus($item, $user); - $xml = Diaspora::buildPostXml($status["type"], $status["message"]); - - // Send the envelope - header("Content-Type: application/magic-envelope+xml; charset=utf-8"); - echo Diaspora::buildMagicEnvelope($xml, $user); - - exit(); -} diff --git a/mod/filer.php b/mod/filer.php deleted file mode 100644 index 0d9afe638..000000000 --- a/mod/filer.php +++ /dev/null @@ -1,42 +0,0 @@ -argc > 1) ? intval($a->argv[1]) : 0); - - Logger::log('filer: tag ' . $term . ' item ' . $item_id); - - if ($item_id && strlen($term)) { - // file item - FileTag::saveFile(local_user(), $item_id, $term); - } else { - // return filer dialog - $filetags = PConfig::get(local_user(), 'system', 'filetags'); - $filetags = FileTag::fileToList($filetags, 'file'); - $filetags = explode(",", $filetags); - - $tpl = Renderer::getMarkupTemplate("filer_dialog.tpl"); - $o = Renderer::replaceMacros($tpl, [ - '$field' => ['term', L10n::t("Save to Folder:"), '', '', $filetags, L10n::t('- select -')], - '$submit' => L10n::t('Save'), - ]); - - echo $o; - } - exit(); -} diff --git a/mod/filerm.php b/mod/filerm.php deleted file mode 100644 index 9013dd62b..000000000 --- a/mod/filerm.php +++ /dev/null @@ -1,40 +0,0 @@ -argc > 1) ? intval($a->argv[1]) : 0); - - Logger::log('filerm: tag ' . $term . ' item ' . $item_id . ' category ' . ($category ? 'true' : 'false')); - - if ($item_id && strlen($term)) { - if (FileTag::unsaveFile(local_user(), $item_id, $term, $category)) { - info('Item removed'); - } - } - else { - info('Item was not deleted'); - } - - $a->internalRedirect('/network?f=&file=' . rawurlencode($term)); - exit(); -} diff --git a/mod/follow.php b/mod/follow.php index ba4ff35f1..8a00e0559 100644 --- a/mod/follow.php +++ b/mod/follow.php @@ -17,7 +17,7 @@ use Friendica\Util\Strings; function follow_post(App $a) { if (!local_user()) { - System::httpExit(403, ['title' => L10n::t('Access denied.')]); + throw new \Friendica\Network\HTTPException\ForbiddenException(L10n::t('Access denied.')); } if (isset($_REQUEST['cancel'])) { @@ -91,32 +91,34 @@ function follow_content(App $a) $ret = Probe::uri($url); - if (($ret['network'] == Protocol::DIASPORA) && !Config::get('system', 'diaspora_enabled')) { + $protocol = Contact::getProtocol($ret['url'], $ret['network']); + + if (($protocol == Protocol::DIASPORA) && !Config::get('system', 'diaspora_enabled')) { notice(L10n::t("Diaspora support isn't enabled. Contact can't be added.")); $submit = ''; //$a->internalRedirect($_SESSION['return_path']); // NOTREACHED } - if (($ret['network'] == Protocol::OSTATUS) && Config::get('system', 'ostatus_disabled')) { + if (($protocol == Protocol::OSTATUS) && Config::get('system', 'ostatus_disabled')) { notice(L10n::t("OStatus support is disabled. Contact can't be added.")); $submit = ''; //$a->internalRedirect($_SESSION['return_path']); // NOTREACHED } - if ($ret['network'] == Protocol::PHANTOM) { + if ($protocol == Protocol::PHANTOM) { notice(L10n::t("The network type couldn't be detected. Contact can't be added.")); $submit = ''; //$a->internalRedirect($_SESSION['return_path']); // NOTREACHED } - if ($ret['network'] == Protocol::MAIL) { + if ($protocol == Protocol::MAIL) { $ret['url'] = $ret['addr']; } - if (($ret['network'] === Protocol::DFRN) && !DBA::isResult($r)) { + if (($protocol === Protocol::DFRN) && !DBA::isResult($r)) { $request = $ret['request']; $tpl = Renderer::getMarkupTemplate('dfrn_request.tpl'); } else { @@ -147,7 +149,7 @@ function follow_content(App $a) $gcontact_id = $r[0]['id']; } - if ($ret['network'] === Protocol::DIASPORA) { + if ($protocol === Protocol::DIASPORA) { $r[0]['location'] = ''; $r[0]['about'] = ''; } diff --git a/mod/friendica.php b/mod/friendica.php deleted file mode 100644 index 4942e4c8f..000000000 --- a/mod/friendica.php +++ /dev/null @@ -1,141 +0,0 @@ -argv[1]) && ($a->argv[1] == "json")) { - $register_policies = [ - Register::CLOSED => 'REGISTER_CLOSED', - Register::APPROVE => 'REGISTER_APPROVE', - Register::OPEN => 'REGISTER_OPEN' - ]; - - $register_policy_int = intval(Config::get('config', 'register_policy')); - if ($register_policy_int !== Register::CLOSED && Config::get('config', 'invitation_only')) { - $register_policy = 'REGISTER_INVITATION'; - } else { - $register_policy = $register_policies[$register_policy_int]; - } - - $condition = []; - $admin = false; - if (!empty(Config::get('config', 'admin_nickname'))) { - $condition['nickname'] = Config::get('config', 'admin_nickname'); - } - if (!empty(Config::get('config', 'admin_email'))) { - $adminlist = explode(",", str_replace(" ", "", Config::get('config', 'admin_email'))); - $condition['email'] = $adminlist[0]; - $administrator = DBA::selectFirst('user', ['username', 'nickname'], $condition); - if (DBA::isResult($administrator)) { - $admin = [ - 'name' => $administrator['username'], - 'profile'=> System::baseUrl() . '/profile/' . $administrator['nickname'], - ]; - } - } - - $visible_addons = Addon::getVisibleList(); - - Config::load('feature_lock'); - $locked_features = []; - $featureLock = Config::get('config', 'feature_lock'); - if (isset($featureLock)) { - foreach ($featureLock as $k => $v) { - if ($k === 'config_loaded') { - continue; - } - - $locked_features[$k] = intval($v); - } - } - - $data = [ - 'version' => FRIENDICA_VERSION, - 'url' => System::baseUrl(), - 'addons' => $visible_addons, - 'locked_features' => $locked_features, - 'explicit_content' => (int)Config::get('system', 'explicit_content', false), - 'language' => Config::get('system','language'), - 'register_policy' => $register_policy, - 'admin' => $admin, - 'site_name' => Config::get('config', 'sitename'), - 'platform' => FRIENDICA_PLATFORM, - 'info' => Config::get('config', 'info'), - 'no_scrape_url' => System::baseUrl().'/noscrape' - ]; - - header('Content-type: application/json; charset=utf-8'); - echo json_encode($data); - exit(); - } -} - -function friendica_content(App $a) -{ - $o = '

Friendica

' . PHP_EOL; - $o .= '

'; - $o .= L10n::t('This is Friendica, version %s that is running at the web location %s. The database version is %s, the post update version is %s.', - '' . FRIENDICA_VERSION . '', System::baseUrl(), '' . DB_UPDATE_VERSION . '', - '' . Config::get("system", "post_update_version") . ''); - $o .= '

' . PHP_EOL; - - $o .= '

'; - $o .= L10n::t('Please visit Friendi.ca to learn more about the Friendica project.') . PHP_EOL; - $o .= '

' . PHP_EOL; - - $o .= '

'; - $o .= L10n::t('Bug reports and issues: please visit') . ' ' . ''.L10n::t('the bugtracker at github').''; - $o .= '

' . PHP_EOL; - $o .= '

'; - $o .= L10n::t('Suggestions, praise, etc. - please email "info" at "friendi - dot - ca'); - $o .= '

' . PHP_EOL; - - $visible_addons = Addon::getVisibleList(); - if (count($visible_addons)) { - $o .= '

' . L10n::t('Installed addons/apps:') . '

' . PHP_EOL; - $sorted = $visible_addons; - $s = ''; - sort($sorted); - foreach ($sorted as $p) { - if (strlen($p)) { - if (strlen($s)) { - $s .= ', '; - } - $s .= $p; - } - } - $o .= '
' . $s . '
' . PHP_EOL; - } else { - $o .= '

' . L10n::t('No installed addons/apps') . '

' . PHP_EOL; - } - - if (Config::get('system', 'tosdisplay')) - { - $o .= '

'.L10n::t('Read about the Terms of Service of this node.', System::baseurl()).'

'; - } - - $blocklist = Config::get('system', 'blocklist', []); - if (!empty($blocklist)) { - $o .= '

' . L10n::t('On this server the following remote servers are blocked.') . '

' . PHP_EOL; - $o .= '' . PHP_EOL; - foreach ($blocklist as $b) { - $o .= '' . PHP_EOL; - } - $o .= '
' . L10n::t('Blocked domain') . '' . L10n::t('Reason for the block') . '
' . $b['domain'] .'' . $b['reason'] . '
' . PHP_EOL; - } - - Hook::callAll('about_hook', $o); - - return $o; -} diff --git a/mod/fsuggest.php b/mod/fsuggest.php index 58bb11670..2bddf4813 100644 --- a/mod/fsuggest.php +++ b/mod/fsuggest.php @@ -10,10 +10,11 @@ use Friendica\Core\Worker; use Friendica\Database\DBA; use Friendica\Util\DateTimeFormat; use Friendica\Util\Strings; +use Friendica\Worker\Delivery; function fsuggest_post(App $a) { - if (! local_user()) { + if (!local_user()) { return; } @@ -22,53 +23,38 @@ function fsuggest_post(App $a) } $contact_id = intval($a->argv[1]); + if (empty($contact_id)) { + return; + } - $contact = DBA::selectFirst('contact', [], ['id' => $contact_id, 'uid' => local_user()]); - if (! DBA::isResult($contact)) { + // We do query the "uid" as well to ensure that it is our contact + if (!DBA::exists('contact', ['id' => $contact_id, 'uid' => local_user()])) { notice(L10n::t('Contact not found.') . EOL); return; } - $new_contact = intval($_POST['suggest']); + $suggest_contact_id = intval($_POST['suggest']); + if (empty($suggest_contact_id)) { + return; + } - $hash = Strings::getRandomHex(); + // We do query the "uid" as well to ensure that it is our contact + $contact = DBA::selectFirst('contact', ['name', 'url', 'request', 'avatar'], ['id' => $suggest_contact_id, 'uid' => local_user()]); + if (!DBA::isResult($contact)) { + notice(L10n::t('Suggested contact not found.') . EOL); + return; + } $note = Strings::escapeHtml(trim(defaults($_POST, 'note', ''))); - if ($new_contact) { - $r = q("SELECT * FROM `contact` WHERE `id` = %d AND `uid` = %d LIMIT 1", - intval($new_contact), - intval(local_user()) - ); - if (DBA::isResult($r)) { - q("INSERT INTO `fsuggest` ( `uid`,`cid`,`name`,`url`,`request`,`photo`,`note`,`created`) - VALUES ( %d, %d, '%s','%s','%s','%s','%s','%s')", - intval(local_user()), - intval($contact_id), - DBA::escape($contact['name']), - DBA::escape($contact['url']), - DBA::escape($contact['request']), - DBA::escape($contact['photo']), - DBA::escape($hash), - DBA::escape(DateTimeFormat::utcNow()) - ); - $r = q("SELECT `id` FROM `fsuggest` WHERE `note` = '%s' AND `uid` = %d LIMIT 1", - DBA::escape($hash), - intval(local_user()) - ); - if (DBA::isResult($r)) { - $fsuggest_id = $contact['id']; - q("UPDATE `fsuggest` SET `note` = '%s' WHERE `id` = %d AND `uid` = %d", - DBA::escape($note), - intval($fsuggest_id), - intval(local_user()) - ); - Worker::add(PRIORITY_HIGH, 'Notifier', 'suggest', $fsuggest_id); - } + $fields = ['uid' => local_user(),'cid' => $contact_id, 'name' => $contact['name'], + 'url' => $contact['url'], 'request' => $contact['request'], + 'photo' => $contact['avatar'], 'note' => $note, 'created' => DateTimeFormat::utcNow()]; + DBA::insert('fsuggest', $fields); - info(L10n::t('Friend suggestion sent.') . EOL); - } - } + Worker::add(PRIORITY_HIGH, 'Notifier', Delivery::SUGGESTION, DBA::lastInsertId()); + + info(L10n::t('Friend suggestion sent.') . EOL); } function fsuggest_content(App $a) diff --git a/mod/hcard.php b/mod/hcard.php index cbaebc8ff..828eeaf09 100644 --- a/mod/hcard.php +++ b/mod/hcard.php @@ -17,9 +17,7 @@ function hcard_init(App $a) if ($a->argc > 1) { $which = $a->argv[1]; } else { - notice(L10n::t('No profile') . EOL); - $a->error = 404; - return; + throw new \Friendica\Network\HTTPException\NotFoundException(L10n::t('No profile')); } $profile = 0; diff --git a/mod/help.php b/mod/help.php deleted file mode 100644 index cdfedac6f..000000000 --- a/mod/help.php +++ /dev/null @@ -1,119 +0,0 @@ -argc > 1) { - $path = ''; - // looping through the argv keys bigger than 0 to build - // a path relative to /help - for ($x = 1; $x < $a->argc; $x ++) { - if (strlen($path)) { - $path .= '/'; - } - - $path .= $a->getArgumentValue($x); - } - $title = basename($path); - $filename = $path; - $text = load_doc_file('doc/' . $path . '.md'); - $a->page['title'] = L10n::t('Help:') . ' ' . str_replace('-', ' ', Strings::escapeTags($title)); - } - - $home = load_doc_file('doc/Home.md'); - if (!$text) { - $text = $home; - $filename = "Home"; - $a->page['title'] = L10n::t('Help'); - } else { - $a->page['aside'] = Markdown::convert($home, false); - } - - if (!strlen($text)) { - header($_SERVER["SERVER_PROTOCOL"] . ' 404 ' . L10n::t('Not Found')); - $tpl = Renderer::getMarkupTemplate("404.tpl"); - return Renderer::replaceMacros($tpl, [ - '$message' => L10n::t('Page not found.') - ]); - } - - $html = Markdown::convert($text, false); - - if ($filename !== "Home") { - // create TOC but not for home - $lines = explode("\n", $html); - $toc = "

TOC

    "; - $lastlevel = 1; - $idnum = [0, 0, 0, 0, 0, 0, 0]; - foreach ($lines as &$line) { - if (substr($line, 0, 2) == " $lastlevel) { - $toc .= "
    • "; - } - - $idnum[$level] ++; - $id = implode("_", array_slice($idnum, 1, $level)); - $href = System::baseUrl() . "/help/{$filename}#{$id}"; - $toc .= "
    • " . strip_tags($line) . "
    • "; - $line = "" . $line; - $lastlevel = $level; - } - } - } - - for ($k = 0; $k < $lastlevel; $k++) { - $toc .= "
    "; - } - - $html = implode("\n", $lines); - - $a->page['aside'] = '
    ' . $toc . '
    ' . $a->page['aside'] . '
    '; - } - - return $html; -} diff --git a/mod/home.php b/mod/home.php deleted file mode 100644 index 9f1810120..000000000 --- a/mod/home.php +++ /dev/null @@ -1,64 +0,0 @@ -user['nickname'])) { - $a->internalRedirect('network'); - } - - if (strlen(Config::get('system','singleuser'))) { - $a->internalRedirect('profile/' . Config::get('system','singleuser')); - } - -}} - -if(! function_exists('home_content')) { -function home_content(App $a) { - - if (!empty($_SESSION['theme'])) { - unset($_SESSION['theme']); - } - if (!empty($_SESSION['mobile-theme'])) { - unset($_SESSION['mobile-theme']); - } - - $customhome = false; - $defaultheader = '

    ' . (Config::get('config', 'sitename') ? L10n::t('Welcome to %s', Config::get('config', 'sitename')) : '') . '

    '; - - $homefilepath = $a->getBasePath() . "/home.html"; - $cssfilepath = $a->getBasePath() . "/home.css"; - if (file_exists($homefilepath)) { - $customhome = $homefilepath; - if (file_exists($cssfilepath)) { - $a->page['htmlhead'] .= ''; - } - } - - $login = Login::form($a->query_string, intval(Config::get('config', 'register_policy')) === \Friendica\Module\Register::CLOSED ? 0 : 1); - - $content = ''; - Hook::callAll("home_content",$content); - - - $tpl = Renderer::getMarkupTemplate('home.tpl'); - return Renderer::replaceMacros($tpl, [ - '$defaultheader' => $defaultheader, - '$customhome' => $customhome, - '$login' => $login, - '$content' => $content - ]); -}} diff --git a/mod/hostxrd.php b/mod/hostxrd.php deleted file mode 100644 index 93a9d833c..000000000 --- a/mod/hostxrd.php +++ /dev/null @@ -1,34 +0,0 @@ - $a->getHostName(), - '$zroot' => System::baseUrl(), - '$domain' => System::baseUrl(), - '$bigkey' => Salmon::salmonKey(Config::get('system', 'site_pubkey'))] - ); - - exit(); -} diff --git a/mod/hovercard.php b/mod/hovercard.php index 603c617ca..ca3991963 100644 --- a/mod/hovercard.php +++ b/mod/hovercard.php @@ -31,7 +31,7 @@ function hovercard_content() // Get out if the system doesn't have public access allowed if (intval(Config::get('system', 'block_public'))) { - System::httpExit(401); + throw new \Friendica\Network\HTTPException\ForbiddenException(); } // Return the raw content of the template. We use this to make templates usable for js functions. diff --git a/mod/invite.php b/mod/invite.php deleted file mode 100644 index e8901d071..000000000 --- a/mod/invite.php +++ /dev/null @@ -1,158 +0,0 @@ - $max_invites) { - notice(L10n::t('Total invitation limit exceeded.') . EOL); - return; - } - - - $recipients = !empty($_POST['recipients']) ? explode("\n", $_POST['recipients']) : []; - $message = !empty($_POST['message']) ? Strings::escapeTags(trim($_POST['message'])) : ''; - - $total = 0; - $invitation_only = false; - $invites_remaining = null; - - if (Config::get('system', 'invitation_only')) { - $invitation_only = true; - $invites_remaining = PConfig::get(local_user(), 'system', 'invites_remaining'); - if ((! $invites_remaining) && (! is_site_admin())) { - return; - } - } - - foreach ($recipients as $recipient) { - $recipient = trim($recipient); - - if (!filter_var($recipient, FILTER_VALIDATE_EMAIL)) { - notice(L10n::t('%s : Not a valid email address.', $recipient) . EOL); - continue; - } - - if ($invitation_only && ($invites_remaining || is_site_admin())) { - $code = Friendica\Model\Register::createForInvitation(); - $nmessage = str_replace('$invite_code', $code, $message); - - if (! is_site_admin()) { - $invites_remaining --; - if ($invites_remaining >= 0) { - PConfig::set(local_user(), 'system', 'invites_remaining', $invites_remaining); - } else { - return; - } - } - } else { - $nmessage = $message; - } - - $additional_headers = 'From: ' . $a->user['email'] . "\n" - . 'Sender: ' . $a->getSenderEmailAddress() . "\n" - . 'Content-type: text/plain; charset=UTF-8' . "\n" - . 'Content-transfer-encoding: 8bit'; - - $res = mail( - $recipient, - Email::encodeHeader(L10n::t('Please join us on Friendica'), 'UTF-8'), - $nmessage, - $additional_headers); - - if ($res) { - $total ++; - $current_invites ++; - PConfig::set(local_user(), 'system', 'sent_invites', $current_invites); - if ($current_invites > $max_invites) { - notice(L10n::t('Invitation limit exceeded. Please contact your site administrator.') . EOL); - return; - } - } else { - notice(L10n::t('%s : Message delivery failed.', $recipient) . EOL); - } - - } - notice(L10n::tt("%d message sent.", "%d messages sent.", $total) . EOL); - return; -} - -function invite_content(App $a) { - - if (! local_user()) { - notice(L10n::t('Permission denied.') . EOL); - return; - } - - $tpl = Renderer::getMarkupTemplate('invite.tpl'); - $invonly = false; - - if (Config::get('system', 'invitation_only')) { - $invonly = true; - $x = PConfig::get(local_user(), 'system', 'invites_remaining'); - if ((! $x) && (! is_site_admin())) { - notice(L10n::t('You have no more invitations available') . EOL); - return ''; - } - } - - $dirloc = Config::get('system', 'directory'); - if (strlen($dirloc)) { - if (intval(Config::get('config', 'register_policy')) === Register::CLOSED) { - $linktxt = L10n::t('Visit %s for a list of public sites that you can join. Friendica members on other sites can all connect with each other, as well as with members of many other social networks.', $dirloc . '/servers'); - } else { - $linktxt = L10n::t('To accept this invitation, please visit and register at %s or any other public Friendica website.', System::baseUrl()) - . "\r\n" . "\r\n" . L10n::t('Friendica sites all inter-connect to create a huge privacy-enhanced social web that is owned and controlled by its members. They can also connect with many traditional social networks. See %s for a list of alternate Friendica sites you can join.', $dirloc . '/servers'); - } - } else { // there is no global directory URL defined - if (intval(Config::get('config', 'register_policy')) === Register::CLOSED) { - $o = L10n::t('Our apologies. This system is not currently configured to connect with other public sites or invite members.'); - return $o; - } else { - $linktxt = L10n::t('To accept this invitation, please visit and register at %s.', System::baseUrl() - . "\r\n" . "\r\n" . L10n::t('Friendica sites all inter-connect to create a huge privacy-enhanced social web that is owned and controlled by its members. They can also connect with many traditional social networks.')); - } - } - - $o = Renderer::replaceMacros($tpl, [ - '$form_security_token' => BaseModule::getFormSecurityToken("send_invite"), - '$title' => L10n::t('Send invitations'), - '$recipients' => ['recipients', L10n::t('Enter email addresses, one per line:')], - '$message' => ['message', L10n::t('Your message:'),L10n::t('You are cordially invited to join me and other close friends on Friendica - and help us to create a better social web.') . "\r\n" . "\r\n" - . $linktxt - . "\r\n" . "\r\n" . (($invonly) ? L10n::t('You will need to supply this invitation code: $invite_code') . "\r\n" . "\r\n" : '') .L10n::t('Once you have registered, please connect with me via my profile page at:') - . "\r\n" . "\r\n" . System::baseUrl() . '/profile/' . $a->user['nickname'] - . "\r\n" . "\r\n" . L10n::t('For more information about the Friendica project and why we feel it is important, please visit http://friendi.ca') . "\r\n" . "\r\n"], - '$submit' => L10n::t('Submit') - ]); - - return $o; -} diff --git a/mod/item.php b/mod/item.php index b126c4825..20dc9dfda 100644 --- a/mod/item.php +++ b/mod/item.php @@ -27,12 +27,12 @@ use Friendica\Core\Protocol; use Friendica\Core\System; use Friendica\Core\Worker; use Friendica\Database\DBA; +use Friendica\Model\Attach; use Friendica\Model\Contact; use Friendica\Model\Conversation; use Friendica\Model\FileTag; use Friendica\Model\Item; use Friendica\Model\Photo; -use Friendica\Model\Attach; use Friendica\Model\Term; use Friendica\Protocol\Diaspora; use Friendica\Protocol\Email; @@ -40,6 +40,7 @@ use Friendica\Util\DateTimeFormat; use Friendica\Util\Emailer; use Friendica\Util\Security; use Friendica\Util\Strings; +use Friendica\Worker\Delivery; require_once 'include/items.php'; @@ -327,10 +328,9 @@ function item_post(App $a) { } } - if (!empty($categories)) - { + if (!empty($categories)) { // get the "fileas" tags for this post - $filedas = FileTag::fileToList($categories, 'file'); + $filedas = FileTag::fileToArray($categories); } // save old and new categories, so we can determine what needs to be deleted from pconfig @@ -338,10 +338,9 @@ function item_post(App $a) { $categories = FileTag::listToFile(trim(defaults($_REQUEST, 'category', '')), 'category'); $categories_new = $categories; - if (!empty($filedas)) - { + if (!empty($filedas) && is_array($filedas)) { // append the fileas stuff to the new categories list - $categories .= FileTag::listToFile($filedas, 'file'); + $categories .= FileTag::arrayToFile($filedas); } // get contact info for poster @@ -605,8 +604,6 @@ function item_post(App $a) { $origin = $_REQUEST['origin']; } - $notify_type = ($toplevel_item_id ? 'comment-new' : 'wall-new'); - $uri = ($message_id ? $message_id : Item::newURI($api_source ? $profile_uid : $uid, $guid)); // Fallback so that we alway have a parent uri @@ -871,7 +868,7 @@ function item_post(App $a) { // When we are doing some forum posting via ! we have to start the notifier manually. // These kind of posts don't initiate the notifier call in the item class. if ($only_to_forum) { - Worker::add(PRIORITY_HIGH, "Notifier", $notify_type, $post_id); + Worker::add(PRIORITY_HIGH, "Notifier", Delivery::POST, $post_id); } Logger::log('post_complete'); diff --git a/mod/like.php b/mod/like.php deleted file mode 100644 index 7fc7b2b42..000000000 --- a/mod/like.php +++ /dev/null @@ -1,48 +0,0 @@ -argc > 1) ? Strings::escapeTags(trim($a->argv[1])) : 0); - - $r = Item::performLike($item_id, $verb); - if (!$r) { - return; - } - - // See if we've been passed a return path to redirect to - $return_path = defaults($_REQUEST, 'return', ''); - - like_content_return($a, $return_path); - exit(); -} - - -// Decide how to return. If we were called with a 'return' argument, -// then redirect back to the calling page. If not, just quietly end - -function like_content_return(App $a, $return_path) { - if ($return_path) { - $rand = '_=' . time(); - if (strpos($return_path, '?')) { - $rand = "&$rand"; - } else { - $rand = "?$rand"; - } - - $a->internalRedirect($return_path . $rand); - } -} diff --git a/mod/localtime.php b/mod/localtime.php deleted file mode 100644 index f68c3fba5..000000000 --- a/mod/localtime.php +++ /dev/null @@ -1,59 +0,0 @@ -data['mod-localtime'] = DateTimeFormat::convert($t, $_POST['timezone'], 'UTC', $bd_format); - } -} - -function localtime_content(App $a) -{ - $t = $_REQUEST['time']; - if (! $t) { - $t = 'now'; - } - - $o = '

    ' . L10n::t('Time Conversion') . '

    '; - - $o .= '

    ' . L10n::t('Friendica provides this service for sharing events with other networks and friends in unknown timezones.') . '

    '; - - - - $o .= '

    ' . L10n::t('UTC time: %s', $t) . '

    '; - - if ($_REQUEST['timezone']) { - $o .= '

    ' . L10n::t('Current timezone: %s', $_REQUEST['timezone']) . '

    '; - } - - if (!empty($a->data['mod-localtime'])) { - $o .= '

    ' . L10n::t('Converted localtime: %s', $a->data['mod-localtime']) . '

    '; - } - - - $o .= '
    '; - - $o .= '

    ' . L10n::t('Please select your timezone:') . '

    '; - - $o .= Temporal::getTimezoneSelect(($_REQUEST['timezone']) ? $_REQUEST['timezone'] : 'America/Los_Angeles'); - - $o .= '
    '; - - return $o; -} diff --git a/mod/lostpass.php b/mod/lostpass.php index 548cea185..01e84268b 100644 --- a/mod/lostpass.php +++ b/mod/lostpass.php @@ -139,7 +139,6 @@ function lostpass_generate_password($user) '$lbl5' => '' . L10n::t('click here to login') . '.', '$lbl6' => L10n::t('Your password may be changed from the Settings page after successful login.'), '$newpass' => $new_password, - '$baseurl' => System::baseUrl() ]); info("Your password has been reset." . EOL); diff --git a/mod/maintenance.php b/mod/maintenance.php deleted file mode 100644 index 8e0197b86..000000000 --- a/mod/maintenance.php +++ /dev/null @@ -1,29 +0,0 @@ - L10n::t('System down for maintenance'), - '$reason' => $reason - ]); -} diff --git a/mod/manage.php b/mod/manage.php index 52ddfdf03..58590264a 100644 --- a/mod/manage.php +++ b/mod/manage.php @@ -2,11 +2,12 @@ /** * @file mod/manage.php */ + use Friendica\App; -use Friendica\Core\Authentication; use Friendica\Core\Hook; use Friendica\Core\L10n; use Friendica\Core\Renderer; +use Friendica\Core\Session; use Friendica\Database\DBA; function manage_post(App $a) { @@ -108,7 +109,7 @@ function manage_post(App $a) { unset($_SESSION['sysmsg_info']); } - Authentication::setAuthenticatedSessionForUser($r[0], true, true); + Session::setAuthenticatedForUser($a, $r[0], true, true); if ($limited_id) { $_SESSION['submanage'] = $original_id; diff --git a/mod/manifest.php b/mod/manifest.php deleted file mode 100644 index bdb5298ef..000000000 --- a/mod/manifest.php +++ /dev/null @@ -1,28 +0,0 @@ - System::baseUrl(), - '$touch_icon' => $touch_icon, - '$title' => Config::get('config', 'sitename', 'Friendica'), - ]); - - echo $o; - - exit(); -} diff --git a/mod/modexp.php b/mod/modexp.php deleted file mode 100644 index cae91c464..000000000 --- a/mod/modexp.php +++ /dev/null @@ -1,36 +0,0 @@ -argc != 2) - exit(); - - $nick = $a->argv[1]; - $r = q("SELECT `spubkey` FROM `user` WHERE `nickname` = '%s' LIMIT 1", - DBA::escape($nick) - ); - - if (! DBA::isResult($r)) { - exit(); - } - - $lines = explode("\n",$r[0]['spubkey']); - unset($lines[0]); - unset($lines[count($lines)]); - $x = base64_decode(implode('',$lines)); - - $r = ASN_BASE::parseASNString($x); - - $m = $r[0]->asnData[1]->asnData[0]->asnData[0]->asnData; - $e = $r[0]->asnData[1]->asnData[0]->asnData[1]->asnData; - - header("Content-type: application/magic-public-key"); - echo 'RSA' . '.' . $m . '.' . $e; - - exit(); - -} - diff --git a/mod/network.php b/mod/network.php index 30e31cea3..7753ba2ca 100644 --- a/mod/network.php +++ b/mod/network.php @@ -77,9 +77,7 @@ function network_init(App $a) // convert query string to array. remove friendica args $query_array = []; - $query_string = str_replace($a->cmd . '?', '', $a->query_string); - parse_str($query_string, $query_array); - array_shift($query_array); + parse_str(parse_url($a->query_string, PHP_URL_QUERY), $query_array); // fetch last used network view and redirect if needed if (!$is_a_date_query) { @@ -99,7 +97,7 @@ function network_init(App $a) if ($remember_tab) { // redirect if current selected tab is '/network' and - // last selected tab is _not_ '/network?f=&order=comment'. + // last selected tab is _not_ '/network?order=comment'. // and this isn't a date query $tab_baseurls = [ @@ -111,12 +109,12 @@ function network_init(App $a) '', //bookmarked ]; $tab_args = [ - 'f=&order=comment', //all - 'f=&order=post', //postord - 'f=&conv=1', //conv + 'order=comment', //all + 'order=post', //postord + 'conv=1', //conv '', //new - 'f=&star=1', //starred - 'f=&bmark=1', //bookmarked + 'star=1', //starred + 'bmark=1', //bookmarked ]; $k = array_search('active', $last_sel_tabs); @@ -140,7 +138,7 @@ function network_init(App $a) if ($remember_tab) { $net_args = array_merge($query_array, $net_args); - $net_queries = build_querystring($net_args); + $net_queries = http_build_query($net_args); $redir_url = ($net_queries ? $net_baseurl . '?' . $net_queries : $net_baseurl); @@ -154,7 +152,7 @@ function network_init(App $a) $a->page['aside'] .= Group::sidebarWidget('network/0', 'network', 'standard', $group_id); $a->page['aside'] .= ForumManager::widget(local_user(), $cid); - $a->page['aside'] .= posted_date_widget('network', local_user(), false); + $a->page['aside'] .= Widget::postedByYear('network', local_user(), false); $a->page['aside'] .= Widget::networks('network', defaults($_GET, 'nets', '') ); $a->page['aside'] .= saved_searches($search); $a->page['aside'] .= Widget::fileAs('network', defaults($_GET, 'file', '') ); @@ -202,12 +200,12 @@ function saved_searches($search) * * urls -> returns * '/network' => $no_active = 'active' - * '/network?f=&order=comment' => $comment_active = 'active' - * '/network?f=&order=post' => $postord_active = 'active' - * '/network?f=&conv=1', => $conv_active = 'active' + * '/network?order=comment' => $comment_active = 'active' + * '/network?order=post' => $postord_active = 'active' + * '/network?conv=1', => $conv_active = 'active' * '/network/new', => $new_active = 'active' - * '/network?f=&star=1', => $starred_active = 'active' - * '/network?f=&bmark=1', => $bookmarked_active = 'active' + * '/network?star=1', => $starred_active = 'active' + * '/network?bmark=1', => $bookmarked_active = 'active' * * @param App $a * @return array ($no_active, $comment_active, $postord_active, $conv_active, $new_active, $starred_active, $bookmarked_active); @@ -973,7 +971,7 @@ function network_tabs(App $a) $tabs = [ [ 'label' => L10n::t('Commented Order'), - 'url' => str_replace('/new', '', $cmd) . '?f=&order=comment' . (!empty($_GET['cid']) ? '&cid=' . $_GET['cid'] : ''), + 'url' => str_replace('/new', '', $cmd) . '?order=comment' . (!empty($_GET['cid']) ? '&cid=' . $_GET['cid'] : ''), 'sel' => $all_active, 'title' => L10n::t('Sort by Comment Date'), 'id' => 'commented-order-tab', @@ -981,7 +979,7 @@ function network_tabs(App $a) ], [ 'label' => L10n::t('Posted Order'), - 'url' => str_replace('/new', '', $cmd) . '?f=&order=post' . (!empty($_GET['cid']) ? '&cid=' . $_GET['cid'] : ''), + 'url' => str_replace('/new', '', $cmd) . '?order=post' . (!empty($_GET['cid']) ? '&cid=' . $_GET['cid'] : ''), 'sel' => $postord_active, 'title' => L10n::t('Sort by Post Date'), 'id' => 'posted-order-tab', @@ -991,7 +989,7 @@ function network_tabs(App $a) $tabs[] = [ 'label' => L10n::t('Personal'), - 'url' => str_replace('/new', '', $cmd) . (!empty($_GET['cid']) ? '/?f=&cid=' . $_GET['cid'] : '/?f=') . '&conv=1', + 'url' => str_replace('/new', '', $cmd) . (!empty($_GET['cid']) ? '/?cid=' . $_GET['cid'] : '/?f=') . '&conv=1', 'sel' => $conv_active, 'title' => L10n::t('Posts that mention or involve you'), 'id' => 'personal-tab', @@ -1001,7 +999,7 @@ function network_tabs(App $a) if (Feature::isEnabled(local_user(), 'new_tab')) { $tabs[] = [ 'label' => L10n::t('New'), - 'url' => 'network/new' . (!empty($_GET['cid']) ? '/?f=&cid=' . $_GET['cid'] : ''), + 'url' => 'network/new' . (!empty($_GET['cid']) ? '/?cid=' . $_GET['cid'] : ''), 'sel' => $new_active, 'title' => L10n::t('Activity Stream - by date'), 'id' => 'activitiy-by-date-tab', @@ -1012,7 +1010,7 @@ function network_tabs(App $a) if (Feature::isEnabled(local_user(), 'link_tab')) { $tabs[] = [ 'label' => L10n::t('Shared Links'), - 'url' => str_replace('/new', '', $cmd) . (!empty($_GET['cid']) ? '/?f=&cid=' . $_GET['cid'] : '/?f=') . '&bmark=1', + 'url' => str_replace('/new', '', $cmd) . (!empty($_GET['cid']) ? '/?cid=' . $_GET['cid'] : '/?f=') . '&bmark=1', 'sel' => $bookmarked_active, 'title' => L10n::t('Interesting Links'), 'id' => 'shared-links-tab', @@ -1022,7 +1020,7 @@ function network_tabs(App $a) $tabs[] = [ 'label' => L10n::t('Starred'), - 'url' => str_replace('/new', '', $cmd) . (!empty($_GET['cid']) ? '/?f=&cid=' . $_GET['cid'] : '/?f=') . '&star=1', + 'url' => str_replace('/new', '', $cmd) . (!empty($_GET['cid']) ? '/?cid=' . $_GET['cid'] : '/?f=') . '&star=1', 'sel' => $starred_active, 'title' => L10n::t('Favourite Posts'), 'id' => 'starred-posts-tab', diff --git a/mod/newmember.php b/mod/newmember.php deleted file mode 100644 index b1eda7a2d..000000000 --- a/mod/newmember.php +++ /dev/null @@ -1,61 +0,0 @@ -'; - $o .= '

    ' . L10n::t('Welcome to Friendica') . '

    '; - $o .= '

    ' . L10n::t('New Member Checklist') . '

    '; - $o .= '
    '; - $o .= L10n::t('We would like to offer some tips and links to help make your experience enjoyable. Click any item to visit the relevant page. A link to this page will be visible from your home page for two weeks after your initial registration and then will quietly disappear.'); - $o .= '

    ' . L10n::t('Getting Started') . '

    '; - $o .= '
      '; - $o .= '
    • ' . '' . L10n::t('Friendica Walk-Through') . '
      ' . L10n::t('On your Quick Start page - find a brief introduction to your profile and network tabs, make some new connections, and find some groups to join.') . '
    • ' . EOL; - $o .= '
    '; - $o .= '

    ' . L10n::t('Settings') . '

    '; - $o .= '
      '; - $o .= '
    • ' . '' . L10n::t('Go to Your Settings') . '
      ' . L10n::t('On your Settings page - change your initial password. Also make a note of your Identity Address. This looks just like an email address - and will be useful in making friends on the free social web.') . '
    • ' . EOL; - $o .= '
    • ' . L10n::t('Review the other settings, particularly the privacy settings. An unpublished directory listing is like having an unlisted phone number. In general, you should probably publish your listing - unless all of your friends and potential friends know exactly how to find you.') . '
    • ' . EOL; - $o .= '
    '; - $o .= '

    ' . L10n::t('Profile') . '

    '; - $o .= '
      '; - $o .= '
    • ' . '' . L10n::t('Upload Profile Photo') . '
      ' . L10n::t('Upload a profile photo if you have not done so already. Studies have shown that people with real photos of themselves are ten times more likely to make friends than people who do not.') . '
    • ' . EOL; - $o .= '
    • ' . '' . L10n::t('Edit Your Profile') . '
      ' . L10n::t('Edit your default profile to your liking. Review the settings for hiding your list of friends and hiding the profile from unknown visitors.') . '
    • ' . EOL; - $o .= '
    • ' . '' . L10n::t('Profile Keywords') . '
      ' . L10n::t('Set some public keywords for your default profile which describe your interests. We may be able to find other people with similar interests and suggest friendships.') . '
    • ' . EOL; - $o .= '
    '; - $o .= '

    ' . L10n::t('Connecting') . '

    '; - $o .= '
      '; - - $mail_disabled = ((function_exists('imap_open') && (!Config::get('system', 'imap_disabled'))) ? 0 : 1); - - if (!$mail_disabled) { - $o .= '
    • ' . '' . L10n::t('Importing Emails') . '
      ' . L10n::t('Enter your email access information on your Connector Settings page if you wish to import and interact with friends or mailing lists from your email INBOX') . '
    • ' . EOL; - } - - $o .= '
    • ' . '' . L10n::t('Go to Your Contacts Page') . '
      ' . L10n::t('Your Contacts page is your gateway to managing friendships and connecting with friends on other networks. Typically you enter their address or site URL in the Add New Contact dialog.') . '
    • ' . EOL; - $o .= '
    • ' . '' . L10n::t("Go to Your Site's Directory") . '
      ' . L10n::t('The Directory page lets you find other people in this network or other federated sites. Look for a Connect or Follow link on their profile page. Provide your own Identity Address if requested.') . '
    • ' . EOL; - $o .= '
    • ' . '' . L10n::t('Finding New People') . '
      ' . L10n::t("On the side panel of the Contacts page are several tools to find new friends. We can match people by interest, look up people by name or interest, and provide suggestions based on network relationships. On a brand new site, friend suggestions will usually begin to be populated within 24 hours.") . '
    • ' . EOL; - $o .= '
    '; - $o .= '

    ' . L10n::t('Groups') . '

    '; - $o .= '
      '; - $o .= '
    • ' . '' . L10n::t('Group Your Contacts') . '
      ' . L10n::t('Once you have made some friends, organize them into private conversation groups from the sidebar of your Contacts page and then you can interact with each group privately on your Network page.') . '
    • ' . EOL; - - if (Config::get('system', 'newuser_private')) { - $o .= '
    • ' . '' . L10n::t("Why Aren't My Posts Public?") . '
      ' . L10n::t("Friendica respects your privacy. By default, your posts will only show up to people you've added as friends. For more information, see the help section from the link above.") . '
    • ' . EOL; - } - - $o .= '
    '; - $o .= '

    ' . L10n::t('Getting Help') . '

    '; - $o .= ''; - $o .= '
    '; - $o .= ''; - - return $o; -} diff --git a/mod/nodeinfo.php b/mod/nodeinfo.php deleted file mode 100644 index da9dbb87c..000000000 --- a/mod/nodeinfo.php +++ /dev/null @@ -1,221 +0,0 @@ - [['rel' => 'http://nodeinfo.diaspora.software/ns/schema/1.0', - 'href' => System::baseUrl().'/nodeinfo/1.0']]]; - - header('Content-type: application/json; charset=utf-8'); - echo json_encode($nodeinfo, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); - exit; -} - -function nodeinfo_init(App $a) { - if (!Config::get('system', 'nodeinfo')) { - System::httpExit(404); - } - - if (($a->argc != 2) || ($a->argv[1] != '1.0')) { - System::httpExit(404); - } - - $smtp = (function_exists('imap_open') && !Config::get('system', 'imap_disabled') && !Config::get('system', 'dfrn_only')); - - $nodeinfo = []; - $nodeinfo['version'] = '1.0'; - $nodeinfo['software'] = ['name' => 'friendica', 'version' => FRIENDICA_VERSION.'-'.DB_UPDATE_VERSION]; - - $nodeinfo['protocols'] = []; - $nodeinfo['protocols']['inbound'] = []; - $nodeinfo['protocols']['outbound'] = []; - - if (Config::get('system', 'diaspora_enabled')) { - $nodeinfo['protocols']['inbound'][] = 'diaspora'; - $nodeinfo['protocols']['outbound'][] = 'diaspora'; - } - - $nodeinfo['protocols']['inbound'][] = 'friendica'; - $nodeinfo['protocols']['outbound'][] = 'friendica'; - - if (!Config::get('system', 'ostatus_disabled')) { - $nodeinfo['protocols']['inbound'][] = 'gnusocial'; - $nodeinfo['protocols']['outbound'][] = 'gnusocial'; - } - - $nodeinfo['services'] = []; - $nodeinfo['services']['inbound'] = []; - $nodeinfo['services']['outbound'] = []; - - $nodeinfo['usage'] = []; - - $nodeinfo['openRegistrations'] = intval(Config::get('config', 'register_policy')) !== \Friendica\Module\Register::CLOSED; - - $nodeinfo['metadata'] = ['nodeName' => Config::get('config', 'sitename')]; - - if (Config::get('system', 'nodeinfo')) { - - $nodeinfo['usage']['users'] = ['total' => (int)Config::get('nodeinfo', 'total_users'), - 'activeHalfyear' => (int)Config::get('nodeinfo', 'active_users_halfyear'), - 'activeMonth' => (int)Config::get('nodeinfo', 'active_users_monthly')]; - $nodeinfo['usage']['localPosts'] = (int)Config::get('nodeinfo', 'local_posts'); - $nodeinfo['usage']['localComments'] = (int)Config::get('nodeinfo', 'local_comments'); - - if (Addon::isEnabled('blogger')) { - $nodeinfo['services']['outbound'][] = 'blogger'; - } - if (Addon::isEnabled('dwpost')) { - $nodeinfo['services']['outbound'][] = 'dreamwidth'; - } - if (Addon::isEnabled('statusnet')) { - $nodeinfo['services']['inbound'][] = 'gnusocial'; - $nodeinfo['services']['outbound'][] = 'gnusocial'; - } - if (Addon::isEnabled('ijpost')) { - $nodeinfo['services']['outbound'][] = 'insanejournal'; - } - if (Addon::isEnabled('libertree')) { - $nodeinfo['services']['outbound'][] = 'libertree'; - } - if (Addon::isEnabled('buffer')) { - $nodeinfo['services']['outbound'][] = 'linkedin'; - } - if (Addon::isEnabled('ljpost')) { - $nodeinfo['services']['outbound'][] = 'livejournal'; - } - if (Addon::isEnabled('buffer')) { - $nodeinfo['services']['outbound'][] = 'pinterest'; - } - if (Addon::isEnabled('posterous')) { - $nodeinfo['services']['outbound'][] = 'posterous'; - } - if (Addon::isEnabled('pumpio')) { - $nodeinfo['services']['inbound'][] = 'pumpio'; - $nodeinfo['services']['outbound'][] = 'pumpio'; - } - - if ($smtp) { - $nodeinfo['services']['outbound'][] = 'smtp'; - } - if (Addon::isEnabled('tumblr')) { - $nodeinfo['services']['outbound'][] = 'tumblr'; - } - if (Addon::isEnabled('twitter') || Addon::isEnabled('buffer')) { - $nodeinfo['services']['outbound'][] = 'twitter'; - } - if (Addon::isEnabled('wppost')) { - $nodeinfo['services']['outbound'][] = 'wordpress'; - } - $nodeinfo['metadata']['protocols'] = $nodeinfo['protocols']; - $nodeinfo['metadata']['protocols']['outbound'][] = 'atom1.0'; - $nodeinfo['metadata']['protocols']['inbound'][] = 'atom1.0'; - $nodeinfo['metadata']['protocols']['inbound'][] = 'rss2.0'; - - $nodeinfo['metadata']['services'] = $nodeinfo['services']; - - if (Addon::isEnabled('twitter')) { - $nodeinfo['metadata']['services']['inbound'][] = 'twitter'; - } - - $nodeinfo['metadata']['explicitContent'] = Config::get('system', 'explicit_content', false) == true; - } - - header('Content-type: application/json; charset=utf-8'); - echo json_encode($nodeinfo, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); - exit; -} - - - -function nodeinfo_cron() { - - $a = \get_app(); - - // If the addon 'statistics_json' is enabled then disable it and activate nodeinfo. - if (Addon::isEnabled('statistics_json')) { - Config::set('system', 'nodeinfo', true); - - $addon = 'statistics_json'; - $addons = Config::get('system', 'addon'); - - if ($addons) { - $addons_arr = explode(',',str_replace(' ', '',$addons)); - - $idx = array_search($addon, $addons_arr); - if ($idx !== false) { - unset($addons_arr[$idx]); - Addon::uninstall($addon); - Config::set('system', 'addon', implode(', ',$addons_arr)); - } - } - } - - if (!Config::get('system', 'nodeinfo')) { - return; - } - - Logger::log('cron_start'); - - $users = q("SELECT `user`.`uid`, `user`.`login_date`, `contact`.`last-item` - FROM `user` - INNER JOIN `profile` ON `profile`.`uid` = `user`.`uid` AND `profile`.`is-default` - INNER JOIN `contact` ON `contact`.`uid` = `user`.`uid` AND `contact`.`self` - WHERE (`profile`.`publish` OR `profile`.`net-publish`) AND `user`.`verified` - AND NOT `user`.`blocked` AND NOT `user`.`account_removed` - AND NOT `user`.`account_expired`"); - if (is_array($users)) { - $total_users = count($users); - $active_users_halfyear = 0; - $active_users_monthly = 0; - - $halfyear = time() - (180 * 24 * 60 * 60); - $month = time() - (30 * 24 * 60 * 60); - - foreach ($users AS $user) { - if ((strtotime($user['login_date']) > $halfyear) || - (strtotime($user['last-item']) > $halfyear)) { - ++$active_users_halfyear; - } - if ((strtotime($user['login_date']) > $month) || - (strtotime($user['last-item']) > $month)) { - ++$active_users_monthly; - } - } - Config::set('nodeinfo', 'total_users', $total_users); - Config::set('nodeinfo', 'active_users_halfyear', $active_users_halfyear); - Config::set('nodeinfo', 'active_users_monthly', $active_users_monthly); - - Logger::log('total_users: ' . $total_users . '/' . $active_users_halfyear. '/' . $active_users_monthly, Logger::DEBUG); - } - - $local_posts = DBA::count('thread', ["`wall` AND NOT `deleted` AND `uid` != 0"]); - Config::set('nodeinfo', 'local_posts', $local_posts); - Logger::log('local_posts: ' . $local_posts, Logger::DEBUG); - - $local_comments = DBA::count('item', ["`origin` AND `id` != `parent` AND NOT `deleted` AND `uid` != 0"]); - Config::set('nodeinfo', 'local_comments', $local_comments); - Logger::log('local_comments: ' . $local_comments, Logger::DEBUG); - - // Now trying to register - $url = 'http://the-federation.info/register/'.$a->getHostName(); - Logger::log('registering url: '.$url, Logger::DEBUG); - $ret = Network::fetchUrl($url); - Logger::log('registering answer: '.$ret, Logger::DEBUG); - - Logger::log('cron_end'); -} diff --git a/mod/nogroup.php b/mod/nogroup.php deleted file mode 100644 index 20dbd23a3..000000000 --- a/mod/nogroup.php +++ /dev/null @@ -1,23 +0,0 @@ -internalRedirect('group/none'); -} diff --git a/mod/notes.php b/mod/notes.php index fdb12d6cc..1f67e486d 100644 --- a/mod/notes.php +++ b/mod/notes.php @@ -28,7 +28,7 @@ function notes_content(App $a, $update = false) return; } - $o = Profile::getTabs($a, true); + $o = Profile::getTabs($a, 'notes', true); if (!$update) { $o .= '

    ' . L10n::t('Personal Notes') . '

    '; diff --git a/mod/notice.php b/mod/notice.php deleted file mode 100644 index 1a584000c..000000000 --- a/mod/notice.php +++ /dev/null @@ -1,24 +0,0 @@ - friendica items permanent-url compatibility - */ - -use Friendica\App; -use Friendica\Core\L10n; -use Friendica\Database\DBA; - -function notice_init(App $a) -{ - $id = $a->argv[1]; - $r = q("SELECT `user`.`nickname` FROM `user` LEFT JOIN `item` ON `item`.`uid` = `user`.`uid` WHERE `item`.`id` = %d", intval($id)); - if (DBA::isResult($r)) { - $nick = $r[0]['nickname']; - $a->internalRedirect('display/' . $nick . '/' . $id); - } else { - $a->error = 404; - notice(L10n::t('Item not found.') . EOL); - } - - return; -} diff --git a/mod/notifications.php b/mod/notifications.php index 909b297eb..8bc9a76c3 100644 --- a/mod/notifications.php +++ b/mod/notifications.php @@ -121,6 +121,9 @@ function notifications_content(App $a) } elseif (($a->argc > 1) && ($a->argv[1] == 'home')) { $notif_header = L10n::t('Home Notifications'); $notifs = $nm->homeNotifs($show, $startrec, $perpage); + // fallback - redirect to main page + } else { + $a->internalRedirect('notifications'); } // Set the pager @@ -223,6 +226,14 @@ function notifications_content(App $a) '$as_fan' => (($notif['network'] == Protocol::DIASPORA) ? L10n::t('Sharer') : L10n::t('Subscriber')) ]); + $contact = DBA::selectFirst('contact', ['network', 'protocol'], ['id' => $notif['contact_id']]); + + if (($contact['network'] != Protocol::DFRN) || ($contact['protocol'] == Protocol::ACTIVITYPUB)) { + $action = 'follow_confirm'; + } else { + $action = 'dfrn_confirm'; + } + $header = $notif['name']; if ($notif['addr'] != '') { @@ -270,6 +281,7 @@ function notifications_content(App $a) '$note' => $notif['note'], '$ignore' => L10n::t('Ignore'), '$discard' => $discard, + '$action' => $action, ]); break; } diff --git a/mod/notify.php b/mod/notify.php deleted file mode 100644 index 870358178..000000000 --- a/mod/notify.php +++ /dev/null @@ -1,90 +0,0 @@ -argc > 2 && $a->argv[1] === 'view' && intval($a->argv[2])) { - $note = $nm->getByID($a->argv[2]); - if ($note) { - $nm->setSeen($note); - - // The friendica client has problems with the GUID. this is some workaround - if ($a->isFriendicaApp()) { - require_once("include/items.php"); - $urldata = parse_url($note['link']); - $guid = basename($urldata["path"]); - $itemdata = Item::getIdAndNickByGuid($guid, local_user()); - if ($itemdata["id"] != 0) { - $note['link'] = System::baseUrl().'/display/'.$itemdata["nick"].'/'.$itemdata["id"]; - } - } - - System::externalRedirect($note['link']); - } - - $a->internalRedirect(); - } - - if ($a->argc > 2 && $a->argv[1] === 'mark' && $a->argv[2] === 'all') { - $r = $nm->setAllSeen(); - $j = json_encode(['result' => ($r) ? 'success' : 'fail']); - echo $j; - exit(); - } -} - -function notify_content(App $a) -{ - if (! local_user()) { - return Login::form(); - } - - $notif_content = ''; - - $nm = new NotificationsManager(); - - $notif_tpl = Renderer::getMarkupTemplate('notifications.tpl'); - - $not_tpl = Renderer::getMarkupTemplate('notify.tpl'); - - $r = $nm->getAll(['seen'=>0]); - if (DBA::isResult($r) > 0) { - foreach ($r as $it) { - $notif_content .= Renderer::replaceMacros($not_tpl, [ - '$item_link' => System::baseUrl(true).'/notify/view/'. $it['id'], - '$item_image' => $it['photo'], - '$item_text' => strip_tags(BBCode::convert($it['msg'])), - '$item_when' => Temporal::getRelativeDate($it['date']) - ]); - } - } else { - $notif_content .= L10n::t('No more system notifications.'); - } - - $o = Renderer::replaceMacros($notif_tpl, [ - '$notif_header' => L10n::t('System Notifications'), - '$tabs' => false, // $tabs, - '$notif_content' => $notif_content, - ]); - - return $o; -} diff --git a/mod/openid.php b/mod/openid.php index 7300c686b..def34ff08 100644 --- a/mod/openid.php +++ b/mod/openid.php @@ -4,10 +4,10 @@ */ use Friendica\App; -use Friendica\Core\Authentication; use Friendica\Core\Config; use Friendica\Core\L10n; use Friendica\Core\Logger; +use Friendica\Core\Session; use Friendica\Database\DBA; use Friendica\Util\Strings; @@ -52,7 +52,7 @@ function openid_content(App $a) { unset($_SESSION['openid']); - Authentication::setAuthenticatedSessionForUser($r[0],true,true); + Session::setAuthenticatedForUser($a, $r[0],true,true); // just in case there was no return url set // and we fell through diff --git a/mod/opensearch.php b/mod/opensearch.php deleted file mode 100644 index 2057f0b5d..000000000 --- a/mod/opensearch.php +++ /dev/null @@ -1,21 +0,0 @@ - System::baseUrl(), - '$nodename' => $a->getHostName(), - ]); - - echo $o; - - exit(); -} diff --git a/mod/parse_url.php b/mod/parse_url.php index 3b2522ab1..6b393932e 100644 --- a/mod/parse_url.php +++ b/mod/parse_url.php @@ -9,12 +9,14 @@ * * @see ParseUrl::getSiteinfo() for more information about scraping embeddable content */ + use Friendica\App; use Friendica\Core\Hook; use Friendica\Core\Logger; use Friendica\Core\System; use Friendica\Util\Network; use Friendica\Util\ParseUrl; +use Friendica\Util\Strings; function parse_url_content(App $a) { @@ -25,10 +27,14 @@ function parse_url_content(App $a) $br = "\n"; - if (!empty($_GET['binurl'])) { + if (!empty($_GET['binurl']) && Strings::isHex($_GET['binurl'])) { $url = trim(hex2bin($_GET['binurl'])); - } else { + } elseif (!empty($_GET['url'])) { $url = trim($_GET['url']); + // fallback in case no url is valid + } else { + Logger::info('No url given'); + exit(); } if (!empty($_GET['title'])) { @@ -64,9 +70,8 @@ function parse_url_content(App $a) // Check if the URL is an image, video or audio file. If so format // the URL with the corresponding BBCode media tag - $redirects = 0; // Fetch the header of the URL - $curlResponse = Network::curl($url, false, $redirects, ['novalidate' => true, 'nobody' => true]); + $curlResponse = Network::curl($url, false, ['novalidate' => true, 'nobody' => true]); if ($curlResponse->isSuccess()) { // Convert the header fields into an array diff --git a/mod/photos.php b/mod/photos.php index 137e0adb5..b904abe31 100644 --- a/mod/photos.php +++ b/mod/photos.php @@ -29,8 +29,8 @@ use Friendica\Util\Crypto; use Friendica\Util\DateTimeFormat; use Friendica\Util\Map; use Friendica\Util\Security; -use Friendica\Util\Temporal; use Friendica\Util\Strings; +use Friendica\Util\Temporal; use Friendica\Util\XML; function photos_init(App $a) { @@ -47,23 +47,21 @@ function photos_init(App $a) { if ($a->argc > 1) { $nick = $a->argv[1]; - $user = q("SELECT * FROM `user` WHERE `nickname` = '%s' AND `blocked` = 0 LIMIT 1", - DBA::escape($nick) - ); + $user = DBA::selectFirst('user', [], ['nickname' => $nick, 'blocked' => false]); if (!DBA::isResult($user)) { return; } - $a->data['user'] = $user[0]; - $a->profile_uid = $user[0]['uid']; + $a->data['user'] = $user; + $a->profile_uid = $user['uid']; $is_owner = (local_user() && (local_user() == $a->profile_uid)); $profile = Profile::getByNickname($nick, $a->profile_uid); $account_type = Contact::getAccountType($profile); - $tpl = Renderer::getMarkupTemplate("vcard-widget.tpl"); + $tpl = Renderer::getMarkupTemplate("widget/vcard.tpl"); $vcard_widget = Renderer::replaceMacros($tpl, [ '$name' => $profile['name'], @@ -115,7 +113,6 @@ function photos_init(App $a) { '$title' => L10n::t('Photo Albums'), '$recent' => L10n::t('Recent Photos'), '$albums' => $ret['albums'], - '$baseurl' => System::baseUrl(), '$upload' => [L10n::t('Upload New Photos'), 'photos/' . $a->data['user']['nickname'] . '/upload'], '$can_post' => $can_post ]); @@ -170,12 +167,7 @@ function photos_post(App $a) } if ($contact_id > 0) { - $r = q("SELECT `uid` FROM `contact` WHERE `blocked` = 0 AND `pending` = 0 AND `id` = %d AND `uid` = %d LIMIT 1", - intval($contact_id), - intval($page_owner_uid) - ); - - if (DBA::isResult($r)) { + if (DBA::exists('contact', ['id' => $contact_id, 'uid' => $page_owner_uid, 'blocked' => false, 'pending' => false])) { $can_post = true; $visitor = $contact_id; } @@ -196,6 +188,9 @@ function photos_post(App $a) } if ($a->argc > 3 && $a->argv[2] === 'album') { + if (!Strings::isHex($a->argv[3])) { + $a->internalRedirect('photos/' . $a->data['user']['nickname'] . '/album'); + } $album = hex2bin($a->argv[3]); if ($album === L10n::t('Profile Photos') || $album === 'Contact Photos' || $album === L10n::t('Contact Photos')) { @@ -235,36 +230,12 @@ function photos_post(App $a) } /* - * DELETE photo album and all its photos + * DELETE all photos filed in a given album */ - - if ($_POST['dropalbum'] == L10n::t('Delete Album')) { - // Check if we should do HTML-based delete confirmation - if (!empty($_REQUEST['confirm'])) { - $drop_url = $a->query_string; - - $extra_inputs = [ - ['name' => 'albumname', 'value' => $_POST['albumname']], - ]; - - $a->page['content'] = Renderer::replaceMacros(Renderer::getMarkupTemplate('confirm.tpl'), [ - '$method' => 'post', - '$message' => L10n::t('Do you really want to delete this photo album and all its photos?'), - '$extra_inputs' => $extra_inputs, - '$confirm' => L10n::t('Delete Album'), - '$confirm_url' => $drop_url, - '$confirm_name' => 'dropalbum', // Needed so that confirmation will bring us back into this if statement - '$cancel' => L10n::t('Cancel'), - ]); - - $a->error = 1; // Set $a->error so the other module functions don't execute - return; - } - + if (!empty($_POST['dropalbum'])) { $res = []; // get the list of photos we are about to delete - if ($visitor) { $r = q("SELECT distinct(`resource-id`) as `rid` FROM `photo` WHERE `contact-id` = %d AND `uid` = %d AND `album` = '%s'", intval($visitor), @@ -282,77 +253,57 @@ function photos_post(App $a) foreach ($r as $rr) { $res[] = $rr['rid']; } + + // remove the associated photos + Photo::delete(['resource-id' => $res, 'uid' => $page_owner_uid]); + + // find and delete the corresponding item with all the comments and likes/dislikes + Item::deleteForUser(['resource-id' => $res, 'uid' => $page_owner_uid], $page_owner_uid); + + // Update the photo albums cache + Photo::clearAlbumCache($page_owner_uid); + notice(L10n::t('Album successfully deleted')); } else { - $a->internalRedirect($_SESSION['photo_return']); - return; // NOTREACHED + notice(L10n::t('Album was empty.')); + } + } + + $a->internalRedirect('photos/' . $a->argv[1]); + } + + if ($a->argc > 3 && $a->argv[2] === 'image') { + // Check if the user has responded to a delete confirmation query for a single photo + if (!empty($_POST['canceled'])) { + $a->internalRedirect('photos/' . $a->argv[1] . '/image/' . $a->argv[3]); + } + + if (!empty($_POST['delete'])) { + // same as above but remove single photo + if ($visitor) { + $condition = ['contact-id' => $visitor, 'uid' => $page_owner_uid, 'resource-id' => $a->argv[3]]; + + } else { + $condition = ['uid' => local_user(), 'resource-id' => $a->argv[3]]; } - // remove the associated photos - Photo::delete(['resource-id' => $res, 'uid' => $page_owner_uid]); + $photo = DBA::selectFirst('photo', ['resource-id'], $condition); - // find and delete the corresponding item with all the comments and likes/dislikes - Item::deleteForUser(['resource-id' => $res, 'uid' => $page_owner_uid], $page_owner_uid); + if (DBA::isResult($photo)) { + Photo::delete(['uid' => $page_owner_uid, 'resource-id' => $photo['resource-id']]); - // Update the photo albums cache - Photo::clearAlbumCache($page_owner_uid); + Item::deleteForUser(['resource-id' => $photo['resource-id'], 'uid' => $page_owner_uid], $page_owner_uid); + + // Update the photo albums cache + Photo::clearAlbumCache($page_owner_uid); + notice('Successfully deleted the photo.'); + } else { + notice('Failed to delete the photo.'); + $a->internalRedirect('photos/' . $a->argv[1] . '/image/' . $a->argv[3]); + } + + $a->internalRedirect('photos/' . $a->argv[1]); + return; // NOTREACHED } - - $a->internalRedirect('photos/' . $a->data['user']['nickname']); - return; // NOTREACHED - } - - - // Check if the user has responded to a delete confirmation query for a single photo - if ($a->argc > 2 && !empty($_REQUEST['canceled'])) { - $a->internalRedirect($_SESSION['photo_return']); - } - - if ($a->argc > 2 && defaults($_POST, 'delete', '') === L10n::t('Delete Photo')) { - - // same as above but remove single photo - - // Check if we should do HTML-based delete confirmation - if (!empty($_REQUEST['confirm'])) { - $drop_url = $a->query_string; - - $a->page['content'] = Renderer::replaceMacros(Renderer::getMarkupTemplate('confirm.tpl'), [ - '$method' => 'post', - '$message' => L10n::t('Do you really want to delete this photo?'), - '$extra_inputs' => [], - '$confirm' => L10n::t('Delete Photo'), - '$confirm_url' => $drop_url, - '$confirm_name' => 'delete', // Needed so that confirmation will bring us back into this if statement - '$cancel' => L10n::t('Cancel'), - ]); - - $a->error = 1; // Set $a->error so the other module functions don't execute - return; - } - - if ($visitor) { - $r = q("SELECT `id`, `resource-id` FROM `photo` WHERE `contact-id` = %d AND `uid` = %d AND `resource-id` = '%s' LIMIT 1", - intval($visitor), - intval($page_owner_uid), - DBA::escape($a->argv[2]) - ); - } else { - $r = q("SELECT `id`, `resource-id` FROM `photo` WHERE `uid` = %d AND `resource-id` = '%s' LIMIT 1", - intval(local_user()), - DBA::escape($a->argv[2]) - ); - } - - if (DBA::isResult($r)) { - Photo::delete(['uid' => $page_owner_uid, 'resource-id' => $r[0]['resource-id']]); - - Item::deleteForUser(['resource-id' => $r[0]['resource-id'], 'uid' => $page_owner_uid], $page_owner_uid); - - // Update the photo albums cache - Photo::clearAlbumCache($page_owner_uid); - } - - $a->internalRedirect('photos/' . $a->data['user']['nickname']); - return; // NOTREACHED } if ($a->argc > 2 && (!empty($_POST['desc']) || !empty($_POST['newtag']) || isset($_POST['albname']))) { @@ -367,7 +318,7 @@ function photos_post(App $a) $str_group_deny = !empty($_POST['group_deny']) ? perms2str($_POST['group_deny']) : ''; $str_contact_deny = !empty($_POST['contact_deny']) ? perms2str($_POST['contact_deny']) : ''; - $resource_id = $a->argv[2]; + $resource_id = $a->argv[3]; if (!strlen($albname)) { $albname = DateTimeFormat::localNow('Y'); @@ -470,10 +421,11 @@ function photos_post(App $a) if ($item_id) { $item = Item::selectFirst(['tag', 'inform'], ['id' => $item_id, 'uid' => $page_owner_uid]); - } - if (DBA::isResult($item)) { - $old_tag = $item['tag']; - $old_inform = $item['inform']; + + if (DBA::isResult($item)) { + $old_tag = $item['tag']; + $old_inform = $item['inform']; + } } if (strlen($rawtags)) { @@ -493,6 +445,7 @@ function photos_post(App $a) foreach ($tags as $tag) { if (strpos($tag, '@') === 0) { $profile = ''; + $contact = null; $name = substr($tag,1); if ((strpos($name, '@')) || (strpos($name, 'http://'))) { @@ -527,34 +480,26 @@ function photos_post(App $a) } if ($tagcid) { - $r = q("SELECT * FROM `contact` WHERE `id` = %d AND `uid` = %d LIMIT 1", - intval($tagcid), - intval($page_owner_uid) - ); + $contact = DBA::selectFirst('contact', [], ['id' => $tagcid, 'uid' => $page_owner_uid]); } else { $newname = str_replace('_',' ',$name); //select someone from this user's contacts by name - $r = q("SELECT * FROM `contact` WHERE `name` = '%s' AND `uid` = %d LIMIT 1", - DBA::escape($newname), - intval($page_owner_uid) - ); - - if (!DBA::isResult($r)) { + $contact = DBA::selectFirst('contact', [], ['name' => $newname, 'uid' => $page_owner_uid]); + if (!DBA::isResult($contact)) { //select someone by attag or nick and the name passed in - $r = q("SELECT * FROM `contact` WHERE `attag` = '%s' OR `nick` = '%s' AND `uid` = %d ORDER BY `attag` DESC LIMIT 1", - DBA::escape($name), - DBA::escape($name), - intval($page_owner_uid) + $contact = DBA::selectFirst('contact', [], + ['(`attag` = ? OR `nick` = ?) AND `uid` = ?', $name, $name, $page_owner_uid], + ['order' => ['attag' => true]] ); } } - if (DBA::isResult($r)) { - $newname = $r[0]['name']; - $profile = $r[0]['url']; + if (DBA::isResult($contact)) { + $newname = $contact['name']; + $profile = $contact['url']; - $notify = 'cid:' . $r[0]['id']; + $notify = 'cid:' . $contact['id']; if (strlen($inform)) { $inform .= ','; } @@ -563,8 +508,8 @@ function photos_post(App $a) } if ($profile) { - if (substr($notify, 0, 4) === 'cid:') { - $taginfo[] = [$newname, $profile, $notify, $r[0], '@[url=' . str_replace(',','%2c',$profile) . ']' . $newname . '[/url]']; + if (!empty($contact)) { + $taginfo[] = [$newname, $profile, $notify, $contact, '@[url=' . str_replace(',', '%2c', $profile) . ']' . $newname . '[/url]']; } else { $taginfo[] = [$newname, $profile, $notify, null, $str_tags .= '@[url=' . $profile . ']' . $newname . '[/url]']; } @@ -583,13 +528,13 @@ function photos_post(App $a) } } - $newtag = $old_tag; + $newtag = $old_tag ?? ''; if (strlen($newtag) && strlen($str_tags)) { $newtag .= ','; } $newtag .= $str_tags; - $newinform = $old_inform; + $newinform = $old_inform ?? ''; if (strlen($newinform) && strlen($inform)) { $newinform .= ','; } @@ -794,7 +739,7 @@ function photos_post(App $a) @unlink($src); $foo = 0; Hook::callAll('photo_post_end',$foo); - exit(); + return; } $exif = $image->orient($src); @@ -820,7 +765,7 @@ function photos_post(App $a) if (!$r) { Logger::log('mod/photos.php: photos_post(): image store failed', Logger::DEBUG); notice(L10n::t('Image upload failed.') . EOL); - exit(); + return; } if ($width > 640 || $height > 640) { @@ -896,8 +841,10 @@ function photos_content(App $a) // photos/name/upload/xxxxx (xxxxx is album name) // photos/name/album/xxxxx // photos/name/album/xxxxx/edit + // photos/name/album/xxxxx/drop // photos/name/image/xxxxx // photos/name/image/xxxxx/edit + // photos/name/image/xxxxx/drop if (Config::get('system', 'block_public') && !local_user() && !remote_user()) { notice(L10n::t('Public access denied.') . EOL); @@ -936,7 +883,8 @@ function photos_content(App $a) $contact = null; $remote_contact = false; $contact_id = 0; - $edit = false; + $edit = ''; + $drop = ''; $owner_uid = $a->data['user']['uid']; @@ -954,15 +902,12 @@ function photos_content(App $a) } } } - if ($contact_id) { - $r = q("SELECT `uid` FROM `contact` WHERE `blocked` = 0 AND `pending` = 0 AND `id` = %d AND `uid` = %d LIMIT 1", - intval($contact_id), - intval($owner_uid) - ); - if (DBA::isResult($r)) { + if ($contact_id) { + $contact = DBA::selectFirst('contact', [], ['id' => $contact_id, 'uid' => $owner_uid, 'blocked' => false, 'pending' => false]); + + if (DBA::isResult($contact)) { $can_post = true; - $contact = $r[0]; $remote_contact = true; $visitor = $contact_id; } @@ -983,16 +928,13 @@ function photos_content(App $a) } } } + if ($contact_id) { $groups = Group::getIdsByContactId($contact_id); - $r = q("SELECT * FROM `contact` WHERE `blocked` = 0 AND `pending` = 0 AND `id` = %d AND `uid` = %d LIMIT 1", - intval($contact_id), - intval($owner_uid) - ); - if (DBA::isResult($r)) { - $contact = $r[0]; - $remote_contact = true; - } + + $contact = DBA::selectFirst('contact', [], ['id' => $contact_id, 'uid' => $owner_uid, 'blocked' => false, 'pending' => false]); + + $remote_contact = DBA::isResult($contact); } } @@ -1012,7 +954,7 @@ function photos_content(App $a) // tabs $is_owner = (local_user() && (local_user() == $owner_uid)); - $o .= Profile::getTabs($a, $is_owner, $a->data['user']['nickname']); + $o .= Profile::getTabs($a, 'photos', $is_owner, $a->data['user']['nickname']); // Display upload form if ($datatype === 'upload') { @@ -1021,7 +963,7 @@ function photos_content(App $a) return; } - $selname = $datum ? hex2bin($datum) : ''; + $selname = Strings::isHex($datum) ? hex2bin($datum) : ''; $albumselect = ''; @@ -1088,6 +1030,10 @@ function photos_content(App $a) // Display a single photo album if ($datatype === 'album') { + // if $datum is not a valid hex, redirect to the default page + if (!Strings::isHex($datum)) { + $a->internalRedirect('photos/' . $a->data['user']['nickname']. '/album'); + } $album = hex2bin($datum); $total = 0; @@ -1121,6 +1067,24 @@ function photos_content(App $a) $pager->getItemsPerPage() ); + if ($cmd === 'drop') { + $drop_url = $a->query_string; + + $extra_inputs = [ + ['name' => 'albumname', 'value' => $_POST['albumname']], + ]; + + return Renderer::replaceMacros(Renderer::getMarkupTemplate('confirm.tpl'), [ + '$method' => 'post', + '$message' => L10n::t('Do you really want to delete this photo album and all its photos?'), + '$extra_inputs' => $extra_inputs, + '$confirm' => L10n::t('Delete Album'), + '$confirm_url' => $drop_url, + '$confirm_name' => 'dropalbum', + '$cancel' => L10n::t('Cancel'), + ]); + } + // edit album name if ($cmd === 'edit') { if (($album !== L10n::t('Profile Photos')) && ($album !== 'Contact Photos') && ($album !== L10n::t('Contact Photos'))) { @@ -1142,6 +1106,7 @@ function photos_content(App $a) } else { if (($album !== L10n::t('Profile Photos')) && ($album !== 'Contact Photos') && ($album !== L10n::t('Contact Photos')) && $can_post) { $edit = [L10n::t('Edit Album'), 'photos/' . $a->data['user']['nickname'] . '/album/' . bin2hex($album) . '/edit']; + $drop = [L10n::t('Drop Album'), 'photos/' . $a->data['user']['nickname'] . '/album/' . bin2hex($album) . '/drop']; } } @@ -1187,6 +1152,7 @@ function photos_content(App $a) '$upload' => [L10n::t('Upload New Photos'), 'photos/' . $a->data['user']['nickname'] . '/upload/' . bin2hex($album)], '$order' => $order, '$edit' => $edit, + '$drop' => $drop, '$paginate' => $pager->renderFull($total), ]); @@ -1204,12 +1170,7 @@ function photos_content(App $a) ); if (!DBA::isResult($ph)) { - $ph = q("SELECT `id` FROM `photo` WHERE `uid` = %d AND `resource-id` = '%s' - LIMIT 1", - intval($owner_uid), - DBA::escape($datum) - ); - if (DBA::isResult($ph)) { + if (DBA::exists('photo', ['resource-id' => $datum, 'uid' => $owner_uid])) { notice(L10n::t('Permission denied. Access to this item may be restricted.')); } else { notice(L10n::t('Photo not available') . EOL); @@ -1217,6 +1178,20 @@ function photos_content(App $a) return; } + if ($cmd === 'drop') { + $drop_url = $a->query_string; + + return Renderer::replaceMacros(Renderer::getMarkupTemplate('confirm.tpl'), [ + '$method' => 'post', + '$message' => L10n::t('Do you really want to delete this photo?'), + '$extra_inputs' => [], + '$confirm' => L10n::t('Delete Photo'), + '$confirm_url' => $drop_url, + '$confirm_name' => 'delete', + '$cancel' => L10n::t('Cancel'), + ]); + } + $prevlink = ''; $nextlink = ''; @@ -1225,7 +1200,7 @@ function photos_content(App $a) * The query leads to a really intense used index. * By now we hide it if someone wants to. */ - if (!Config::get('system', 'no_count', false)) { + if ($cmd === 'view' && !Config::get('system', 'no_count', false)) { $order_field = defaults($_GET, 'order', ''); if ($order_field === 'posted') { @@ -1256,12 +1231,26 @@ function photos_content(App $a) break; } } - $edit_suffix = ((($cmd === 'edit') && $can_post) ? '/edit' : ''); + if (!is_null($prv)) { - $prevlink = 'photos/' . $a->data['user']['nickname'] . '/image/' . $prvnxt[$prv]['resource-id'] . $edit_suffix . ($order_field === 'posted' ? '?f=&order=posted' : ''); + $prevlink = 'photos/' . $a->data['user']['nickname'] . '/image/' . $prvnxt[$prv]['resource-id'] . ($order_field === 'posted' ? '?f=&order=posted' : ''); } if (!is_null($nxt)) { - $nextlink = 'photos/' . $a->data['user']['nickname'] . '/image/' . $prvnxt[$nxt]['resource-id'] . $edit_suffix . ($order_field === 'posted' ? '?f=&order=posted' : ''); + $nextlink = 'photos/' . $a->data['user']['nickname'] . '/image/' . $prvnxt[$nxt]['resource-id'] . ($order_field === 'posted' ? '?f=&order=posted' : ''); + } + + $tpl = Renderer::getMarkupTemplate('photo_edit_head.tpl'); + $a->page['htmlhead'] .= Renderer::replaceMacros($tpl,[ + '$prevlink' => $prevlink, + '$nextlink' => $nextlink + ]); + + if ($prevlink) { + $prevlink = [$prevlink, '']; + } + + if ($nextlink) { + $nextlink = [$nextlink, '']; } } } @@ -1283,33 +1272,23 @@ function photos_content(App $a) $album_link = 'photos/' . $a->data['user']['nickname'] . '/album/' . bin2hex($ph[0]['album']); $tools = null; - $lock = null; if ($can_post && ($ph[0]['uid'] == $owner_uid)) { - $tools = [ - 'edit' => ['photos/' . $a->data['user']['nickname'] . '/image/' . $datum . (($cmd === 'edit') ? '' : '/edit'), (($cmd === 'edit') ? L10n::t('View photo') : L10n::t('Edit photo'))], - 'profile'=>['profile_photo/use/'.$ph[0]['resource-id'], L10n::t('Use as profile photo')], - ]; + $tools = []; + if ($cmd === 'edit') { + $tools['view'] = ['photos/' . $a->data['user']['nickname'] . '/image/' . $datum, L10n::t('View photo')]; + } else { + $tools['edit'] = ['photos/' . $a->data['user']['nickname'] . '/image/' . $datum . '/edit', L10n::t('Edit photo')]; + $tools['delete'] = ['photos/' . $a->data['user']['nickname'] . '/image/' . $datum . '/drop', L10n::t('Delete photo')]; + $tools['profile'] = ['profile_photo/use/'.$ph[0]['resource-id'], L10n::t('Use as profile photo')]; + } - // lock - $lock = ((($ph[0]['uid'] == local_user()) && (strlen($ph[0]['allow_cid']) || strlen($ph[0]['allow_gid']) - || strlen($ph[0]['deny_cid']) || strlen($ph[0]['deny_gid']))) - ? L10n::t('Private Message') - : Null); - - - } - - if ($cmd === 'edit') { - $tpl = Renderer::getMarkupTemplate('photo_edit_head.tpl'); - $a->page['htmlhead'] .= Renderer::replaceMacros($tpl,[ - '$prevlink' => $prevlink, - '$nextlink' => $nextlink - ]); - } - - if ($prevlink) { - $prevlink = [$prevlink, '']; + if ( + $ph[0]['uid'] == local_user() + && (strlen($ph[0]['allow_cid']) || strlen($ph[0]['allow_gid']) || strlen($ph[0]['deny_cid']) || strlen($ph[0]['deny_gid'])) + ) { + $tools['lock'] = L10n::t('Private Photo'); + } } $photo = [ @@ -1322,11 +1301,6 @@ function photos_content(App $a) 'filename' => $hires['filename'], ]; - if ($nextlink) { - $nextlink = [$nextlink, '']; - } - - // Do we have an item for this photo? // FIXME! - replace following code to display the conversation with our normal @@ -1431,7 +1405,7 @@ function photos_content(App $a) $tpl = Renderer::getMarkupTemplate('photo_item.tpl'); $return_path = $a->cmd; - if ($can_post || Security::canWriteToUserWall($owner_uid)) { + if ($cmd === 'view' && ($can_post || Security::canWriteToUserWall($owner_uid))) { $like_tpl = Renderer::getMarkupTemplate('like_noshare.tpl'); $likebuttons = Renderer::replaceMacros($like_tpl, [ '$id' => $link_item['id'], @@ -1510,7 +1484,7 @@ function photos_content(App $a) continue; } - $profile_url = Contact::MagicLinkById($item['author-id']); + $profile_url = Contact::magicLinkbyId($item['author-id']); if (strpos($profile_url, 'redir/') === 0) { $sparkle = ' sparkle'; } else { @@ -1537,7 +1511,7 @@ function photos_content(App $a) '$title' => $title_e, '$body' => $body_e, '$ago' => Temporal::getRelativeDate($item['created']), - '$indent' => (($item['parent'] != $item['item_id']) ? ' comment' : ''), + '$indent' => (($item['parent'] != $item['id']) ? ' comment' : ''), '$drop' => $drop, '$comment' => $comment ]); @@ -1546,7 +1520,7 @@ function photos_content(App $a) $comments .= Renderer::replaceMacros($cmnt_tpl, [ '$return_path' => '', '$jsreload' => $return_path, - '$id' => $item['item_id'], + '$id' => $item['id'], '$parent' => $item['parent'], '$profile_uid' => $owner_uid, '$mylink' => $contact['url'], @@ -1574,7 +1548,6 @@ function photos_content(App $a) '$id' => $ph[0]['id'], '$album' => [$album_link, $ph[0]['album']], '$tools' => $tools, - '$lock' => $lock, '$photo' => $photo, '$prevlink' => $prevlink, '$nextlink' => $nextlink, diff --git a/mod/phpinfo.php b/mod/phpinfo.php deleted file mode 100644 index 3228d250b..000000000 --- a/mod/phpinfo.php +++ /dev/null @@ -1,14 +0,0 @@ -isFriendicaApp() || !$regularnotifications) { - $notif['message'] = str_replace("{0}", $notif['name'], $notif['message']); - } - $contact = Contact::getDetailsByURL($notif['url']); if (isset($contact['micro'])) { $notif['photo'] = ProxyUtils::proxifyUrl($contact['micro'], false, ProxyUtils::SIZE_MICRO); diff --git a/mod/poco.php b/mod/poco.php index 064e0e9a8..c288f6b63 100644 --- a/mod/poco.php +++ b/mod/poco.php @@ -22,7 +22,7 @@ function poco_init(App $a) { $system_mode = false; if (intval(Config::get('system', 'block_public')) || (Config::get('system', 'block_local_dir'))) { - System::httpExit(401); + throw new \Friendica\Network\HTTPException\ForbiddenException(); } if ($a->argc > 1) { @@ -31,7 +31,7 @@ function poco_init(App $a) { if (empty($nickname)) { $c = q("SELECT * FROM `pconfig` WHERE `cat` = 'system' AND `k` = 'suggestme' AND `v` = 1"); if (!DBA::isResult($c)) { - System::httpExit(401); + throw new \Friendica\Network\HTTPException\ForbiddenException(); } $system_mode = true; } @@ -73,7 +73,7 @@ function poco_init(App $a) { DBA::escape($nickname) ); if (! DBA::isResult($users) || $users[0]['hidewall'] || $users[0]['hide-friends']) { - System::httpExit(404); + throw new \Friendica\Network\HTTPException\NotFoundException(); } $user = $users[0]; @@ -371,8 +371,9 @@ function poco_init(App $a) { $ret['entry'][] = []; } } else { - System::httpExit(500); + throw new \Friendica\Network\HTTPException\InternalServerErrorException(); } + Logger::log("End of poco", Logger::DEBUG); if ($format === 'xml') { @@ -385,6 +386,6 @@ function poco_init(App $a) { echo json_encode($ret); exit(); } else { - System::httpExit(500); + throw new \Friendica\Network\HTTPException\InternalServerErrorException(); } } diff --git a/mod/poke.php b/mod/poke.php index b8a1ba7bf..e8ddf86cd 100644 --- a/mod/poke.php +++ b/mod/poke.php @@ -154,15 +154,11 @@ function poke_content(App $a) $name = $contact['name']; $id = $contact['id']; - $base = System::baseUrl(); - $head_tpl = Renderer::getMarkupTemplate('poke_head.tpl'); $a->page['htmlhead'] .= Renderer::replaceMacros($head_tpl,[ '$baseurl' => System::baseUrl(true), - '$base' => $base ]); - $parent = (!empty($_GET['parent']) ? intval($_GET['parent']) : '0'); diff --git a/mod/pretheme.php b/mod/pretheme.php deleted file mode 100644 index 14d1f2b9e..000000000 --- a/mod/pretheme.php +++ /dev/null @@ -1,25 +0,0 @@ - Theme::getScreenshot($theme), 'desc' => $desc, 'version' => $version, 'credits' => $credits]); - } - - exit(); -} diff --git a/mod/probe.php b/mod/probe.php deleted file mode 100644 index e120ce172..000000000 --- a/mod/probe.php +++ /dev/null @@ -1,37 +0,0 @@ - L10n::t("Public access denied."), - "description" => L10n::t("Only logged in users are permitted to perform a probing.")]); - exit(); - } - - $o = '
    '; - $o .= '

    Probe Diagnostic

    '; - - $o .= '
    '; - $o .= 'Lookup address: '; - $o .= '
    '; - - $o .= '

    '; - - if (!empty($_GET['addr'])) { - $addr = trim($_GET['addr']); - $res = Probe::uri($addr, "", 0, false); - $o .= '
    ';
    -		$o .= str_replace("\n", '
    ', print_r($res, true)); - $o .= '
    '; - } - $o .= '
    '; - - return $o; -} diff --git a/mod/pubsub.php b/mod/pubsub.php index c5744f399..e5ede6c80 100644 --- a/mod/pubsub.php +++ b/mod/pubsub.php @@ -12,10 +12,9 @@ use Friendica\Core\System; function hub_return($valid, $body) { if ($valid) { - header($_SERVER["SERVER_PROTOCOL"] . ' 200 OK'); echo $body; } else { - System::httpExit(404); + throw new \Friendica\Network\HTTPException\NotFoundException(); } exit(); } @@ -24,7 +23,7 @@ function hub_return($valid, $body) function hub_post_return() { - System::httpExit(200); + throw new \Friendica\Network\HTTPException\OKException(); } function pubsub_init(App $a) diff --git a/mod/pubsubhubbub.php b/mod/pubsubhubbub.php index 342facd10..f71984ec1 100644 --- a/mod/pubsubhubbub.php +++ b/mod/pubsubhubbub.php @@ -17,7 +17,7 @@ function pubsubhubbub_init(App $a) { // PuSH subscription must be considered "public" so just block it // if public access isn't enabled. if (Config::get('system', 'block_public')) { - System::httpExit(403); + throw new \Friendica\Network\HTTPException\ForbiddenException(); } // Subscription request from subscriber @@ -44,7 +44,7 @@ function pubsubhubbub_init(App $a) { $subscribe = 0; } else { Logger::log("Invalid hub_mode=$hub_mode, ignoring."); - System::httpExit(404); + throw new \Friendica\Network\HTTPException\NotFoundException(); } Logger::log("$hub_mode request from " . $_SERVER['REMOTE_ADDR']); @@ -61,7 +61,7 @@ function pubsubhubbub_init(App $a) { if (!$nick) { Logger::log('Bad hub_topic=$hub_topic, ignoring.'); - System::httpExit(404); + throw new \Friendica\Network\HTTPException\NotFoundException(); } // fetch user from database given the nickname @@ -69,13 +69,13 @@ function pubsubhubbub_init(App $a) { $owner = DBA::selectFirst('user', ['uid', 'hidewall', 'nickname'], $condition); if (!DBA::isResult($owner)) { Logger::log('Local account not found: ' . $nick . ' - topic: ' . $hub_topic . ' - callback: ' . $hub_callback); - System::httpExit(404); + throw new \Friendica\Network\HTTPException\NotFoundException(); } // abort if user's wall is supposed to be private if ($owner['hidewall']) { Logger::log('Local user ' . $nick . 'has chosen to hide wall, ignoring.'); - System::httpExit(403); + throw new \Friendica\Network\HTTPException\ForbiddenException(); } // get corresponding row from contact table @@ -84,7 +84,7 @@ function pubsubhubbub_init(App $a) { $contact = DBA::selectFirst('contact', ['poll'], $condition); if (!DBA::isResult($contact)) { Logger::log('Self contact for user ' . $owner['uid'] . ' not found.'); - System::httpExit(404); + throw new \Friendica\Network\HTTPException\NotFoundException(); } // sanity check that topic URLs are the same @@ -93,7 +93,7 @@ function pubsubhubbub_init(App $a) { if (!Strings::compareLink($hub_topic, $contact['poll']) && !Strings::compareLink($hub_topic2, $contact['poll']) && !Strings::compareLink($hub_topic, $self)) { Logger::log('Hub topic ' . $hub_topic . ' != ' . $contact['poll']); - System::httpExit(404); + throw new \Friendica\Network\HTTPException\NotFoundException(); } // do subscriber verification according to the PuSH protocol @@ -121,19 +121,19 @@ function pubsubhubbub_init(App $a) { // give up if the HTTP return code wasn't a success (2xx) if ($ret < 200 || $ret > 299) { Logger::log("Subscriber verification for $hub_topic at $hub_callback returned $ret, ignoring."); - System::httpExit(404); + throw new \Friendica\Network\HTTPException\NotFoundException(); } // check that the correct hub_challenge code was echoed back if (trim($body) !== $hub_challenge) { Logger::log("Subscriber did not echo back hub.challenge, ignoring."); Logger::log("\"$hub_challenge\" != \"".trim($body)."\""); - System::httpExit(404); + throw new \Friendica\Network\HTTPException\NotFoundException(); } PushSubscriber::renew($owner['uid'], $nick, $subscribe, $hub_callback, $hub_topic, $hub_secret); - System::httpExit(202); + throw new \Friendica\Network\HTTPException\AcceptedException(); } exit(); } diff --git a/mod/randprof.php b/mod/randprof.php deleted file mode 100644 index 10bff588e..000000000 --- a/mod/randprof.php +++ /dev/null @@ -1,19 +0,0 @@ -redirect($link); - } - - $a->internalRedirect('profile'); -} diff --git a/mod/receive.php b/mod/receive.php index b0258acbd..db1287ea6 100644 --- a/mod/receive.php +++ b/mod/receive.php @@ -22,7 +22,7 @@ function receive_post(App $a) $enabled = intval(Config::get('system', 'diaspora_enabled')); if (!$enabled) { Logger::log('mod-diaspora: disabled'); - System::httpExit(500); + throw new \Friendica\Network\HTTPException\InternalServerErrorException(); } if (($a->argc == 2) && ($a->argv[1] === 'public')) { @@ -32,13 +32,13 @@ function receive_post(App $a) $public = false; if ($a->argc != 3 || $a->argv[1] !== 'users') { - System::httpExit(500); + throw new \Friendica\Network\HTTPException\InternalServerErrorException(); } $guid = $a->argv[2]; $importer = DBA::selectFirst('user', [], ['guid' => $guid, 'account_expired' => false, 'account_removed' => false]); if (!DBA::isResult($importer)) { - System::httpExit(500); + throw new \Friendica\Network\HTTPException\InternalServerErrorException(); } } @@ -49,7 +49,7 @@ function receive_post(App $a) if (empty($_POST['xml'])) { $postdata = file_get_contents("php://input"); if ($postdata == '') { - System::httpExit(500); + throw new \Friendica\Network\HTTPException\InternalServerErrorException(); } Logger::log('mod-diaspora: message is in the new format', Logger::DEBUG); @@ -71,7 +71,7 @@ function receive_post(App $a) Logger::log('mod-diaspora: decoded msg: ' . print_r($msg, true), Logger::DATA); if (!is_array($msg)) { - System::httpExit(500); + throw new \Friendica\Network\HTTPException\InternalServerErrorException(); } Logger::log('mod-diaspora: dispatching', Logger::DEBUG); @@ -83,6 +83,9 @@ function receive_post(App $a) $ret = Diaspora::dispatch($importer, $msg); } - System::httpExit(($ret) ? 200 : 500); - // NOTREACHED + if ($ret) { + throw new \Friendica\Network\HTTPException\OKException(); + } else { + throw new \Friendica\Network\HTTPException\InternalServerErrorException(); + } } diff --git a/mod/redir.php b/mod/redir.php index 4dbae5498..233ec9b00 100644 --- a/mod/redir.php +++ b/mod/redir.php @@ -3,12 +3,13 @@ use Friendica\App; use Friendica\Core\L10n; use Friendica\Core\Logger; +use Friendica\Core\Session; use Friendica\Core\System; use Friendica\Database\DBA; use Friendica\Model\Contact; use Friendica\Model\Profile; -use Friendica\Util\Strings; use Friendica\Util\Network; +use Friendica\Util\Strings; function redir_init(App $a) { @@ -70,7 +71,9 @@ function redir_init(App $a) { && is_array($_SESSION['remote'])) { foreach ($_SESSION['remote'] as $v) { - if ($v['uid'] == $_SESSION['visitor_visiting'] && $v['cid'] == $_SESSION['visitor_id']) { + if (!empty($v['uid']) && !empty($v['cid']) && + $v['uid'] == Session::get('visitor_visiting') && + $v['cid'] == Session::get('visitor_id')) { // Remote user is already authenticated. $target_url = defaults($url, $contact_url); Logger::log($contact['name'] . " is already authenticated. Redirecting to " . $target_url, Logger::DEBUG); diff --git a/mod/robots_txt.php b/mod/robots_txt.php deleted file mode 100644 index 0575742dd..000000000 --- a/mod/robots_txt.php +++ /dev/null @@ -1,30 +0,0 @@ - - - - Friendica - http://friendica.com/ - - - - http://status.net/wiki/TwitterCompatibleAPI - false - - - - -'; - - exit(); -} diff --git a/mod/salmon.php b/mod/salmon.php index 84d2942d5..ba1bc8d46 100644 --- a/mod/salmon.php +++ b/mod/salmon.php @@ -28,7 +28,7 @@ function salmon_post(App $a, $xml = '') { DBA::escape($nick) ); if (! DBA::isResult($r)) { - System::httpExit(500); + throw new \Friendica\Network\HTTPException\InternalServerErrorException(); } $importer = $r[0]; @@ -49,7 +49,7 @@ function salmon_post(App $a, $xml = '') { if (empty($base)) { Logger::log('unable to locate salmon data in xml '); - System::httpExit(400); + throw new \Friendica\Network\HTTPException\BadRequestException(); } // Stash the signature away for now. We have to find their key or it won't be good for anything. @@ -87,7 +87,7 @@ function salmon_post(App $a, $xml = '') { if(! $author_link) { Logger::log('Could not retrieve author URI.'); - System::httpExit(400); + throw new \Friendica\Network\HTTPException\BadRequestException(); } // Once we have the author URI, go to the web and try to find their public key @@ -98,7 +98,7 @@ function salmon_post(App $a, $xml = '') { if(! $key) { Logger::log('Could not retrieve author key.'); - System::httpExit(400); + throw new \Friendica\Network\HTTPException\BadRequestException(); } $key_info = explode('.',$key); @@ -130,7 +130,7 @@ function salmon_post(App $a, $xml = '') { if (! $verify) { Logger::log('Message did not verify. Discarding.'); - System::httpExit(400); + throw new \Friendica\Network\HTTPException\BadRequestException(); } Logger::log('Message verified with mode '.$mode); @@ -177,8 +177,7 @@ function salmon_post(App $a, $xml = '') { //if((DBA::isResult($r)) && (($r[0]['readonly']) || ($r[0]['rel'] == Contact::FOLLOWER) || ($r[0]['blocked']))) { if (DBA::isResult($r) && $r[0]['blocked']) { Logger::log('Ignoring this author.'); - System::httpExit(202); - // NOTREACHED + throw new \Friendica\Network\HTTPException\AcceptedException(); } // Placeholder for hub discovery. @@ -188,5 +187,5 @@ function salmon_post(App $a, $xml = '') { OStatus::import($data, $importer, $contact_rec, $hub); - System::httpExit(200); + throw new \Friendica\Network\HTTPException\OKException(); } diff --git a/mod/search.php b/mod/search.php index 9a70bcac9..4144e2608 100644 --- a/mod/search.php +++ b/mod/search.php @@ -12,13 +12,11 @@ use Friendica\Core\Config; use Friendica\Core\L10n; use Friendica\Core\Logger; use Friendica\Core\Renderer; -use Friendica\Core\System; use Friendica\Database\DBA; use Friendica\Model\Item; +use Friendica\Module\BaseSearchModule; use Friendica\Util\Strings; -require_once 'mod/dirfind.php'; - function search_saved_searches() { $o = ''; @@ -98,12 +96,9 @@ function search_content(App $a) { } if (Config::get('system','local_search') && !local_user() && !remote_user()) { - System::httpExit(403, - ["title" => L10n::t("Public access denied."), - "description" => L10n::t("Only logged in users are permitted to perform a search.")]); - exit(); - //notice(L10n::t('Public access denied.').EOL); - //return; + $e = new \Friendica\Network\HTTPException\ForbiddenException(L10n::t("Only logged in users are permitted to perform a search.")); + $e->httpdesc = L10n::t("Public access denied."); + throw $e; } if (Config::get('system','permit_crawling') && !local_user() && !remote_user()) { @@ -123,10 +118,7 @@ function search_content(App $a) { if (!is_null($result)) { $resultdata = json_decode($result); if (($resultdata->time > (time() - $crawl_permit_period)) && ($resultdata->accesses > $free_crawls)) { - System::httpExit(429, - ["title" => L10n::t("Too Many Requests"), - "description" => L10n::t("Only one search per minute is permitted for not logged in users.")]); - exit(); + throw new \Friendica\Network\HTTPException\TooManyRequestsException(L10n::t("Only one search per minute is permitted for not logged in users.")); } Cache::set("remote_search:".$remote, json_encode(["time" => time(), "accesses" => $resultdata->accesses + 1]), Cache::HOUR); } else @@ -156,10 +148,10 @@ function search_content(App $a) { $search = substr($search,1); } if (strpos($search,'@') === 0) { - return dirfind_content($a); + return BaseSearchModule::performSearch(); } if (strpos($search,'!') === 0) { - return dirfind_content($a); + return BaseSearchModule::performSearch(); } if (!empty($_GET['search-option'])) @@ -170,11 +162,9 @@ function search_content(App $a) { $tag = true; break; case 'contacts': - return dirfind_content($a, "@"); - break; + return BaseSearchModule::performSearch('@'); case 'forums': - return dirfind_content($a, "!"); - break; + return BaseSearchModule::performSearch('!'); } if (!$search) diff --git a/mod/settings.php b/mod/settings.php index b1be28f8c..ab7586733 100644 --- a/mod/settings.php +++ b/mod/settings.php @@ -27,9 +27,12 @@ use Friendica\Protocol\Email; use Friendica\Util\Network; use Friendica\Util\Strings; use Friendica\Util\Temporal; +use Friendica\Worker\Delivery; function get_theme_config_file($theme) { + $theme = Strings::sanitizeFilePathItem($theme); + $a = \get_app(); $base_theme = defaults($a->theme_info, 'extends'); @@ -65,6 +68,13 @@ function settings_init(App $a) ], ]; + $tabs[] = [ + 'label' => L10n::t('Two-factor authentication'), + 'url' => 'settings/2fa', + 'selected' => (($a->argc > 1) && ($a->argv[1] === '2fa') ? 'active' : ''), + 'accesskey' => 'o', + ]; + $tabs[] = [ 'label' => L10n::t('Profiles'), 'url' => 'profiles', @@ -380,7 +390,7 @@ function settings_post(App $a) BaseModule::checkFormSecurityTokenRedirectOnError('/settings', 'settings'); if (!empty($_POST['resend_relocate'])) { - Worker::add(PRIORITY_HIGH, 'Notifier', 'relocate', local_user()); + Worker::add(PRIORITY_HIGH, 'Notifier', Delivery::RELOCATION, local_user()); info(L10n::t("Relocate message has been send to your contacts")); $a->internalRedirect('settings'); } @@ -877,40 +887,30 @@ function settings_content(App $a) $default_mobile_theme = 'none'; } - $allowed_themes_str = Config::get('system', 'allowed_themes'); - $allowed_themes_raw = explode(',', $allowed_themes_str); - $allowed_themes = []; - if (count($allowed_themes_raw)) { - foreach ($allowed_themes_raw as $x) { - if (strlen(trim($x)) && is_dir("view/theme/$x")) { - $allowed_themes[] = trim($x); - } - } - } - + $allowed_themes = Theme::getAllowedList(); $themes = []; $mobile_themes = ["---" => L10n::t('No special theme for mobile devices')]; - if ($allowed_themes) { - foreach ($allowed_themes as $theme) { - $is_experimental = file_exists('view/theme/' . $theme . '/experimental'); - $is_unsupported = file_exists('view/theme/' . $theme . '/unsupported'); - $is_mobile = file_exists('view/theme/' . $theme . '/mobile'); - if (!$is_experimental || ($is_experimental && (Config::get('experimentals', 'exp_themes')==1 || is_null(Config::get('experimentals', 'exp_themes'))))) { - $theme_name = ucfirst($theme); - if ($is_unsupported) { - $theme_name = L10n::t("%s - \x28Unsupported\x29", $theme_name); - } elseif ($is_experimental) { - $theme_name = L10n::t("%s - \x28Experimental\x29", $theme_name); - } - if ($is_mobile) { - $mobile_themes[$theme] = $theme_name; - } else { - $themes[$theme] = $theme_name; - } + foreach ($allowed_themes as $theme) { + $is_experimental = file_exists('view/theme/' . $theme . '/experimental'); + $is_unsupported = file_exists('view/theme/' . $theme . '/unsupported'); + $is_mobile = file_exists('view/theme/' . $theme . '/mobile'); + if (!$is_experimental || ($is_experimental && (Config::get('experimentals', 'exp_themes')==1 || is_null(Config::get('experimentals', 'exp_themes'))))) { + $theme_name = ucfirst($theme); + if ($is_unsupported) { + $theme_name = L10n::t('%s - (Unsupported)', $theme_name); + } elseif ($is_experimental) { + $theme_name = L10n::t('%s - (Experimental)', $theme_name); + } + + if ($is_mobile) { + $mobile_themes[$theme] = $theme_name; + } else { + $themes[$theme] = $theme_name; } } } + $theme_selected = defaults($_SESSION, 'theme' , $default_theme); $mobile_theme_selected = defaults($_SESSION, 'mobile-theme', $default_mobile_theme); diff --git a/mod/smilies.php b/mod/smilies.php deleted file mode 100644 index bbb7de2e2..000000000 --- a/mod/smilies.php +++ /dev/null @@ -1,32 +0,0 @@ -argv[1]) && ($a->argv[1] === "json")) { - $results = []; - for ($i = 0; $i < count($smilies['texts']); $i++) { - $results[] = ['text' => $smilies['texts'][$i], 'icon' => $smilies['icons'][$i]]; - } - System::jsonExit($results); - } else { - $s = '
    '; - for ($x = 0; $x < count($smilies['texts']); $x ++) { - $s .= '
    ' . $smilies['texts'][$x] . '
    ' . $smilies['icons'][$x] . '
    '; - } - $s .= '
    '; - - return $s; - } -} diff --git a/mod/starred.php b/mod/starred.php deleted file mode 100644 index 0705d5436..000000000 --- a/mod/starred.php +++ /dev/null @@ -1,51 +0,0 @@ -argc > 1) { - $message_id = intval($a->argv[1]); - } - if (!$message_id) { - exit(); - } - - $item = Item::selectFirstForUser(local_user(), ['starred'], ['uid' => local_user(), 'id' => $message_id]); - if (!DBA::isResult($item)) { - exit(); - } - - if (!intval($item['starred'])) { - $starred = 1; - } - - Item::update(['starred' => $starred], ['id' => $message_id]); - - // See if we've been passed a return path to redirect to - $return_path = defaults($_REQUEST, 'return', ''); - if ($return_path) { - $rand = '_=' . time(); - if (strpos($return_path, '?')) { - $rand = "&$rand"; - } else { - $rand = "?$rand"; - } - - $a->internalRedirect($return_path . $rand); - } - - // the json doesn't really matter, it will either be 0 or 1 - - echo json_encode($starred); - exit(); -} diff --git a/mod/statistics_json.php b/mod/statistics_json.php deleted file mode 100644 index a8d3c8a5f..000000000 --- a/mod/statistics_json.php +++ /dev/null @@ -1,63 +0,0 @@ - Config::get('config', 'sitename'), - "network" => FRIENDICA_PLATFORM, - "version" => FRIENDICA_VERSION . "-" . DB_UPDATE_VERSION, - "registrations_open" => $registration_open, - "total_users" => Config::get('nodeinfo', 'total_users'), - "active_users_halfyear" => Config::get('nodeinfo', 'active_users_halfyear'), - "active_users_monthly" => Config::get('nodeinfo', 'active_users_monthly'), - "local_posts" => Config::get('nodeinfo', 'local_posts') - ]; - - $statistics["services"] = []; - $statistics["services"]["appnet"] = Addon::isEnabled("appnet"); - $statistics["services"]["blogger"] = Addon::isEnabled("blogger"); - $statistics["services"]["buffer"] = Addon::isEnabled("buffer"); - $statistics["services"]["dreamwidth"] = Addon::isEnabled("dwpost"); - $statistics["services"]["gnusocial"] = Addon::isEnabled("statusnet"); - $statistics["services"]["libertree"] = Addon::isEnabled("libertree"); - $statistics["services"]["livejournal"] = Addon::isEnabled("ljpost"); - $statistics["services"]["pumpio"] = Addon::isEnabled("pumpio"); - $statistics["services"]["twitter"] = Addon::isEnabled("twitter"); - $statistics["services"]["tumblr"] = Addon::isEnabled("tumblr"); - $statistics["services"]["wordpress"] = Addon::isEnabled("wppost"); - - $statistics["appnet"] = $statistics["services"]["appnet"]; - $statistics["blogger"] = $statistics["services"]["blogger"]; - $statistics["buffer"] = $statistics["services"]["buffer"]; - $statistics["dreamwidth"] = $statistics["services"]["dreamwidth"]; - $statistics["gnusocial"] = $statistics["services"]["gnusocial"]; - $statistics["libertree"] = $statistics["services"]["libertree"]; - $statistics["livejournal"] = $statistics["services"]["livejournal"]; - $statistics["pumpio"] = $statistics["services"]["pumpio"]; - $statistics["twitter"] = $statistics["services"]["twitter"]; - $statistics["tumblr"] = $statistics["services"]["tumblr"]; - $statistics["wordpress"] = $statistics["services"]["wordpress"]; - - header("Content-Type: application/json"); - echo json_encode($statistics, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); - Logger::log("statistics_init: printed " . print_r($statistics, true), Logger::DATA); - exit(); -} diff --git a/mod/subthread.php b/mod/subthread.php index 23ebf4fe4..9fa1a410d 100644 --- a/mod/subthread.php +++ b/mod/subthread.php @@ -87,7 +87,7 @@ function subthread_content(App $a) { $post_type = (($item['resource-id']) ? L10n::t('photo') : L10n::t('status')); $objtype = (($item['resource-id']) ? ACTIVITY_OBJ_IMAGE : ACTIVITY_OBJ_NOTE ); - $link = XML::escape('' . "\n"); + $link = XML::escape('' . "\n"); $body = $item['body']; $obj = <<< EOT @@ -128,7 +128,7 @@ EOT; $ulink = '[url=' . $contact['url'] . ']' . $contact['name'] . '[/url]'; $alink = '[url=' . $item['author-link'] . ']' . $item['author-name'] . '[/url]'; - $plink = '[url=' . System::baseUrl() . '/display/' . $owner['nickname'] . '/' . $item['id'] . ']' . $post_type . '[/url]'; + $plink = '[url=' . System::baseUrl() . '/display/' . $item['guid'] . ']' . $post_type . '[/url]'; $arr['body'] = sprintf( $bodyverb, $ulink, $alink, $plink ); $arr['verb'] = $activity; diff --git a/mod/suggest.php b/mod/suggest.php index bca2694d3..4b67dd6eb 100644 --- a/mod/suggest.php +++ b/mod/suggest.php @@ -19,39 +19,16 @@ function suggest_init(App $a) if (! local_user()) { return; } +} - if (!empty($_GET['ignore'])) { - // Check if we should do HTML-based delete confirmation - if ($_REQUEST['confirm']) { - //
    can't take arguments in its "action" parameter - // so add any arguments as hidden inputs - $query = explode_querystring($a->query_string); - $inputs = []; - foreach ($query['args'] as $arg) { - if (strpos($arg, 'confirm=') === false) { - $arg_parts = explode('=', $arg); - $inputs[] = ['name' => $arg_parts[0], 'value' => $arg_parts[1]]; - } - } - - $a->page['content'] = Renderer::replaceMacros(Renderer::getMarkupTemplate('confirm.tpl'), [ - '$method' => 'get', - '$message' => L10n::t('Do you really want to delete this suggestion?'), - '$extra_inputs' => $inputs, - '$confirm' => L10n::t('Yes'), - '$confirm_url' => $query['base'], - '$confirm_name' => 'confirmed', - '$cancel' => L10n::t('Cancel'), - ]); - $a->error = 1; // Set $a->error so the other module functions don't execute - return; - } - // Now check how the user responded to the confirmation query - if (!$_REQUEST['canceled']) { - DBA::insert('gcign', ['uid' => local_user(), 'gcid' => $_GET['ignore']]); - } +function suggest_post(App $a) +{ + if (!empty($_POST['ignore']) && !empty($_POST['confirm'])) { + DBA::insert('gcign', ['uid' => local_user(), 'gcid' => $_POST['ignore']]); + notice(L10n::t('Contact suggestion successfully ignored.')); } + $a->internalRedirect('suggest'); } function suggest_content(App $a) @@ -76,11 +53,34 @@ function suggest_content(App $a) return $o; } + + if (!empty($_GET['ignore'])) { + // can't take arguments in its "action" parameter + // so add any arguments as hidden inputs + $query = explode_querystring($a->query_string); + $inputs = []; + foreach ($query['args'] as $arg) { + if (strpos($arg, 'confirm=') === false) { + $arg_parts = explode('=', $arg); + $inputs[] = ['name' => $arg_parts[0], 'value' => $arg_parts[1]]; + } + } + + return Renderer::replaceMacros(Renderer::getMarkupTemplate('confirm.tpl'), [ + '$method' => 'post', + '$message' => L10n::t('Do you really want to delete this suggestion?'), + '$extra_inputs' => $inputs, + '$confirm' => L10n::t('Yes'), + '$confirm_url' => $query['base'], + '$confirm_name' => 'confirm', + '$cancel' => L10n::t('Cancel'), + ]); + } + $id = 0; $entries = []; foreach ($r as $rr) { - $connlnk = System::baseUrl() . '/follow/?url=' . (($rr['connect']) ? $rr['connect'] : $rr['url']); $ignlnk = System::baseUrl() . '/suggest?ignore=' . $rr['id']; $photo_menu = [ diff --git a/mod/tagger.php b/mod/tagger.php index 7cb43e330..2c15cdd28 100644 --- a/mod/tagger.php +++ b/mod/tagger.php @@ -12,6 +12,7 @@ use Friendica\Database\DBA; use Friendica\Model\Item; use Friendica\Util\Strings; use Friendica\Util\XML; +use Friendica\Worker\Delivery; function tagger_content(App $a) { @@ -40,14 +41,12 @@ function tagger_content(App $a) { } $owner_uid = $item['uid']; - $owner_nick = ''; $blocktags = 0; - $r = q("select `nickname`,`blocktags` from user where uid = %d limit 1", + $r = q("select `blocktags` from user where uid = %d limit 1", intval($owner_uid) ); if (DBA::isResult($r)) { - $owner_nick = $r[0]['nickname']; $blocktags = $r[0]['blocktags']; } @@ -69,12 +68,7 @@ function tagger_content(App $a) { $xterm = XML::escape($term); $post_type = (($item['resource-id']) ? L10n::t('photo') : L10n::t('status')); $targettype = (($item['resource-id']) ? ACTIVITY_OBJ_IMAGE : ACTIVITY_OBJ_NOTE ); - - if ($owner_nick) { - $href = System::baseUrl() . '/display/' . $owner_nick . '/' . $item['id']; - } else { - $href = System::baseUrl() . '/display/' . $item['guid']; - } + $href = System::baseUrl() . '/display/' . $item['guid']; $link = XML::escape('' . "\n"); @@ -201,7 +195,7 @@ EOT; Hook::callAll('post_local_end', $arr); - Worker::add(PRIORITY_HIGH, "Notifier", "tag", $post_id); + Worker::add(PRIORITY_HIGH, "Notifier", Delivery::POST, $post_id); exit(); } diff --git a/mod/toggle_mobile.php b/mod/toggle_mobile.php deleted file mode 100644 index 2821e0a64..000000000 --- a/mod/toggle_mobile.php +++ /dev/null @@ -1,21 +0,0 @@ -redirect($address); -} diff --git a/mod/uexport.php b/mod/uexport.php index 17715ac35..dfeb25abd 100644 --- a/mod/uexport.php +++ b/mod/uexport.php @@ -2,20 +2,27 @@ /** * @file mod/uexport.php */ + use Friendica\App; use Friendica\Core\Hook; use Friendica\Core\L10n; use Friendica\Core\Renderer; use Friendica\Core\System; use Friendica\Database\DBA; +use Friendica\Database\DBStructure; function uexport_init(App $a) { + /// @todo Don't forget to move this global field as static field in src/Modules + global $dbStructure; + if (!local_user()) { exit(); } require_once("mod/settings.php"); settings_init($a); + + $dbStructure = DBStructure::definition($a->getBasePath()); } function uexport_content(App $a) { @@ -49,20 +56,31 @@ function uexport_content(App $a) { $tpl = Renderer::getMarkupTemplate("uexport.tpl"); return Renderer::replaceMacros($tpl, [ - '$baseurl' => System::baseUrl(), '$title' => L10n::t('Export personal data'), '$options' => $options ]); } function _uexport_multirow($query) { + global $dbStructure; + + preg_match("/\s+from\s+`?([a-z\d_]+)`?/i", $query, $match); + $table = $match[1]; + $result = []; $r = q($query); if (DBA::isResult($r)) { foreach ($r as $rr) { $p = []; foreach ($rr as $k => $v) { - $p[$k] = $v; + switch ($dbStructure[$table]['fields'][$k]['type']) { + case 'datetime': + $p[$k] = $v ?? DBA::NULL_DATETIME; + break; + default: + $p[$k] = $v; + break; + } } $result[] = $p; } @@ -71,12 +89,25 @@ function _uexport_multirow($query) { } function _uexport_row($query) { + global $dbStructure; + + preg_match("/\s+from\s+`?([a-z\d_]+)`?/i", $query, $match); + $table = $match[1]; + $result = []; $r = q($query); if (DBA::isResult($r)) { + foreach ($r as $rr) { foreach ($rr as $k => $v) { - $result[$k] = $v; + switch ($dbStructure[$table]['fields'][$k]['type']) { + case 'datetime': + $result[$k] = $v ?? DBA::NULL_DATETIME; + break; + default: + $result[$k] = $v; + break; + } } } } diff --git a/mod/videos.php b/mod/videos.php index 4120c136f..9e19ecf11 100644 --- a/mod/videos.php +++ b/mod/videos.php @@ -49,7 +49,7 @@ function videos_init(App $a) $account_type = Contact::getAccountType($profile); - $tpl = Renderer::getMarkupTemplate("vcard-widget.tpl"); + $tpl = Renderer::getMarkupTemplate("widget/vcard.tpl"); $vcard_widget = Renderer::replaceMacros($tpl, [ '$name' => $profile['name'], @@ -67,9 +67,7 @@ function videos_init(App $a) $a->page['aside'] .= $vcard_widget; $tpl = Renderer::getMarkupTemplate("videos_head.tpl"); - $a->page['htmlhead'] .= Renderer::replaceMacros($tpl,[ - '$baseurl' => System::baseUrl(), - ]); + $a->page['htmlhead'] .= Renderer::replaceMacros($tpl); } return; @@ -84,33 +82,6 @@ function videos_post(App $a) } if (($a->argc == 2) && !empty($_POST['delete']) && !empty($_POST['id'])) { - // Check if we should do HTML-based delete confirmation - if (empty($_REQUEST['confirm'])) { - if (!empty($_REQUEST['canceled'])) { - $a->internalRedirect('videos/' . $a->data['user']['nickname']); - } - - $drop_url = $a->query_string; - - $a->page['content'] = Renderer::replaceMacros(Renderer::getMarkupTemplate('confirm.tpl'), [ - '$method' => 'post', - '$message' => L10n::t('Do you really want to delete this video?'), - '$extra_inputs' => [ - ['name' => 'id' , 'value' => $_POST['id']], - ['name' => 'delete', 'value' => 'x'] - ], - '$confirm' => L10n::t('Delete Video'), - '$confirm_url' => $drop_url, - '$confirm_name' => 'confirm', // Needed so that confirmation will bring us back into this if statement - '$cancel' => L10n::t('Cancel'), - - ]); - - $a->error = 1; // Set $a->error so the other module functions don't execute - - return; - } - $video_id = $_POST['id']; if (Attach::exists(['id' => $video_id, 'uid' => local_user()])) { @@ -246,7 +217,7 @@ function videos_content(App $a) // tabs $_is_owner = (local_user() && (local_user() == $owner_uid)); - $o .= Profile::getTabs($a, $_is_owner, $a->data['user']['nickname']); + $o .= Profile::getTabs($a, 'videos', $_is_owner, $a->data['user']['nickname']); // // dispatch request diff --git a/mod/view.php b/mod/view.php deleted file mode 100644 index f4f1692cb..000000000 --- a/mod/view.php +++ /dev/null @@ -1,23 +0,0 @@ -argc == 4){ - $theme = $a->argv[2]; - // set the path for later use in the theme styles - $THEMEPATH = "view/theme/$theme"; - if(file_exists("view/theme/$theme/style.php")) - require_once("view/theme/$theme/style.php"); - } - - exit(); -} diff --git a/mod/viewcontacts.php b/mod/viewcontacts.php deleted file mode 100644 index 7c2b96fad..000000000 --- a/mod/viewcontacts.php +++ /dev/null @@ -1,120 +0,0 @@ - L10n::t('Access denied.')]); - } - - if ($a->argc < 2) { - System::httpExit(403, ["title" => L10n::t('Access denied.')]); - } - - Nav::setSelected('home'); - - $user = DBA::selectFirst('user', [], ['nickname' => $a->argv[1], 'blocked' => false]); - if (!DBA::isResult($user)) { - System::httpExit(404, ["title" => L10n::t('Page not found.')]); - } - - $a->data['user'] = $user; - $a->profile_uid = $user['uid']; - - Profile::load($a, $a->argv[1]); -} - -function viewcontacts_content(App $a) -{ - if (Config::get('system', 'block_public') && !local_user() && !remote_user()) { - notice(L10n::t('Public access denied.') . EOL); - return; - } - - $is_owner = $a->profile['profile_uid'] == local_user(); - - // tabs - $o = Profile::getTabs($a, $is_owner, $a->data['user']['nickname']); - - if (!count($a->profile) || $a->profile['hide-friends']) { - notice(L10n::t('Permission denied.') . EOL); - return $o; - } - - $condition = [ - 'uid' => $a->profile['uid'], - 'blocked' => false, - 'pending' => false, - 'hidden' => false, - 'archive' => false, - 'network' => [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS] - ]; - - $total = DBA::count('contact', $condition); - - $pager = new Pager($a->query_string); - - $params = ['order' => ['name' => false], 'limit' => [$pager->getStart(), $pager->getItemsPerPage()]]; - - $contacts_stmt = DBA::select('contact', [], $condition, $params); - - if (!DBA::isResult($contacts_stmt)) { - info(L10n::t('No contacts.') . EOL); - return $o; - } - - $contacts = []; - - while ($contact = DBA::fetch($contacts_stmt)) { - /// @TODO This triggers an E_NOTICE if 'self' is not there - if ($contact['self']) { - continue; - } - - $contact_details = Contact::getDetailsByURL($contact['url'], $a->profile['uid'], $contact); - - $contacts[] = [ - 'id' => $contact['id'], - 'img_hover' => L10n::t('Visit %s\'s profile [%s]', $contact_details['name'], $contact['url']), - 'photo_menu' => Contact::photoMenu($contact), - 'thumb' => ProxyUtils::proxifyUrl($contact_details['thumb'], false, ProxyUtils::SIZE_THUMB), - 'name' => substr($contact_details['name'], 0, 20), - 'username' => $contact_details['name'], - 'details' => $contact_details['location'], - 'tags' => $contact_details['keywords'], - 'about' => $contact_details['about'], - 'account_type' => Contact::getAccountType($contact_details), - 'url' => Contact::magicLink($contact['url']), - 'sparkle' => '', - 'itemurl' => (($contact_details['addr'] != "") ? $contact_details['addr'] : $contact['url']), - 'network' => ContactSelector::networkToName($contact['network'], $contact['url']), - ]; - } - - DBA::close($contacts_stmt); - - $tpl = Renderer::getMarkupTemplate("viewcontact_template.tpl"); - $o .= Renderer::replaceMacros($tpl, [ - '$title' => L10n::t('Contacts'), - '$contacts' => $contacts, - '$paginate' => $pager->renderFull($total), - ]); - - return $o; -} diff --git a/mod/viewsrc.php b/mod/viewsrc.php deleted file mode 100644 index f05996d2f..000000000 --- a/mod/viewsrc.php +++ /dev/null @@ -1,37 +0,0 @@ -argc > 1) ? intval($a->argv[1]) : 0); - - if (!$item_id) { - $a->error = 404; - notice(L10n::t('Item not found.') . EOL); - return; - } - - $item = Item::selectFirst(['body'], ['uid' => local_user(), 'id' => $item_id]); - - if (DBA::isResult($item)) { - if ($a->isAjax()) { - echo str_replace("\n", '
    ', $item['body']); - exit(); - } else { - $o .= str_replace("\n", '
    ', $item['body']); - } - } - return $o; -} diff --git a/mod/webfinger.php b/mod/webfinger.php deleted file mode 100644 index b22b1ee64..000000000 --- a/mod/webfinger.php +++ /dev/null @@ -1,42 +0,0 @@ - L10n::t("Public access denied."), - "description" => L10n::t("Only logged in users are permitted to perform a probing.") - ] - ); - exit(); - } - - $o = '
    '; - $o .= '

    Webfinger Diagnostic

    '; - - $o .= ''; - $o .= 'Lookup address: '; - $o .= ''; - - $o .= '

    '; - - if (!empty($_GET['addr'])) { - $addr = trim($_GET['addr']); - $res = Probe::lrdd($addr); - $o .= '
    ';
    -		$o .= str_replace("\n", '
    ', print_r($res, true)); - $o .= '
    '; - } - $o .= '
    '; - - return $o; -} diff --git a/mod/xrd.php b/mod/xrd.php deleted file mode 100644 index b4cb60afe..000000000 --- a/mod/xrd.php +++ /dev/null @@ -1,134 +0,0 @@ -argv[0] == 'xrd') { - if (empty($_GET['uri'])) { - System::httpExit(404); - } - - $uri = urldecode(Strings::escapeTags(trim($_GET['uri']))); - if (defaults($_SERVER, 'HTTP_ACCEPT', '') == 'application/jrd+json') { - $mode = 'json'; - } else { - $mode = 'xml'; - } - } else { - if (empty($_GET['resource'])) { - System::httpExit(404); - } - - $uri = urldecode(Strings::escapeTags(trim($_GET['resource']))); - if (defaults($_SERVER, 'HTTP_ACCEPT', '') == 'application/xrd+xml') { - $mode = 'xml'; - } else { - $mode = 'json'; - } - } - - if (substr($uri, 0, 4) === 'http') { - $name = ltrim(basename($uri), '~'); - } else { - $local = str_replace('acct:', '', $uri); - if (substr($local, 0, 2) == '//') { - $local = substr($local, 2); - } - - $name = substr($local, 0, strpos($local, '@')); - } - - $user = DBA::selectFirst('user', [], ['nickname' => $name]); - if (!DBA::isResult($user)) { - System::httpExit(404); - } - - $profile_url = System::baseUrl().'/profile/'.$user['nickname']; - - $alias = str_replace('/profile/', '/~', $profile_url); - - $addr = 'acct:'.$user['nickname'].'@'.$a->getHostName(); - if ($a->getURLPath()) { - $addr .= '/'.$a->getURLPath(); - } - - if ($mode == 'xml') { - xrd_xml($addr, $alias, $profile_url, $user); - } else { - xrd_json($addr, $alias, $profile_url, $user); - } -} - -function xrd_json($uri, $alias, $profile_url, $r) -{ - $salmon_key = Salmon::salmonKey($r['spubkey']); - - header('Access-Control-Allow-Origin: *'); - header("Content-type: application/json; charset=utf-8"); - - $json = ['subject' => $uri, - 'aliases' => [$alias, $profile_url], - 'links' => [ - ['rel' => NAMESPACE_DFRN, 'href' => $profile_url], - ['rel' => NAMESPACE_FEED, 'type' => 'application/atom+xml', 'href' => System::baseUrl().'/dfrn_poll/'.$r['nickname']], - ['rel' => 'http://webfinger.net/rel/profile-page', 'type' => 'text/html', 'href' => $profile_url], - ['rel' => 'self', 'type' => 'application/activity+json', 'href' => $profile_url], - ['rel' => 'http://microformats.org/profile/hcard', 'type' => 'text/html', 'href' => System::baseUrl().'/hcard/'.$r['nickname']], - ['rel' => NAMESPACE_POCO, 'href' => System::baseUrl().'/poco/'.$r['nickname']], - ['rel' => 'http://webfinger.net/rel/avatar', 'type' => 'image/jpeg', 'href' => System::baseUrl().'/photo/profile/'.$r['uid'].'.jpg'], - ['rel' => 'http://joindiaspora.com/seed_location', 'type' => 'text/html', 'href' => System::baseUrl()], - ['rel' => 'salmon', 'href' => System::baseUrl().'/salmon/'.$r['nickname']], - ['rel' => 'http://salmon-protocol.org/ns/salmon-replies', 'href' => System::baseUrl().'/salmon/'.$r['nickname']], - ['rel' => 'http://salmon-protocol.org/ns/salmon-mention', 'href' => System::baseUrl().'/salmon/'.$r['nickname'].'/mention'], - ['rel' => 'http://ostatus.org/schema/1.0/subscribe', 'template' => System::baseUrl().'/follow?url={uri}'], - ['rel' => 'magic-public-key', 'href' => 'data:application/magic-public-key,'.$salmon_key], - ['rel' => 'http://purl.org/openwebauth/v1', 'type' => 'application/x-zot+json', 'href' => System::baseUrl().'/owa'] - ] - ]; - - echo json_encode($json); - exit(); -} - -function xrd_xml($uri, $alias, $profile_url, $r) -{ - $salmon_key = Salmon::salmonKey($r['spubkey']); - - header('Access-Control-Allow-Origin: *'); - header("Content-type: text/xml"); - - $tpl = Renderer::getMarkupTemplate('xrd_person.tpl'); - - $o = Renderer::replaceMacros($tpl, [ - '$nick' => $r['nickname'], - '$accturi' => $uri, - '$alias' => $alias, - '$profile_url' => $profile_url, - '$hcard_url' => System::baseUrl() . '/hcard/' . $r['nickname'], - '$atom' => System::baseUrl() . '/dfrn_poll/' . $r['nickname'], - '$poco_url' => System::baseUrl() . '/poco/' . $r['nickname'], - '$photo' => System::baseUrl() . '/photo/profile/' . $r['uid'] . '.jpg', - '$baseurl' => System::baseUrl(), - '$salmon' => System::baseUrl() . '/salmon/' . $r['nickname'], - '$salmen' => System::baseUrl() . '/salmon/' . $r['nickname'] . '/mention', - '$subscribe' => System::baseUrl() . '/follow?url={uri}', - '$openwebauth' => System::baseUrl() . '/owa', - '$modexp' => 'data:application/magic-public-key,' . $salmon_key] - ); - - $arr = ['user' => $r, 'xml' => $o]; - Hook::callAll('personal_xrd', $arr); - - echo $arr['xml']; - exit(); -} diff --git a/mods/local.config.vagrant.php b/mods/local.config.vagrant.php index a9b95d02d..c35b1a33d 100644 --- a/mods/local.config.vagrant.php +++ b/mods/local.config.vagrant.php @@ -1,41 +1,42 @@ - [ - 'hostname' => 'localhost', - 'username' => 'friendica', - 'password' => 'friendica', - 'database' => 'friendica', - 'charset' => 'utf8mb4', - ], - - // **************************************************************** - // The configuration below will be overruled by the admin panel. - // Changes made below will only have an effect if the database does - // not contain any configuration for the friendica system. - // **************************************************************** - - 'config' => [ - 'admin_email' => 'admin@friendica.local', - 'sitename' => 'Friendica Social Network', - 'register_policy' => \Friendica\Module\Register::OPEN, - 'register_text' => '', - ], - 'system' => [ - 'default_timezone' => 'UTC', - 'language' => 'en', - ], -]; + [ + 'hostname' => 'localhost', + 'username' => 'friendica', + 'password' => 'friendica', + 'database' => 'friendica', + 'charset' => 'utf8mb4', + ], + + // **************************************************************** + // The configuration below will be overruled by the admin panel. + // Changes made below will only have an effect if the database does + // not contain any configuration for the friendica system. + // **************************************************************** + + 'config' => [ + 'admin_email' => 'admin@friendica.local', + 'sitename' => 'Friendica Social Network', + 'register_policy' => \Friendica\Module\Register::OPEN, + 'register_text' => '', + ], + 'system' => [ + 'default_timezone' => 'UTC', + 'language' => 'en', + 'basepath' => '/vagrant', + ], +]; diff --git a/src/App.php b/src/App.php index b5ad68321..89d3009da 100644 --- a/src/App.php +++ b/src/App.php @@ -8,14 +8,18 @@ use Detection\MobileDetect; use DOMDocument; use DOMXPath; use Exception; -use Friendica\Core\Config\Cache\ConfigCacheLoader; use Friendica\Core\Config\Cache\IConfigCache; use Friendica\Core\Config\Configuration; +use Friendica\Core\Hook; +use Friendica\Core\Theme; use Friendica\Database\DBA; use Friendica\Model\Profile; -use Friendica\Network\HTTPException\InternalServerErrorException; +use Friendica\Network\HTTPException; +use Friendica\Util\BaseURL; +use Friendica\Util\Config\ConfigFileLoader; use Friendica\Util\HTTPSignature; use Friendica\Util\Profiler; +use Friendica\Util\Strings; use Psr\Log\LoggerInterface; /** @@ -33,7 +37,6 @@ use Psr\Log\LoggerInterface; */ class App { - public $module_loaded = false; public $module_class = null; public $query_string = ''; public $page = []; @@ -46,7 +49,6 @@ class App public $page_contact; public $content; public $data = []; - public $error = false; public $cmd = ''; public $argv; public $argc; @@ -76,19 +78,14 @@ class App private $mode; /** - * @var string The App base path + * @var App\Router */ - private $basePath; + private $router; /** - * @var string The App URL path + * @var BaseURL */ - private $urlPath; - - /** - * @var bool true, if the call is from the Friendica APP, otherwise false - */ - private $isFriendicaApp; + private $baseURL; /** * @var bool true, if the call is from an backend node (f.e. worker) @@ -135,6 +132,16 @@ class App return $this->config->getCache(); } + /** + * Returns the current config of this node + * + * @return Configuration + */ + public function getConfig() + { + return $this->config; + } + /** * The basepath of this app * @@ -142,7 +149,8 @@ class App */ public function getBasePath() { - return $this->basePath; + // Don't use the basepath of the config table for basepath (it should always be the config-file one) + return $this->config->getCache()->get('system', 'basepath'); } /** @@ -165,6 +173,26 @@ class App return $this->profiler; } + /** + * Returns the Mode of the Application + * + * @return App\Mode The Application Mode + */ + public function getMode() + { + return $this->mode; + } + + /** + * Returns the router of the Application + * + * @return App\Router + */ + public function getRouter() + { + return $this->router; + } + /** * Register a stylesheet file path to be included in the tag of every page. * Inclusion is done in App->initHead(). @@ -173,13 +201,14 @@ class App * @see initHead() * * @param string $path - * @throws InternalServerErrorException */ public function registerStylesheet($path) { - $url = str_replace($this->basePath . DIRECTORY_SEPARATOR, '', $path); + if (mb_strpos($path, $this->getBasePath() . DIRECTORY_SEPARATOR) === 0) { + $path = mb_substr($path, mb_strlen($this->getBasePath() . DIRECTORY_SEPARATOR)); + } - $this->stylesheets[] = trim($url, '/'); + $this->stylesheets[] = trim($path, '/'); } /** @@ -190,52 +219,42 @@ class App * @see initFooter() * * @param string $path - * @throws InternalServerErrorException */ public function registerFooterScript($path) { - $url = str_replace($this->basePath . DIRECTORY_SEPARATOR, '', $path); + $url = str_replace($this->getBasePath() . DIRECTORY_SEPARATOR, '', $path); $this->footerScripts[] = trim($url, '/'); } public $queue; - private $scheme; - private $hostname; /** * @brief App constructor. * - * @param string $basePath The basedir of the app * @param Configuration $config The Configuration + * @param App\Mode $mode The mode of this Friendica app + * @param App\Router $router The router of this Friendica app + * @param BaseURL $baseURL The full base URL of this Friendica app * @param LoggerInterface $logger The current app logger * @param Profiler $profiler The profiler of this application * @param bool $isBackend Whether it is used for backend or frontend (Default true=backend) * * @throws Exception if the Basepath is not usable */ - public function __construct($basePath, Configuration $config, LoggerInterface $logger, Profiler $profiler, $isBackend = true) + public function __construct(Configuration $config, App\Mode $mode, App\Router $router, BaseURL $baseURL, LoggerInterface $logger, Profiler $profiler, $isBackend = true) { BaseObject::setApp($this); - $this->logger = $logger; $this->config = $config; + $this->mode = $mode; + $this->router = $router; + $this->baseURL = $baseURL; $this->profiler = $profiler; - $cfgBasePath = $this->config->get('system', 'basepath'); - $this->basePath = !empty($cfgBasePath) ? $cfgBasePath : $basePath; - - if (!Core\System::isDirectoryUsable($this->basePath, false)) { - throw new Exception('Basepath \'' . $this->basePath . '\' isn\'t usable.'); - } - $this->basePath = rtrim($this->basePath, DIRECTORY_SEPARATOR); - - $this->checkBackend($isBackend); - $this->checkFriendicaApp(); + $this->logger = $logger; $this->profiler->reset(); - $this->mode = new App\Mode($this->basePath); - $this->reload(); set_time_limit(0); @@ -243,31 +262,11 @@ class App // This has to be quite large to deal with embedded private photos ini_set('pcre.backtrack_limit', 500000); - $this->scheme = 'http'; - - if (!empty($_SERVER['HTTPS']) || - !empty($_SERVER['HTTP_FORWARDED']) && preg_match('/proto=https/', $_SERVER['HTTP_FORWARDED']) || - !empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https' || - !empty($_SERVER['HTTP_X_FORWARDED_SSL']) && $_SERVER['HTTP_X_FORWARDED_SSL'] == 'on' || - !empty($_SERVER['FRONT_END_HTTPS']) && $_SERVER['FRONT_END_HTTPS'] == 'on' || - !empty($_SERVER['SERVER_PORT']) && (intval($_SERVER['SERVER_PORT']) == 443) // XXX: reasonable assumption, but isn't this hardcoding too much? - ) { - $this->scheme = 'https'; - } - - if (!empty($_SERVER['SERVER_NAME'])) { - $this->hostname = $_SERVER['SERVER_NAME']; - - if (!empty($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] != 80 && $_SERVER['SERVER_PORT'] != 443) { - $this->hostname .= ':' . $_SERVER['SERVER_PORT']; - } - } - set_include_path( get_include_path() . PATH_SEPARATOR - . $this->basePath . DIRECTORY_SEPARATOR . 'include' . PATH_SEPARATOR - . $this->basePath . DIRECTORY_SEPARATOR . 'library' . PATH_SEPARATOR - . $this->basePath); + . $this->getBasePath() . DIRECTORY_SEPARATOR . 'include' . PATH_SEPARATOR + . $this->getBasePath() . DIRECTORY_SEPARATOR . 'library' . PATH_SEPARATOR + . $this->getBasePath()); if (!empty($_SERVER['QUERY_STRING']) && strpos($_SERVER['QUERY_STRING'], 'pagename=') === 0) { $this->query_string = substr($_SERVER['QUERY_STRING'], 9); @@ -321,6 +320,8 @@ class App $this->module = 'home'; } + $this->isBackend = $isBackend || $this->checkBackend($this->module); + // Detect mobile devices $mobile_detect = new MobileDetect(); @@ -335,33 +336,15 @@ class App Core\Renderer::registerTemplateEngine('Friendica\Render\FriendicaSmartyEngine'); } - /** - * Returns the Mode of the Application - * - * @return App\Mode The Application Mode - * - * @throws InternalServerErrorException when the mode isn't created - */ - public function getMode() - { - if (empty($this->mode)) { - throw new InternalServerErrorException('Mode of the Application is not defined'); - } - - return $this->mode; - } - /** * Reloads the whole app instance */ public function reload() { - $this->determineURLPath(); - - $this->getMode()->determine($this->basePath); + $this->getMode()->determine($this->getBasePath()); if ($this->getMode()->has(App\Mode::DBAVAILABLE)) { - $loader = new ConfigCacheLoader($this->basePath); + $loader = new ConfigFileLoader($this->getBasePath(), $this->getMode()); $this->config->getCache()->load($loader->loadCoreConfig('addon'), true); $this->profiler->update( @@ -369,6 +352,7 @@ class App $this->config->get('rendertime', 'callstack', false)); Core\Hook::loadHooks(); + $loader = new ConfigFileLoader($this->getBasePath(), $this->mode); Core\Hook::callAll('load_config', $loader); } @@ -399,97 +383,26 @@ class App } /** - * Figure out if we are running at the top of a domain or in a sub-directory and adjust accordingly + * Returns the scheme of the current call + * @return string + * + * @deprecated 2019.06 - use BaseURL->getScheme() instead */ - private function determineURLPath() - { - /* - * The automatic path detection in this function is currently deactivated, - * see issue https://github.com/friendica/friendica/issues/6679 - * - * The problem is that the function seems to be confused with some url. - * These then confuses the detection which changes the url path. - */ - - /* Relative script path to the web server root - * Not all of those $_SERVER properties can be present, so we do by inverse priority order - */ -/* - $relative_script_path = ''; - $relative_script_path = defaults($_SERVER, 'REDIRECT_URL' , $relative_script_path); - $relative_script_path = defaults($_SERVER, 'REDIRECT_URI' , $relative_script_path); - $relative_script_path = defaults($_SERVER, 'REDIRECT_SCRIPT_URL', $relative_script_path); - $relative_script_path = defaults($_SERVER, 'SCRIPT_URL' , $relative_script_path); - $relative_script_path = defaults($_SERVER, 'REQUEST_URI' , $relative_script_path); -*/ - $this->urlPath = $this->config->get('system', 'urlpath'); - - /* $relative_script_path gives /relative/path/to/friendica/module/parameter - * QUERY_STRING gives pagename=module/parameter - * - * To get /relative/path/to/friendica we perform dirname() for as many levels as there are slashes in the QUERY_STRING - */ -/* - if (!empty($relative_script_path)) { - // Module - if (!empty($_SERVER['QUERY_STRING'])) { - $path = trim(rdirname($relative_script_path, substr_count(trim($_SERVER['QUERY_STRING'], '/'), '/') + 1), '/'); - } else { - // Root page - $path = trim($relative_script_path, '/'); - } - - if ($path && $path != $this->urlPath) { - $this->urlPath = $path; - } - } -*/ - } - public function getScheme() { - return $this->scheme; + return $this->baseURL->getScheme(); } /** - * @brief Retrieves the Friendica instance base URL + * Retrieves the Friendica instance base URL * - * This function assembles the base URL from multiple parts: - * - Protocol is determined either by the request or a combination of - * system.ssl_policy and the $ssl parameter. - * - Host name is determined either by system.hostname or inferred from request - * - Path is inferred from SCRIPT_NAME + * @param bool $ssl Whether to append http or https under BaseURL::SSL_POLICY_SELFSIGN * - * Note: $ssl parameter value doesn't directly correlate with the resulting protocol - * - * @param bool $ssl Whether to append http or https under SSL_POLICY_SELFSIGN * @return string Friendica server base URL - * @throws InternalServerErrorException */ public function getBaseURL($ssl = false) { - $scheme = $this->scheme; - - if (Core\Config::get('system', 'ssl_policy') == SSL_POLICY_FULL) { - $scheme = 'https'; - } - - // Basically, we have $ssl = true on any links which can only be seen by a logged in user - // (and also the login link). Anything seen by an outsider will have it turned off. - - if (Core\Config::get('system', 'ssl_policy') == SSL_POLICY_SELFSIGN) { - if ($ssl) { - $scheme = 'https'; - } else { - $scheme = 'http'; - } - } - - if (Core\Config::get('config', 'hostname') != '') { - $this->hostname = Core\Config::get('config', 'hostname'); - } - - return $scheme . '://' . $this->hostname . (!empty($this->getURLPath()) ? '/' . $this->getURLPath() : '' ); + return $this->baseURL->get($ssl); } /** @@ -498,55 +411,36 @@ class App * Clears the baseurl cache to prevent inconsistencies * * @param string $url - * @throws InternalServerErrorException + * + * @deprecated 2019.06 - use BaseURL->saveByURL($url) instead */ public function setBaseURL($url) { - $parsed = @parse_url($url); - $hostname = ''; - - if (!empty($parsed)) { - if (!empty($parsed['scheme'])) { - $this->scheme = $parsed['scheme']; - } - - if (!empty($parsed['host'])) { - $hostname = $parsed['host']; - } - - if (!empty($parsed['port'])) { - $hostname .= ':' . $parsed['port']; - } - if (!empty($parsed['path'])) { - $this->urlPath = trim($parsed['path'], '\\/'); - } - - if (file_exists($this->basePath . '/.htpreconfig.php')) { - include $this->basePath . '/.htpreconfig.php'; - } - - if (Core\Config::get('config', 'hostname') != '') { - $this->hostname = Core\Config::get('config', 'hostname'); - } - - if (!isset($this->hostname) || ($this->hostname == '')) { - $this->hostname = $hostname; - } - } + $this->baseURL->saveByURL($url); } + /** + * Returns the current hostname + * + * @return string + * + * @deprecated 2019.06 - use BaseURL->getHostname() instead + */ public function getHostName() { - if (Core\Config::get('config', 'hostname') != '') { - $this->hostname = Core\Config::get('config', 'hostname'); - } - - return $this->hostname; + return $this->baseURL->getHostname(); } + /** + * Returns the sub-path of the full URL + * + * @return string + * + * @deprecated 2019.06 - use BaseURL->getUrlPath() instead + */ public function getURLPath() { - return $this->urlPath; + return $this->baseURL->getUrlPath(); } /** @@ -588,12 +482,12 @@ class App $this->registerStylesheet($stylesheet); - $shortcut_icon = Core\Config::get('system', 'shortcut_icon'); + $shortcut_icon = $this->config->get('system', 'shortcut_icon'); if ($shortcut_icon == '') { $shortcut_icon = 'images/friendica-32.png'; } - $touch_icon = Core\Config::get('system', 'touch_icon'); + $touch_icon = $this->config->get('system', 'touch_icon'); if ($touch_icon == '') { $touch_icon = 'images/friendica-128.png'; } @@ -606,14 +500,13 @@ class App * being first */ $this->page['htmlhead'] = Core\Renderer::replaceMacros($tpl, [ - '$baseurl' => $this->getBaseURL(), '$local_user' => local_user(), '$generator' => 'Friendica' . ' ' . FRIENDICA_VERSION, '$delitem' => Core\L10n::t('Delete this item?'), '$update_interval' => $interval, '$shortcut_icon' => $shortcut_icon, '$touch_icon' => $touch_icon, - '$block_public' => intval(Core\Config::get('system', 'block_public')), + '$block_public' => intval($this->config->get('system', 'block_public')), '$stylesheets' => $this->stylesheets, ]) . $this->page['htmlhead']; } @@ -659,7 +552,6 @@ class App $tpl = Core\Renderer::getMarkupTemplate('footer.tpl'); $this->page['footer'] = Core\Renderer::replaceMacros($tpl, [ - '$baseurl' => $this->getBaseURL(), '$footerScripts' => $this->footerScripts, ]) . $this->page['footer']; } @@ -670,7 +562,7 @@ class App * @param string $origURL * * @return string The cleaned url - * @throws InternalServerErrorException + * @throws HTTPException\InternalServerErrorException */ public function removeBaseURL($origURL) { @@ -691,7 +583,7 @@ class App * Returns the current UserAgent as a String * * @return string the UserAgent as a String - * @throws InternalServerErrorException + * @throws HTTPException\InternalServerErrorException */ public function getUserAgent() { @@ -703,48 +595,32 @@ class App $this->getBaseURL(); } - /** - * Checks, if the call is from the Friendica App - * - * Reason: - * The friendica client has problems with the GUID in the notify. this is some workaround - */ - private function checkFriendicaApp() - { - // Friendica-Client - $this->isFriendicaApp = isset($_SERVER['HTTP_USER_AGENT']) && $_SERVER['HTTP_USER_AGENT'] == 'Apache-HttpClient/UNAVAILABLE (java 1.4)'; - } - - /** - * Is the call via the Friendica app? (not a "normale" call) - * - * @return bool true if it's from the Friendica app - */ - public function isFriendicaApp() - { - return $this->isFriendicaApp; - } - /** * @brief Checks if the site is called via a backend process * * This isn't a perfect solution. But we need this check very early. * So we cannot wait until the modules are loaded. * - * @param string $backend true, if the backend flag was set during App initialization - * + * @param string $module + * @return bool */ - private function checkBackend($backend) { + private function checkBackend($module) { static $backends = [ '_well_known', 'api', 'dfrn_notify', + 'feed', 'fetch', + 'followers', + 'following', 'hcard', 'hostxrd', + 'inbox', + 'manifest', 'nodeinfo', 'noscrape', - 'p', + 'objects', + 'outbox', 'poco', 'post', 'proxy', @@ -758,7 +634,7 @@ class App ]; // Check if current module is in backend or backend flag is set - $this->isBackend = (in_array($this->module, $backends) || $backend || $this->isBackend); + return in_array($module, $backends); } /** @@ -786,13 +662,13 @@ class App * if ($this->is_backend()) { $process = 'backend'; - $max_processes = Core\Config::get('system', 'max_processes_backend'); + $max_processes = $this->config->get('system', 'max_processes_backend'); if (intval($max_processes) == 0) { $max_processes = 5; } } else { $process = 'frontend'; - $max_processes = Core\Config::get('system', 'max_processes_frontend'); + $max_processes = $this->config->get('system', 'max_processes_frontend'); if (intval($max_processes) == 0) { $max_processes = 20; } @@ -815,11 +691,11 @@ class App * @brief Checks if the minimal memory is reached * * @return bool Is the memory limit reached? - * @throws InternalServerErrorException + * @throws HTTPException\InternalServerErrorException */ public function isMinMemoryReached() { - $min_memory = Core\Config::get('system', 'min_memory', 0); + $min_memory = $this->config->get('system', 'min_memory', 0); if ($min_memory == 0) { return false; } @@ -860,19 +736,19 @@ class App * @brief Checks if the maximum load is reached * * @return bool Is the load reached? - * @throws InternalServerErrorException + * @throws HTTPException\InternalServerErrorException */ public function isMaxLoadReached() { if ($this->isBackend()) { $process = 'backend'; - $maxsysload = intval(Core\Config::get('system', 'maxloadavg')); + $maxsysload = intval($this->config->get('system', 'maxloadavg')); if ($maxsysload < 1) { $maxsysload = 50; } } else { $process = 'frontend'; - $maxsysload = intval(Core\Config::get('system', 'maxloadavg_frontend')); + $maxsysload = intval($this->config->get('system', 'maxloadavg_frontend')); if ($maxsysload < 1) { $maxsysload = 50; } @@ -893,7 +769,7 @@ class App * * @param string $command The command to execute * @param array $args Arguments to pass to the command ( [ 'key' => value, 'key2' => value2, ... ] - * @throws InternalServerErrorException + * @throws HTTPException\InternalServerErrorException */ public function proc_run($command, $args) { @@ -919,9 +795,9 @@ class App } if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { - $resource = proc_open('cmd /c start /b ' . $cmdline, [], $foo, $this->basePath); + $resource = proc_open('cmd /c start /b ' . $cmdline, [], $foo, $this->getBasePath()); } else { - $resource = proc_open($cmdline . ' &', [], $foo, $this->basePath); + $resource = proc_open($cmdline . ' &', [], $foo, $this->getBasePath()); } if (!is_resource($resource)) { Core\Logger::log('We got no resource for command ' . $cmdline, Core\Logger::DEBUG); @@ -934,13 +810,13 @@ class App * Generates the site's default sender email address * * @return string - * @throws InternalServerErrorException + * @throws HTTPException\InternalServerErrorException */ public function getSenderEmailAddress() { - $sender_email = Core\Config::get('config', 'sender_email'); + $sender_email = $this->config->get('config', 'sender_email'); if (empty($sender_email)) { - $hostname = $this->getHostName(); + $hostname = $this->baseURL->getHostname(); if (strpos($hostname, ':')) { $hostname = substr($hostname, 0, strpos($hostname, ':')); } @@ -955,7 +831,7 @@ class App * Returns the current theme name. * * @return string the name of the current theme - * @throws InternalServerErrorException + * @throws HTTPException\InternalServerErrorException */ public function getCurrentTheme() { @@ -982,7 +858,7 @@ class App */ private function computeCurrentTheme() { - $system_theme = Core\Config::get('system', 'theme'); + $system_theme = $this->config->get('system', 'theme'); if (!$system_theme) { throw new Exception(Core\L10n::t('No system theme config value set.')); } @@ -990,8 +866,6 @@ class App // Sane default $this->currentTheme = $system_theme; - $allowed_themes = explode(',', Core\Config::get('system', 'allowed_themes', $system_theme)); - $page_theme = null; // Find the theme that belongs to the user whose stuff we are looking at if ($this->profile_uid && ($this->profile_uid != local_user())) { @@ -1007,7 +881,7 @@ class App // Specific mobile theme override if (($this->is_mobile || $this->is_tablet) && Core\Session::get('show-mobile', true)) { - $system_mobile_theme = Core\Config::get('system', 'mobile-theme'); + $system_mobile_theme = $this->config->get('system', 'mobile-theme'); $user_mobile_theme = Core\Session::get('mobile-theme', $system_mobile_theme); // --- means same mobile theme as desktop @@ -1022,8 +896,9 @@ class App $theme_name = $user_theme; } + $theme_name = Strings::sanitizeFilePathItem($theme_name); if ($theme_name - && in_array($theme_name, $allowed_themes) + && in_array($theme_name, Theme::getAllowedList()) && (file_exists('view/theme/' . $theme_name . '/style.css') || file_exists('view/theme/' . $theme_name . '/style.php')) ) { @@ -1037,7 +912,7 @@ class App * Provide a sane default if nothing is chosen or the specified theme does not exist. * * @return string - * @throws InternalServerErrorException + * @throws HTTPException\InternalServerErrorException */ public function getCurrentThemeStylesheetPath() { @@ -1078,7 +953,7 @@ class App */ public function checkURL() { - $url = Core\Config::get('system', 'url'); + $url = $this->config->get('system', 'url'); // if the url isn't set or the stored url is radically different // than the currently visited url, store the current value accordingly. @@ -1086,8 +961,8 @@ class App // and www.example.com vs example.com. // We will only change the url to an ip address if there is no existing setting - if (empty($url) || (!Util\Strings::compareLink($url, $this->getBaseURL())) && (!preg_match("/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/", $this->getHostName()))) { - Core\Config::set('system', 'url', $this->getBaseURL()); + if (empty($url) || (!Util\Strings::compareLink($url, $this->getBaseURL())) && (!preg_match("/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/", $this->baseURL->getHostname()))) { + $this->config->set('system', 'url', $this->getBaseURL()); } } @@ -1103,7 +978,9 @@ class App { // Missing DB connection: ERROR if ($this->getMode()->has(App\Mode::LOCALCONFIGPRESENT) && !$this->getMode()->has(App\Mode::DBAVAILABLE)) { - Core\System::httpExit(500, ['title' => 'Error 500 - Internal Server Error', 'description' => 'Apologies but the website is unavailable at the moment.']); + Module\Special\HTTPException::rawContent( + new HTTPException\InternalServerErrorException('Apologies but the website is unavailable at the moment.') + ); } // Max Load Average reached: ERROR @@ -1111,19 +988,14 @@ class App header('Retry-After: 120'); header('Refresh: 120; url=' . $this->getBaseURL() . "/" . $this->query_string); - Core\System::httpExit(503, ['title' => 'Error 503 - Service Temporarily Unavailable', 'description' => 'Core\System is currently overloaded. Please try again later.']); - } - - if (strstr($this->query_string, '.well-known/host-meta') && ($this->query_string != '.well-known/host-meta')) { - Core\System::httpExit(404); + Module\Special\HTTPException::rawContent( + new HTTPException\ServiceUnavailableException('The node is currently overloaded. Please try again later.') + ); } if (!$this->getMode()->isInstall()) { // Force SSL redirection - if (Core\Config::get('system', 'force_ssl') && ($this->getScheme() == "http") - && intval(Core\Config::get('system', 'ssl_policy')) == SSL_POLICY_FULL - && strpos($this->getBaseURL(), 'https://') === 0 - && $_SERVER['REQUEST_METHOD'] == 'GET') { + if ($this->baseURL->checkRedirectHttps()) { header('HTTP/1.1 302 Moved Temporarily'); header('Location: ' . $this->getBaseURL() . '/' . $this->query_string); exit(); @@ -1169,7 +1041,9 @@ class App // Someone came with an invalid parameter, maybe as a DDoS attempt // We simply stop processing here Core\Logger::log("Invalid ZRL parameter " . $_GET['zrl'], Core\Logger::DEBUG); - Core\System::httpExit(403, ['title' => '403 Forbidden']); + Module\Special\HTTPException::rawContent( + new HTTPException\ForbiddenException() + ); } } } @@ -1198,13 +1072,13 @@ class App // in install mode, any url loads install module // but we need "view" module for stylesheet - if ($this->getMode()->isInstall() && $this->module != 'view') { - $this->module = 'install'; - } elseif (!$this->getMode()->has(App\Mode::MAINTENANCEDISABLED) && $this->module != 'view') { - $this->module = 'maintenance'; + if ($this->getMode()->isInstall() && $this->module !== 'install') { + $this->internalRedirect('install'); + } elseif (!$this->getMode()->isInstall() && !$this->getMode()->has(App\Mode::MAINTENANCEDISABLED) && $this->module !== 'maintenance') { + $this->internalRedirect('maintenance'); } else { $this->checkURL(); - Core\Update::check($this->basePath, false); + Core\Update::check($this->getBasePath(), false, $this->getMode()); Core\Addon::loadAddons(); Core\Hook::loadHooks(); } @@ -1222,110 +1096,110 @@ class App 'title' => '' ]; - if (strlen($this->module)) { - // Compatibility with the Android Diaspora client - if ($this->module == 'stream') { - $this->internalRedirect('network?f=&order=post'); - } + // Compatibility with the Android Diaspora client + if ($this->module == 'stream') { + $this->internalRedirect('network?order=post'); + } - if ($this->module == 'conversations') { - $this->internalRedirect('message'); - } + if ($this->module == 'conversations') { + $this->internalRedirect('message'); + } - if ($this->module == 'commented') { - $this->internalRedirect('network?f=&order=comment'); - } + if ($this->module == 'commented') { + $this->internalRedirect('network?order=comment'); + } - if ($this->module == 'liked') { - $this->internalRedirect('network?f=&order=comment'); - } + if ($this->module == 'liked') { + $this->internalRedirect('network?order=comment'); + } - if ($this->module == 'activity') { - $this->internalRedirect('network/?f=&conv=1'); - } + if ($this->module == 'activity') { + $this->internalRedirect('network?conv=1'); + } - if (($this->module == 'status_messages') && ($this->cmd == 'status_messages/new')) { - $this->internalRedirect('bookmarklet'); - } + if (($this->module == 'status_messages') && ($this->cmd == 'status_messages/new')) { + $this->internalRedirect('bookmarklet'); + } - if (($this->module == 'user') && ($this->cmd == 'user/edit')) { - $this->internalRedirect('settings'); - } + if (($this->module == 'user') && ($this->cmd == 'user/edit')) { + $this->internalRedirect('settings'); + } - if (($this->module == 'tag_followings') && ($this->cmd == 'tag_followings/manage')) { - $this->internalRedirect('search'); - } + if (($this->module == 'tag_followings') && ($this->cmd == 'tag_followings/manage')) { + $this->internalRedirect('search'); + } - // Compatibility with the Firefox App - if (($this->module == "users") && ($this->cmd == "users/sign_in")) { - $this->module = "login"; - } + // Compatibility with the Firefox App + if (($this->module == "users") && ($this->cmd == "users/sign_in")) { + $this->module = "login"; + } - $privateapps = Core\Config::get('config', 'private_addons', false); - if (Core\Addon::isEnabled($this->module) && file_exists("addon/{$this->module}/{$this->module}.php")) { - //Check if module is an app and if public access to apps is allowed or not - if ((!local_user()) && Core\Hook::isAddonApp($this->module) && $privateapps) { - info(Core\L10n::t("You must be logged in to use addons. ")); - } else { - include_once "addon/{$this->module}/{$this->module}.php"; - if (function_exists($this->module . '_module')) { - LegacyModule::setModuleFile("addon/{$this->module}/{$this->module}.php"); - $this->module_class = 'Friendica\\LegacyModule'; - $this->module_loaded = true; - } + /* + * ROUTING + * + * From the request URL, routing consists of obtaining the name of a BaseModule-extending class of which the + * post() and/or content() static methods can be respectively called to produce a data change or an output. + */ + + // First we try explicit routes defined in App\Router + $this->router->collectRoutes(); + + $data = $this->router->getRouteCollector(); + Hook::callAll('route_collection', $data); + + $this->module_class = $this->router->getModuleClass($this->cmd); + + // Then we try addon-provided modules that we wrap in the LegacyModule class + if (!$this->module_class && Core\Addon::isEnabled($this->module) && file_exists("addon/{$this->module}/{$this->module}.php")) { + //Check if module is an app and if public access to apps is allowed or not + $privateapps = $this->config->get('config', 'private_addons', false); + if ((!local_user()) && Core\Hook::isAddonApp($this->module) && $privateapps) { + info(Core\L10n::t("You must be logged in to use addons. ")); + } else { + include_once "addon/{$this->module}/{$this->module}.php"; + if (function_exists($this->module . '_module')) { + LegacyModule::setModuleFile("addon/{$this->module}/{$this->module}.php"); + $this->module_class = LegacyModule::class; } } - - // Controller class routing - if (! $this->module_loaded && class_exists('Friendica\\Module\\' . ucfirst($this->module))) { - $this->module_class = 'Friendica\\Module\\' . ucfirst($this->module); - $this->module_loaded = true; - } - - /* If not, next look for a 'standard' program module in the 'mod' directory - * We emulate a Module class through the LegacyModule class - */ - if (! $this->module_loaded && file_exists("mod/{$this->module}.php")) { - LegacyModule::setModuleFile("mod/{$this->module}.php"); - $this->module_class = 'Friendica\\LegacyModule'; - $this->module_loaded = true; - } - - /* The URL provided does not resolve to a valid module. - * - * On Dreamhost sites, quite often things go wrong for no apparent reason and they send us to '/internal_error.html'. - * We don't like doing this, but as it occasionally accounts for 10-20% or more of all site traffic - - * we are going to trap this and redirect back to the requested page. As long as you don't have a critical error on your page - * this will often succeed and eventually do the right thing. - * - * Otherwise we are going to emit a 404 not found. - */ - if (! $this->module_loaded) { - // Stupid browser tried to pre-fetch our Javascript img template. Don't log the event or return anything - just quietly exit. - if (!empty($_SERVER['QUERY_STRING']) && preg_match('/{[0-9]}/', $_SERVER['QUERY_STRING']) !== 0) { - exit(); - } - - if (!empty($_SERVER['QUERY_STRING']) && ($_SERVER['QUERY_STRING'] === 'q=internal_error.html') && isset($dreamhost_error_hack)) { - Core\Logger::log('index.php: dreamhost_error_hack invoked. Original URI =' . $_SERVER['REQUEST_URI']); - $this->internalRedirect($_SERVER['REQUEST_URI']); - } - - Core\Logger::log('index.php: page not found: ' . $_SERVER['REQUEST_URI'] . ' ADDRESS: ' . $_SERVER['REMOTE_ADDR'] . ' QUERY: ' . $_SERVER['QUERY_STRING'], Core\Logger::DEBUG); - - header($_SERVER["SERVER_PROTOCOL"] . ' 404 ' . Core\L10n::t('Not Found')); - $tpl = Core\Renderer::getMarkupTemplate("404.tpl"); - $this->page['content'] = Core\Renderer::replaceMacros($tpl, [ - '$message' => Core\L10n::t('Page not found.') - ]); - } } - $content = ''; + /* Finally, we look for a 'standard' program module in the 'mod' directory + * We emulate a Module class through the LegacyModule class + */ + if (!$this->module_class && file_exists("mod/{$this->module}.php")) { + LegacyModule::setModuleFile("mod/{$this->module}.php"); + $this->module_class = LegacyModule::class; + } + + /* The URL provided does not resolve to a valid module. + * + * On Dreamhost sites, quite often things go wrong for no apparent reason and they send us to '/internal_error.html'. + * We don't like doing this, but as it occasionally accounts for 10-20% or more of all site traffic - + * we are going to trap this and redirect back to the requested page. As long as you don't have a critical error on your page + * this will often succeed and eventually do the right thing. + * + * Otherwise we are going to emit a 404 not found. + */ + if (!$this->module_class) { + // Stupid browser tried to pre-fetch our Javascript img template. Don't log the event or return anything - just quietly exit. + if (!empty($_SERVER['QUERY_STRING']) && preg_match('/{[0-9]}/', $_SERVER['QUERY_STRING']) !== 0) { + exit(); + } + + if (!empty($_SERVER['QUERY_STRING']) && ($_SERVER['QUERY_STRING'] === 'q=internal_error.html') && isset($dreamhost_error_hack)) { + Core\Logger::log('index.php: dreamhost_error_hack invoked. Original URI =' . $_SERVER['REQUEST_URI']); + $this->internalRedirect($_SERVER['REQUEST_URI']); + } + + Core\Logger::log('index.php: page not found: ' . $_SERVER['REQUEST_URI'] . ' ADDRESS: ' . $_SERVER['REMOTE_ADDR'] . ' QUERY: ' . $_SERVER['QUERY_STRING'], Core\Logger::DEBUG); + + $this->module_class = Module\PageNotFound::class; + } // Initialize module that can set the current theme in the init() method, either directly or via App->profile_uid - if ($this->module_loaded) { - $this->page['page_title'] = $this->module; + $this->page['page_title'] = $this->module; + try { $placeholder = ''; Core\Hook::callAll($this->module . '_mod_init', $placeholder); @@ -1334,41 +1208,41 @@ class App // "rawContent" is especially meant for technical endpoints. // This endpoint doesn't need any theme initialization or other comparable stuff. - if (!$this->error) { - call_user_func([$this->module_class, 'rawContent']); + call_user_func([$this->module_class, 'rawContent']); + + // Load current theme info after module has been initialized as theme could have been set in module + $theme_info_file = 'view/theme/' . $this->getCurrentTheme() . '/theme.php'; + if (file_exists($theme_info_file)) { + require_once $theme_info_file; } - } - // Load current theme info after module has been initialized as theme could have been set in module - $theme_info_file = 'view/theme/' . $this->getCurrentTheme() . '/theme.php'; - if (file_exists($theme_info_file)) { - require_once $theme_info_file; - } + if (function_exists(str_replace('-', '_', $this->getCurrentTheme()) . '_init')) { + $func = str_replace('-', '_', $this->getCurrentTheme()) . '_init'; + $func($this); + } - if (function_exists(str_replace('-', '_', $this->getCurrentTheme()) . '_init')) { - $func = str_replace('-', '_', $this->getCurrentTheme()) . '_init'; - $func($this); - } - - if ($this->module_loaded) { - if (! $this->error && $_SERVER['REQUEST_METHOD'] === 'POST') { + if ($_SERVER['REQUEST_METHOD'] === 'POST') { Core\Hook::callAll($this->module . '_mod_post', $_POST); call_user_func([$this->module_class, 'post']); } - if (! $this->error) { - Core\Hook::callAll($this->module . '_mod_afterpost', $placeholder); - call_user_func([$this->module_class, 'afterpost']); - } + Core\Hook::callAll($this->module . '_mod_afterpost', $placeholder); + call_user_func([$this->module_class, 'afterpost']); + } catch(HTTPException $e) { + Module\Special\HTTPException::rawContent($e); + } - if (! $this->error) { - $arr = ['content' => $content]; - Core\Hook::callAll($this->module . '_mod_content', $arr); - $content = $arr['content']; - $arr = ['content' => call_user_func([$this->module_class, 'content'])]; - Core\Hook::callAll($this->module . '_mod_aftercontent', $arr); - $content .= $arr['content']; - } + $content = ''; + + try { + $arr = ['content' => $content]; + Core\Hook::callAll($this->module . '_mod_content', $arr); + $content = $arr['content']; + $arr = ['content' => call_user_func([$this->module_class, 'content'])]; + Core\Hook::callAll($this->module . '_mod_aftercontent', $arr); + $content .= $arr['content']; + } catch(HTTPException $e) { + $content = Module\Special\HTTPException::content($e); } // initialise content region @@ -1392,16 +1266,10 @@ class App */ $this->initFooter(); - /* now that we've been through the module content, see if the page reported - * a permission problem and if so, a 403 response would seem to be in order. - */ - if (stristr(implode("", $_SESSION['sysmsg']), Core\L10n::t('Permission denied'))) { - header($_SERVER["SERVER_PROTOCOL"] . ' 403 ' . Core\L10n::t('Permission denied.')); + if (!$this->isAjax()) { + Core\Hook::callAll('page_end', $this->page['content']); } - // Report anything which needs to be communicated in the notification area (before the main body) - Core\Hook::callAll('page_end', $this->page['content']); - // Add the navigation (menu) template if ($this->module != 'install' && $this->module != 'maintenance') { $this->page['htmlhead'] .= Core\Renderer::replaceMacros(Core\Renderer::getMarkupTemplate('nav_head.tpl'), []); @@ -1446,7 +1314,7 @@ class App header("X-Friendica-Version: " . FRIENDICA_VERSION); header("Content-type: text/html; charset=utf-8"); - if (Core\Config::get('system', 'hsts') && (Core\Config::get('system', 'ssl_policy') == SSL_POLICY_FULL)) { + if ($this->config->get('system', 'hsts') && ($this->baseURL->getSSLPolicy() == BaseUrl::SSL_POLICY_FULL)) { header("Strict-Transport-Security: max-age=31536000"); } @@ -1489,12 +1357,12 @@ class App * @param string $toUrl The destination URL (Default is empty, which is the default page of the Friendica node) * @param bool $ssl if true, base URL will try to get called with https:// (works just for relative paths) * - * @throws InternalServerErrorException In Case the given URL is not relative to the Friendica node + * @throws HTTPException\InternalServerErrorException In Case the given URL is not relative to the Friendica node */ public function internalRedirect($toUrl = '', $ssl = false) { if (!empty(parse_url($toUrl, PHP_URL_SCHEME))) { - throw new InternalServerErrorException("'$toUrl is not a relative path, please use System::externalRedirectTo"); + throw new HTTPException\InternalServerErrorException("'$toUrl is not a relative path, please use System::externalRedirectTo"); } $redirectTo = $this->getBaseURL($ssl) . '/' . ltrim($toUrl, '/'); @@ -1506,7 +1374,7 @@ class App * Should only be used if it isn't clear if the URL is either internal or external * * @param string $toUrl The target URL - * @throws InternalServerErrorException + * @throws HTTPException\InternalServerErrorException */ public function redirect($toUrl) { diff --git a/src/App/Router.php b/src/App/Router.php new file mode 100644 index 000000000..6720f5443 --- /dev/null +++ b/src/App/Router.php @@ -0,0 +1,260 @@ +collectRoutes. + * + * @package Friendica\App + */ +class Router +{ + /** @var RouteCollector */ + protected $routeCollector; + + /** + * Static declaration of Friendica routes. + * + * Supports: + * - Route groups + * - Variable parts + * Disregards: + * - HTTP method other than GET + * - Named parameters + * + * Handler must be the name of a class extending Friendica\BaseModule. + * + * @brief Static declaration of Friendica routes. + */ + public function collectRoutes() + { + $this->routeCollector->addRoute(['GET'], '[/]', Module\Home::class); + $this->routeCollector->addGroup('/.well-known', function (RouteCollector $collector) { + $collector->addRoute(['GET'], '/host-meta' , Module\WellKnown\HostMeta::class); + $collector->addRoute(['GET'], '/nodeinfo[/1.0]' , Module\NodeInfo::class); + $collector->addRoute(['GET'], '/webfinger' , Module\Xrd::class); + $collector->addRoute(['GET'], '/x-social-relay' , Module\WellKnown\XSocialRelay::class); + }); + $this->routeCollector->addGroup('/2fa', function (RouteCollector $collector) { + $collector->addRoute(['GET', 'POST'], '[/]' , Module\TwoFactor\Verify::class); + $collector->addRoute(['GET', 'POST'], '/recovery' , Module\TwoFactor\Recovery::class); + }); + $this->routeCollector->addGroup('/admin', function (RouteCollector $collector) { + $collector->addRoute(['GET'] , '[/]' , Module\Admin\Summary::class); + + $collector->addRoute(['GET', 'POST'], '/addons' , Module\Admin\Addons\Index::class); + $collector->addRoute(['GET', 'POST'], '/addons/{addon}' , Module\Admin\Addons\Details::class); + + $collector->addRoute(['GET', 'POST'], '/blocklist/contact' , Module\Admin\Blocklist\Contact::class); + $collector->addRoute(['GET', 'POST'], '/blocklist/server' , Module\Admin\Blocklist\Server::class); + + $collector->addRoute(['GET'] , '/dbsync[/check]' , Module\Admin\DBSync::class); + $collector->addRoute(['GET'] , '/dbsync/{update:\d+}' , Module\Admin\DBSync::class); + $collector->addRoute(['GET'] , '/dbsync/mark/{update:\d+}', Module\Admin\DBSync::class); + + $collector->addRoute(['GET', 'POST'], '/features' , Module\Admin\Features::class); + $collector->addRoute(['GET'] , '/federation' , Module\Admin\Federation::class); + + $collector->addRoute(['GET', 'POST'], '/item/delete' , Module\Admin\Item\Delete::class); + $collector->addRoute(['GET', 'POST'], '/item/source[/{guid}]' , Module\Admin\Item\Source::class); + + $collector->addRoute(['GET'] , '/logs/view' , Module\Admin\Logs\View::class); + $collector->addRoute(['GET', 'POST'], '/logs' , Module\Admin\Logs\Settings::class); + + $collector->addRoute(['GET'] , '/phpinfo' , Module\Admin\PhpInfo::class); + + $collector->addRoute(['GET'] , '/queue[/deferred]' , Module\Admin\Queue::class); + + $collector->addRoute(['GET', 'POST'], '/site' , Module\Admin\Site::class); + + $collector->addRoute(['GET', 'POST'], '/themes' , Module\Admin\Themes\Index::class); + $collector->addRoute(['GET', 'POST'], '/themes/{theme}' , Module\Admin\Themes\Details::class); + $collector->addRoute(['GET', 'POST'], '/themes/{theme}/embed' , Module\Admin\Themes\Embed::class); + + $collector->addRoute(['GET', 'POST'], '/tos' , Module\Admin\Tos::class); + + $collector->addRoute(['GET', 'POST'], '/users[/{action}/{uid}]' , Module\Admin\Users::class); + }); + $this->routeCollector->addRoute(['GET'], '/amcd', Module\AccountManagementControlDocument::class); + $this->routeCollector->addRoute(['GET'], '/acctlink', Module\Acctlink::class); + $this->routeCollector->addRoute(['GET'], '/allfriends/{id:\d+}', Module\AllFriends::class); + $this->routeCollector->addRoute(['GET'], '/apps', Module\Apps::class); + $this->routeCollector->addRoute(['GET'], '/attach/{item:\d+}', Module\Attach::class); + $this->routeCollector->addRoute(['GET'], '/babel', Module\Debug\Babel::class); + $this->routeCollector->addRoute(['GET'], '/bookmarklet', Module\Bookmarklet::class); + $this->routeCollector->addGroup('/contact', function (RouteCollector $collector) { + $collector->addRoute(['GET'], '[/]', Module\Contact::class); + $collector->addRoute(['GET', 'POST'], '/{id:\d+}[/]', Module\Contact::class); + $collector->addRoute(['GET'], '/{id:\d+}/archive', Module\Contact::class); + $collector->addRoute(['GET'], '/{id:\d+}/block', Module\Contact::class); + $collector->addRoute(['GET'], '/{id:\d+}/conversations', Module\Contact::class); + $collector->addRoute(['GET'], '/{id:\d+}/drop', Module\Contact::class); + $collector->addRoute(['GET'], '/{id:\d+}/ignore', Module\Contact::class); + $collector->addRoute(['GET'], '/{id:\d+}/posts', Module\Contact::class); + $collector->addRoute(['GET'], '/{id:\d+}/update', Module\Contact::class); + $collector->addRoute(['GET'], '/{id:\d+}/updateprofile', Module\Contact::class); + $collector->addRoute(['GET'], '/archived', Module\Contact::class); + $collector->addRoute(['GET', 'POST'], '/batch', Module\Contact::class); + $collector->addRoute(['GET'], '/blocked', Module\Contact::class); + $collector->addRoute(['GET'], '/hidden', Module\Contact::class); + $collector->addRoute(['GET'], '/ignored', Module\Contact::class); + }); + $this->routeCollector->addRoute(['GET'], '/credits', Module\Credits::class); + $this->routeCollector->addRoute(['GET'], '/dirfind', Module\Search\Directory::class); + $this->routeCollector->addRoute(['GET'], '/directory', Module\Directory::class); + $this->routeCollector->addGroup('/feed', function (RouteCollector $collector) { + $collector->addRoute(['GET'], '/{nickname}', Module\Feed::class); + $collector->addRoute(['GET'], '/{nickname}/posts', Module\Feed::class); + $collector->addRoute(['GET'], '/{nickname}/comments', Module\Feed::class); + $collector->addRoute(['GET'], '/{nickname}/replies', Module\Feed::class); + $collector->addRoute(['GET'], '/{nickname}/activity', Module\Feed::class); + }); + $this->routeCollector->addRoute(['GET'], '/feedtest', Module\Debug\Feed::class); + $this->routeCollector->addGroup('/fetch', function (RouteCollector $collector) { + $collector->addRoute(['GET'], '/post/{guid}', Module\Diaspora\Fetch::class); + $collector->addRoute(['GET'], '/status_message/{guid}', Module\Diaspora\Fetch::class); + $collector->addRoute(['GET'], '/reshare/{guid}', Module\Diaspora\Fetch::class); + }); + $this->routeCollector->addRoute(['GET'], '/filer[/{id:\d+}]', Module\Filer\SaveTag::class); + $this->routeCollector->addRoute(['GET'], '/filerm/{id:\d+}', Module\Filer\RemoveTag::class); + $this->routeCollector->addRoute(['GET', 'POST'], '/follow_confirm', Module\FollowConfirm::class); + $this->routeCollector->addRoute(['GET'], '/followers/{owner}', Module\Followers::class); + $this->routeCollector->addRoute(['GET'], '/following/{owner}', Module\Following::class); + $this->routeCollector->addRoute(['GET'], '/friendica[/json]', Module\Friendica::class); + $this->routeCollector->addGroup('/group', function (RouteCollector $collector) { + $collector->addRoute(['GET', 'POST'], '[/]', Module\Group::class); + $collector->addRoute(['GET', 'POST'], '/{group:\d+}', Module\Group::class); + $collector->addRoute(['GET', 'POST'], '/none', Module\Group::class); + $collector->addRoute(['GET', 'POST'], '/new', Module\Group::class); + $collector->addRoute(['GET', 'POST'], '/drop/{group:\d+}', Module\Group::class); + $collector->addRoute(['GET', 'POST'], '/{group:\d+}/{contact:\d+}', Module\Group::class); + + $collector->addRoute(['GET', 'POST'], '/{group:\d+}/add/{contact:\d+}', Module\Group::class); + $collector->addRoute(['GET', 'POST'], '/{group:\d+}/remove/{contact:\d+}', Module\Group::class); + }); + $this->routeCollector->addRoute(['GET'], '/hashtag', Module\Hashtag::class); + $this->routeCollector->addRoute(['GET'], '/home', Module\Home::class); + $this->routeCollector->addRoute(['GET'], '/help[/{doc:.+}]', Module\Help::class); + $this->routeCollector->addRoute(['GET'], '/inbox[/{nickname}]', Module\Inbox::class); + $this->routeCollector->addRoute(['GET', 'POST'], '/invite', Module\Invite::class); + $this->routeCollector->addGroup('/install', function (RouteCollector $collector) { + $collector->addRoute(['GET', 'POST'], '[/]', Module\Install::class); + $collector->addRoute(['GET'], '/testrewrite', Module\Install::class); + }); + $this->routeCollector->addRoute(['GET'], '/like/{item:\d+}', Module\Like::class); + $this->routeCollector->addRoute(['GET', 'POST'], '/localtime', Module\Debug\Localtime::class); + $this->routeCollector->addRoute(['GET', 'POST'], '/login', Module\Login::class); + $this->routeCollector->addRoute(['GET', 'POST'], '/logout', Module\Logout::class); + $this->routeCollector->addRoute(['GET'], '/magic', Module\Magic::class); + $this->routeCollector->addRoute(['GET'], '/maintenance', Module\Maintenance::class); + $this->routeCollector->addRoute(['GET'], '/manifest', Module\Manifest::class); + $this->routeCollector->addRoute(['GET'], '/modexp/{nick}', Module\PublicRSAKey::class); + $this->routeCollector->addRoute(['GET'], '/newmember', Module\Welcome::class); + $this->routeCollector->addRoute(['GET'], '/nodeinfo/1.0', Module\NodeInfo::class); + $this->routeCollector->addRoute(['GET'], '/nogroup', Module\Group::class); + $this->routeCollector->addGroup('/notify', function (RouteCollector $collector) { + $collector->addRoute(['GET'], '[/]', Module\Notifications\Notify::class); + $collector->addRoute(['GET'], '/view/{id:\d+}', Module\Notifications\Notify::class); + $collector->addRoute(['GET'], '/mark/all', Module\Notifications\Notify::class); + }); + $this->routeCollector->addRoute(['GET'], '/objects/{guid}', Module\Objects::class); + $this->routeCollector->addGroup('/oembed', function (RouteCollector $collector) { + $collector->addRoute(['GET'], '/b2h', Module\Oembed::class); + $collector->addRoute(['GET'], '/h2b', Module\Oembed::class); + $collector->addRoute(['GET'], '/{hash}', Module\Oembed::class); + }); + $this->routeCollector->addRoute(['GET'], '/outbox/{owner}', Module\Outbox::class); + $this->routeCollector->addRoute(['GET'], '/owa', Module\Owa::class); + $this->routeCollector->addRoute(['GET'], '/opensearch', Module\OpenSearch::class); + $this->routeCollector->addGroup('/photo', function (RouteCollector $collector) { + $collector->addRoute(['GET'], '/{name}', Module\Photo::class); + $collector->addRoute(['GET'], '/{type}/{name}', Module\Photo::class); + $collector->addRoute(['GET'], '/{type}/{customize}/{name}', Module\Photo::class); + }); + $this->routeCollector->addRoute(['GET'], '/pretheme', Module\ThemeDetails::class); + $this->routeCollector->addRoute(['GET'], '/probe', Module\Debug\Probe::class); + $this->routeCollector->addGroup('/profile', function (RouteCollector $collector) { + $collector->addRoute(['GET'], '/{nickname}', Module\Profile::class); + $collector->addRoute(['GET'], '/{nickname}/{to:\d{4}-\d{2}-\d{2}}/{from:\d{4}-\d{2}-\d{2}}', Module\Profile::class); + $collector->addRoute(['GET'], '/{nickname}/contacts[/{type}]', Module\Profile\Contacts::class); + $collector->addRoute(['GET'], '/{profile:\d+}/view', Module\Profile::class); + }); + $this->routeCollector->addGroup('/proxy', function (RouteCollector $collector) { + $collector->addRoute(['GET'], '[/]' , Module\Proxy::class); + $collector->addRoute(['GET'], '/{url}' , Module\Proxy::class); + $collector->addRoute(['GET'], '/{sub1}/{url}' , Module\Proxy::class); + $collector->addRoute(['GET'], '/{sub1}/{sub2}/{url}' , Module\Proxy::class); + }); + + $this->routeCollector->addGroup('/settings', function (RouteCollector $collector) { + $collector->addGroup('/2fa', function (RouteCollector $collector) { + $collector->addRoute(['GET', 'POST'], '[/]' , Module\Settings\TwoFactor\Index::class); + $collector->addRoute(['GET', 'POST'], '/recovery' , Module\Settings\TwoFactor\Recovery::class); + $collector->addRoute(['GET', 'POST'], '/verify' , Module\Settings\TwoFactor\Verify::class); + }); + }); + $this->routeCollector->addRoute(['GET'], '/randprof', Module\RandomProfile::class); + $this->routeCollector->addRoute(['GET', 'POST'], '/register', Module\Register::class); + $this->routeCollector->addRoute(['GET'], '/robots.txt', Module\RobotsTxt::class); + $this->routeCollector->addRoute(['GET'], '/rsd.xml', Module\ReallySimpleDiscovery::class); + $this->routeCollector->addRoute(['GET'], '/smilies[/json]', Module\Smilies::class); + $this->routeCollector->addRoute(['GET'], '/statistics.json', Module\Statistics::class); + $this->routeCollector->addRoute(['GET'], '/starred/{item:\d+}', Module\Starred::class); + $this->routeCollector->addRoute(['GET'], '/toggle_mobile', Module\ToggleMobile::class); + $this->routeCollector->addRoute(['GET'], '/tos', Module\Tos::class); + $this->routeCollector->addRoute(['GET'], '/view/theme/{theme}/style.pcss', Module\Theme::class); + $this->routeCollector->addRoute(['GET'], '/viewsrc/{item:\d+}', Module\Debug\ItemBody::class); + $this->routeCollector->addRoute(['GET'], '/webfinger', Module\Debug\WebFinger::class); + $this->routeCollector->addRoute(['GET'], '/xrd', Module\Xrd::class); + } + + public function __construct(RouteCollector $routeCollector = null) + { + if (!$routeCollector) { + $routeCollector = new RouteCollector(new Std(), new GroupCountBased()); + } + + $this->routeCollector = $routeCollector; + } + + public function getRouteCollector() + { + return $this->routeCollector; + } + + /** + * Returns the relevant module class name for the given page URI or NULL if no route rule matched. + * + * @param string $cmd The path component of the request URL without the query string + * @return string|null A Friendica\BaseModule-extending class name if a route rule matched + */ + public function getModuleClass($cmd) + { + $cmd = '/' . ltrim($cmd, '/'); + + $dispatcher = new \FastRoute\Dispatcher\GroupCountBased($this->routeCollector->getData()); + + $moduleClass = null; + + // @TODO: Enable method-specific modules + $httpMethod = 'GET'; + $routeInfo = $dispatcher->dispatch($httpMethod, $cmd); + if ($routeInfo[0] === Dispatcher::FOUND) { + $moduleClass = $routeInfo[1]; + } + + return $moduleClass; + } +} diff --git a/src/BaseModule.php b/src/BaseModule.php index 85a506bea..dd9059bfb 100644 --- a/src/BaseModule.php +++ b/src/BaseModule.php @@ -35,6 +35,8 @@ abstract class BaseModule extends BaseObject */ public static function rawContent() { + // echo ''; + // exit; } /** @@ -151,7 +153,7 @@ abstract class BaseModule extends BaseObject Logger::log('checkFormSecurityToken failed: user ' . $a->user['guid'] . ' - form element ' . $typename); Logger::log('checkFormSecurityToken failed: _REQUEST data: ' . print_r($_REQUEST, true), Logger::DATA); - System::httpExit(403); + throw new \Friendica\Network\HTTPException\ForbiddenException(); } } } diff --git a/src/BaseObject.php b/src/BaseObject.php index 7e90478a9..9a2d06475 100644 --- a/src/BaseObject.php +++ b/src/BaseObject.php @@ -15,6 +15,9 @@ use Friendica\Network\HTTPException\InternalServerErrorException; */ class BaseObject { + /** + * @var App + */ private static $app = null; /** diff --git a/src/Core/Console/ArchiveContact.php b/src/Console/ArchiveContact.php similarity index 93% rename from src/Core/Console/ArchiveContact.php rename to src/Console/ArchiveContact.php index 27de85901..cf177cf2e 100644 --- a/src/Core/Console/ArchiveContact.php +++ b/src/Console/ArchiveContact.php @@ -1,6 +1,6 @@ true], ['nurl' => $nurl])) { - $condition = ["`cid` IN (SELECT `id` FROM `contact` WHERE `archive`)"]; - DBA::delete('queue', $condition); $this->out(L10n::t('The contact entries have been archived')); } else { throw new RuntimeException('The contact archival failed.'); diff --git a/src/Core/Console/AutomaticInstallation.php b/src/Console/AutomaticInstallation.php similarity index 56% rename from src/Core/Console/AutomaticInstallation.php rename to src/Console/AutomaticInstallation.php index 9d1e8506c..718573d27 100644 --- a/src/Core/Console/AutomaticInstallation.php +++ b/src/Console/AutomaticInstallation.php @@ -1,12 +1,15 @@ prepared config file (e.g. "config/local.config.php" itself) which will override every other config option - except the environment variables) - -s|--savedb Save the DB credentials to the file (if environment variables is used) - -H|--dbhost The host of the mysql/mariadb database (env MYSQL_HOST) - -p|--dbport The port of the mysql/mariadb database (env MYSQL_PORT) - -d|--dbdata The name of the mysql/mariadb database (env MYSQL_DATABASE) - -U|--dbuser The username of the mysql/mariadb database login (env MYSQL_USER or MYSQL_USERNAME) - -P|--dbpass The password of the mysql/mariadb database login (env MYSQL_PASSWORD) - -u|--urlpath The URL path of Friendica - f.e. '/friendica' (env FRIENDICA_URL_PATH) - -b|--phppath The path of the PHP binary (env FRIENDICA_PHP_PATH) - -A|--admin The admin email address of Friendica (env FRIENDICA_ADMIN_MAIL) - -T|--tz The timezone of Friendica (env FRIENDICA_TZ) - -L|--lang The language of Friendica (env FRIENDICA_LANG) + -s|--savedb Save the DB credentials to the file (if environment variables is used) + -H|--dbhost The host of the mysql/mariadb database (env MYSQL_HOST) + -p|--dbport The port of the mysql/mariadb database (env MYSQL_PORT) + -d|--dbdata The name of the mysql/mariadb database (env MYSQL_DATABASE) + -U|--dbuser The username of the mysql/mariadb database login (env MYSQL_USER or MYSQL_USERNAME) + -P|--dbpass The password of the mysql/mariadb database login (env MYSQL_PASSWORD) + -U|--url The full base URL of Friendica - f.e. 'https://friendica.local/sub' (env FRIENDICA_URL) + -B|--phppath The path of the PHP binary (env FRIENDICA_PHP_PATH) + -b|--basepath The basepath of Friendica (env FRIENDICA_BASE_PATH) + -t|--tz The timezone of Friendica (env FRIENDICA_TZ) + -L|--lang The language of Friendica (env FRIENDICA_LANG) Environment variables MYSQL_HOST The host of the mysql/mariadb database (mandatory if mysql and environment is used) @@ -47,8 +50,9 @@ Environment variables MYSQL_USERNAME|MYSQL_USER The username of the mysql/mariadb database login (MYSQL_USERNAME is for mysql, MYSQL_USER for mariadb) MYSQL_PASSWORD The password of the mysql/mariadb database login MYSQL_DATABASE The name of the mysql/mariadb database - FRIENDICA_URL_PATH The URL path of Friendica (f.e. '/friendica') - FRIENDICA_PHP_PATH The path of the PHP binary + FRIENDICA_URL The full base URL of Friendica - f.e. 'https://friendica.local/sub' + FRIENDICA_PHP_PATH The path of the PHP binary - leave empty for auto detection + FRIENDICA_BASE_PATH The basepath of Friendica - leave empty for auto detection FRIENDICA_ADMIN_MAIL The admin email address of Friendica (this email will be used for admin access) FRIENDICA_TZ The timezone of Friendica FRIENDICA_LANG The langauge of Friendica @@ -74,6 +78,9 @@ HELP; $installer = new Installer(); + $configCache = $a->getConfigCache(); + $installer->setUpCache($configCache, BasePath::create($a->getBasePath(), $_SERVER)); + $this->out(" Complete!\n\n"); // Check Environment @@ -81,7 +88,7 @@ HELP; $installer->resetChecks(); - if (!$this->runBasicChecks($installer)) { + if (!$this->runBasicChecks($installer, $configCache)) { $errorMessage = $this->extractErrors($installer->getChecks()); throw new RuntimeException($errorMessage); } @@ -96,47 +103,66 @@ HELP; // Copy config file $this->out("Copying config file...\n"); if (!copy($a->getBasePath() . DIRECTORY_SEPARATOR . $config_file, $a->getBasePath() . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'local.config.php')) { - throw new RuntimeException("ERROR: Saving config file failed. Please copy '$config_file' to '" . $a->getBasePath() . "'" . DIRECTORY_SEPARATOR . "config" . DIRECTORY_SEPARATOR . "local.config.php' manually.\n"); + throw new RuntimeException("ERROR: Saving config file failed. Please copy '$config_file' to '" . $a->getBasePath() . "'" . DIRECTORY_SEPARATOR . "config" . DIRECTORY_SEPARATOR . "local.config.php' manually.\n"); } } - $db_host = $a->getConfigCache()->get('database', 'hostname'); - $db_user = $a->getConfigCache()->get('database', 'username'); - $db_pass = $a->getConfigCache()->get('database', 'password'); - $db_data = $a->getConfigCache()->get('database', 'database'); + //reload the config cache + $loader = new ConfigFileLoader($a->getBasePath(), $a->getMode()); + $loader->setupCache($configCache); + } else { // Creating config file $this->out("Creating config file...\n"); $save_db = $this->getOption(['s', 'savedb'], false); - $db_host = $this->getOption(['H', 'dbhost'], ($save_db) ? getenv('MYSQL_HOST') : ''); + $db_host = $this->getOption(['H', 'dbhost'], ($save_db) ? (getenv('MYSQL_HOST')) : Installer::DEFAULT_HOST); $db_port = $this->getOption(['p', 'dbport'], ($save_db) ? getenv('MYSQL_PORT') : null); - $db_data = $this->getOption(['d', 'dbdata'], ($save_db) ? getenv('MYSQL_DATABASE') : ''); - $db_user = $this->getOption(['U', 'dbuser'], ($save_db) ? getenv('MYSQL_USER') . getenv('MYSQL_USERNAME') : ''); - $db_pass = $this->getOption(['P', 'dbpass'], ($save_db) ? getenv('MYSQL_PASSWORD') : ''); - $url_path = $this->getOption(['u', 'urlpath'], !empty('FRIENDICA_URL_PATH') ? getenv('FRIENDICA_URL_PATH') : null); - $php_path = $this->getOption(['b', 'phppath'], !empty('FRIENDICA_PHP_PATH') ? getenv('FRIENDICA_PHP_PATH') : null); - $admin_mail = $this->getOption(['A', 'admin'], !empty('FRIENDICA_ADMIN_MAIL') ? getenv('FRIENDICA_ADMIN_MAIL') : ''); - $tz = $this->getOption(['T', 'tz'], !empty('FRIENDICA_TZ') ? getenv('FRIENDICA_TZ') : ''); - $lang = $this->getOption(['L', 'lang'], !empty('FRIENDICA_LANG') ? getenv('FRIENDICA_LANG') : ''); + $configCache->set('database', 'hostname', $db_host . (!empty($db_port) ? ':' . $db_port : '')); + $configCache->set('database', 'database', + $this->getOption(['d', 'dbdata'], + ($save_db) ? getenv('MYSQL_DATABASE') : '')); + $configCache->set('database', 'username', + $this->getOption(['U', 'dbuser'], + ($save_db) ? getenv('MYSQL_USER') . getenv('MYSQL_USERNAME') : '')); + $configCache->set('database', 'password', + $this->getOption(['P', 'dbpass'], + ($save_db) ? getenv('MYSQL_PASSWORD') : '')); - if (empty($php_path)) { - $php_path = $installer->getPHPPath(); + $php_path = $this->getOption(['b', 'phppath'], !empty('FRIENDICA_PHP_PATH') ? getenv('FRIENDICA_PHP_PATH') : null); + if (!empty($php_path)) { + $configCache->set('config', 'php_path', $php_path); + } else { + $configCache->set('config', 'php_path', $installer->getPHPPath()); } - $installer->createConfig( - $php_path, - $url_path, - (!empty($db_port) ? $db_host . ':' . $db_port : $db_host), - $db_user, - $db_pass, - $db_data, - $tz, - $lang, - $admin_mail, - $a->getBasePath() - ); + $configCache->set('config', 'admin_email', + $this->getOption(['A', 'admin'], + !empty(getenv('FRIENDICA_ADMIN_MAIL')) ? getenv('FRIENDICA_ADMIN_MAIL') : '')); + $configCache->set('system', 'default_timezone', + $this->getOption(['T', 'tz'], + !empty(getenv('FRIENDICA_TZ')) ? getenv('FRIENDICA_TZ') : Installer::DEFAULT_TZ)); + $configCache->set('system', 'language', + $this->getOption(['L', 'lang'], + !empty(getenv('FRIENDICA_LANG')) ? getenv('FRIENDICA_LANG') : Installer::DEFAULT_LANG)); + + $basepath = $this->getOption(['b', 'basepath'], !empty(getenv('FRIENDICA_BASE_PATH')) ? getenv('FRIENDICA_BASE_PATH') : null); + if (!empty($basepath)) { + $configCache->set('system', 'basepath', $basepath); + } + + $url = $this->getOption(['U', 'url'], !empty(getenv('FRIENDICA_URL')) ? getenv('FRIENDICA_URL') : null); + + if (empty($url)) { + $this->out('The Friendica URL has to be set during CLI installation.'); + return 1; + } else { + $baseUrl = new BaseURL($a->getConfig(), []); + $baseUrl->saveByURL($url); + } + + $installer->createConfig($configCache); } $this->out(" Complete!\n\n"); @@ -146,7 +172,7 @@ HELP; $installer->resetChecks(); - if (!$installer->checkDB($a->getBasePath(), $a->getConfigCache(), $a->getProfiler(), $db_host, $db_user, $db_pass, $db_data)) { + if (!$installer->checkDB($configCache, $a->getProfiler())) { $errorMessage = $this->extractErrors($installer->getChecks()); throw new RuntimeException($errorMessage); } @@ -180,12 +206,13 @@ HELP; } /** - * @param Installer $installer the Installer instance + * @param Installer $installer The Installer instance + * @param Config\Cache\IConfigCache $configCache The config cache * * @return bool true if checks were successfully, otherwise false * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - private function runBasicChecks(Installer $installer) + private function runBasicChecks(Installer $installer, Config\Cache\IConfigCache $configCache) { $checked = true; @@ -206,10 +233,7 @@ HELP; $checked = false; } - $php_path = null; - if (!empty(Config::get('config', 'php_path'))) { - $php_path = Config::get('config', 'php_path'); - } + $php_path = $configCache->get('config', 'php_path'); if (!$installer->checkPHP($php_path, true)) { $checked = false; diff --git a/src/Core/Console/Cache.php b/src/Console/Cache.php similarity index 99% rename from src/Core/Console/Cache.php rename to src/Console/Cache.php index 510c05b04..eefb6cc60 100644 --- a/src/Core/Console/Cache.php +++ b/src/Console/Cache.php @@ -1,6 +1,6 @@ [-h|--help|-?] [-v] + bin/console globalcommunityblock [] [-h|--help|-?] [-v] Description Blocks an account in such a way that no postings or comments this account writes are accepted to this node. + You can provide a optional reason for the block. Options -h|--help|-? Show help information @@ -52,7 +53,7 @@ HELP; return 0; } - if (count($this->args) > 1) { + if (count($this->args) > 2) { throw new \Asika\SimpleConsole\CommandArgsException('Too many arguments'); } @@ -64,7 +65,9 @@ HELP; if (!$contact_id) { throw new \RuntimeException(L10n::t('Could not find any contact entry for this URL (%s)', $this->getArgument(0))); } - if(Contact::block($contact_id)) { + + $block_reason = $this->getArgument(1); + if(Contact::block($contact_id, $block_reason)) { $this->out(L10n::t('The contact has been blocked from the node')); } else { throw new \RuntimeException('The contact block failed.'); diff --git a/src/Core/Console/GlobalCommunitySilence.php b/src/Console/GlobalCommunitySilence.php similarity index 98% rename from src/Core/Console/GlobalCommunitySilence.php rename to src/Console/GlobalCommunitySilence.php index b0281241e..daaf55149 100644 --- a/src/Core/Console/GlobalCommunitySilence.php +++ b/src/Console/GlobalCommunitySilence.php @@ -1,6 +1,6 @@ [-h|--help|-?] [-v] + bin/console serverblock remove [-h|--help|-?] [-v] + +Description + With this tool, you can list the current blocked servers + or you can add / remove a blocked server from the list + +Options + -h|--help|-? Show help information + -v Show more debug information. +HELP; + return $help; + } + + protected function doExecute() + { + $a = BaseObject::getApp(); + + if (count($this->args) == 0) { + $this->printBlockedServers($a->getConfig()); + return 0; + } + + switch ($this->getArgument(0)) { + case 'add': + return $this->addBlockedServer($a->getConfig()); + case 'remove': + return $this->removeBlockedServer($a->getConfig()); + default: + throw new CommandArgsException('Unknown command.'); + break; + } + } + + /** + * Prints the whole list of blocked domains including the reason + * + * @param Configuration $config + */ + private function printBlockedServers(Configuration $config) + { + $table = new Console_Table(); + $table->setHeaders(['Domain', 'Reason']); + $blocklist = $config->get('system', 'blocklist'); + foreach ($blocklist as $domain) { + $table->addRow($domain); + } + $this->out($table->getTable()); + } + + /** + * Adds a server to the blocked list + * + * @param Configuration $config + * + * @return int The return code (0 = success, 1 = failed) + */ + private function addBlockedServer(Configuration $config) + { + if (count($this->args) < 2 || count($this->args) > 3) { + throw new CommandArgsException('Add needs a domain and optional a reason.'); + } + + $domain = $this->getArgument(1); + $reason = (count($this->args) === 3) ? $this->getArgument(2) : self::DEFAULT_REASON; + + $update = false; + + $currBlocklist = $config->get('system', 'blocklist'); + $newBlockList = []; + foreach ($currBlocklist as $blocked) { + if ($blocked['domain'] === $domain) { + $update = true; + $newBlockList[] = [ + 'domain' => $domain, + 'reason' => $reason, + ]; + } else { + $newBlockList[] = $blocked; + } + } + + if (!$update) { + $newBlockList[] = [ + 'domain' => $domain, + 'reason' => $reason, + ]; + } + + if ($config->set('system', 'blocklist', $newBlockList)) { + if ($update) { + $this->out(sprintf("The domain '%s' is now updated. (Reason: '%s')", $domain, $reason)); + } else { + $this->out(sprintf("The domain '%s' is now blocked. (Reason: '%s')", $domain, $reason)); + } + return 0; + } else { + $this->out(sprintf("Couldn't save '%s' as blocked server", $domain)); + return 1; + } + } + + /** + * Removes a server from the blocked list + * + * @param Configuration $config + * + * @return int The return code (0 = success, 1 = failed) + */ + private function removeBlockedServer(Configuration $config) + { + if (count($this->args) !== 2) { + throw new CommandArgsException('Remove needs a second parameter.'); + } + + $domain = $this->getArgument(1); + + $found = false; + + $currBlocklist = $config->get('system', 'blocklist'); + $newBlockList = []; + foreach ($currBlocklist as $blocked) { + if ($blocked['domain'] === $domain) { + $found = true; + } else { + $newBlockList[] = $blocked; + } + } + + if (!$found) { + $this->out(sprintf("The domain '%s' is not blocked.", $domain)); + return 1; + } + + if ($config->set('system', 'blocklist', $newBlockList)) { + $this->out(sprintf("The domain '%s' is not more blocked", $domain)); + return 0; + } else { + $this->out(sprintf("Couldn't remove '%s' from blocked servers", $domain)); + return 1; + } + } +} diff --git a/src/Core/Console/Storage.php b/src/Console/Storage.php similarity index 97% rename from src/Core/Console/Storage.php rename to src/Console/Storage.php index ce89ce1e2..30b556782 100644 --- a/src/Core/Console/Storage.php +++ b/src/Console/Storage.php @@ -1,6 +1,6 @@ out(); - $this->out('This sistem is using legacy storage system'); + $this->out('This system is using legacy storage system'); } if ($current !== '' && !$isregisterd) { $this->out(); diff --git a/src/Core/Console/Typo.php b/src/Console/Typo.php similarity index 86% rename from src/Core/Console/Typo.php rename to src/Console/Typo.php index 8d07051e8..216d05723 100644 --- a/src/Core/Console/Typo.php +++ b/src/Console/Typo.php @@ -1,6 +1,6 @@ getConfigCache()->get('config', 'php_path', 'php'); + $php_path = BaseObject::getApp()->getConfig()->get('config', 'php_path', 'php'); if ($this->getOption('v')) { $this->out('Directory: src'); @@ -57,6 +57,18 @@ HELP; } } + if ($this->getOption('v')) { + $this->out('Directory: tests'); + } + + $Iterator = new \RecursiveDirectoryIterator('tests'); + + foreach (new \RecursiveIteratorIterator($Iterator) as $file) { + if (substr($file, -4) === '.php') { + $this->checkFile($php_path, $file); + } + } + if ($this->getOption('v')) { $this->out('Directory: mod'); } diff --git a/src/Content/ForumManager.php b/src/Content/ForumManager.php index 0ef18df35..98ea7aa6b 100644 --- a/src/Content/ForumManager.php +++ b/src/Content/ForumManager.php @@ -111,7 +111,7 @@ class ForumManager $selected = (($cid == $contact['id']) ? ' forum-selected' : ''); $entry = [ - 'url' => 'network?f=&cid=' . $contact['id'], + 'url' => 'network?cid=' . $contact['id'], 'external_url' => Contact::magicLink($contact['url']), 'name' => $contact['name'], 'cid' => $contact['id'], @@ -196,18 +196,18 @@ class ForumManager */ public static function countUnseenItems() { - $r = q( + $stmtContacts = DBA::p( "SELECT `contact`.`id`, `contact`.`name`, COUNT(*) AS `count` FROM `item` INNER JOIN `contact` ON `item`.`contact-id` = `contact`.`id` - WHERE `item`.`uid` = %d AND `item`.`visible` AND NOT `item`.`deleted` AND `item`.`unseen` + WHERE `item`.`uid` = ? AND `item`.`visible` AND NOT `item`.`deleted` AND `item`.`unseen` AND `contact`.`network`= 'dfrn' AND (`contact`.`forum` OR `contact`.`prv`) AND NOT `contact`.`blocked` AND NOT `contact`.`hidden` AND NOT `contact`.`pending` AND NOT `contact`.`archive` AND `contact`.`success_update` > `failure_update` GROUP BY `contact`.`id` ", - intval(local_user()) + local_user() ); - return $r; + return DBA::toArray($stmtContacts); } } diff --git a/src/Content/Nav.php b/src/Content/Nav.php index 1eae2a3e3..690af1935 100644 --- a/src/Content/Nav.php +++ b/src/Content/Nav.php @@ -67,7 +67,6 @@ class Nav $tpl = Renderer::getMarkupTemplate('nav.tpl'); $nav .= Renderer::replaceMacros($tpl, [ - '$baseurl' => System::baseUrl(), '$sitelocation' => $nav_info['sitelocation'], '$nav' => $nav_info['nav'], '$banner' => $nav_info['banner'], @@ -149,9 +148,13 @@ class Nav $nav['usermenu'] = []; $userinfo = null; - if (local_user()) { + if (local_user() || remote_user()) { $nav['logout'] = ['logout', L10n::t('Logout'), '', L10n::t('End this session')]; + } else { + $nav['login'] = ['login', L10n::t('Login'), ($a->module == 'login' ? 'selected' : ''), L10n::t('Sign in')]; + } + if (local_user()) { // user menu $nav['usermenu'][] = ['profile/' . $a->user['nickname'], L10n::t('Status'), '', L10n::t('Your posts and conversations')]; $nav['usermenu'][] = ['profile/' . $a->user['nickname'] . '?tab=profile', L10n::t('Profile'), '', L10n::t('Your profile page')]; @@ -166,8 +169,6 @@ class Nav 'icon' => (DBA::isResult($contact) ? $a->removeBaseURL($contact['micro']) : 'images/person-48.jpg'), 'name' => $a->user['username'], ]; - } else { - $nav['login'] = ['login', L10n::t('Login'), ($a->module == 'login' ? 'selected' : ''), L10n::t('Sign in')]; } // "Home" should also take you home from an authenticated remote profile connection diff --git a/src/Content/OEmbed.php b/src/Content/OEmbed.php index 0093ba11a..94e95e5f5 100644 --- a/src/Content/OEmbed.php +++ b/src/Content/OEmbed.php @@ -83,8 +83,7 @@ class OEmbed if (!in_array($ext, $noexts)) { // try oembed autodiscovery - $redirects = 0; - $html_text = Network::fetchUrl($embedurl, false, $redirects, 15, 'text/*'); + $html_text = Network::fetchUrl($embedurl, false, 15, 'text/*'); if ($html_text) { $dom = @DOMDocument::loadHTML($html_text); if ($dom) { @@ -181,7 +180,6 @@ class OEmbed $tw = $th * $tr; $tpl = Renderer::getMarkupTemplate('oembed_video.tpl'); $ret .= Renderer::replaceMacros($tpl, [ - '$baseurl' => System::baseUrl(), '$embedurl' => $oembed->embed_url, '$escapedhtml' => base64_encode($oembed->html), '$tw' => $tw, diff --git a/src/Content/Smilies.php b/src/Content/Smilies.php index 902395997..57d14633a 100644 --- a/src/Content/Smilies.php +++ b/src/Content/Smilies.php @@ -213,8 +213,8 @@ class Smilies return $text; } - $text = preg_replace_callback('/
    (.*?)<\/pre>/ism'  , 'self::encode', $text);
    -		$text = preg_replace_callback('/(.*?)<\/code>/ism', 'self::encode', $text);
    +		$text = preg_replace_callback('/<(pre)>(.*?)<\/pre>/ism', 'self::encode', $text);
    +		$text = preg_replace_callback('/<(code)>(.*?)<\/code>/ism', 'self::encode', $text);
     
     		if ($no_images) {
     			$cleaned = ['texts' => [], 'icons' => []];
    @@ -231,8 +231,8 @@ class Smilies
     		$text = preg_replace_callback('/<(3+)/', 'self::pregHeart', $text);
     		$text = self::strOrigReplace($smilies['texts'], $smilies['icons'], $text);
     
    -		$text = preg_replace_callback('/
    (.*?)<\/pre>/ism', 'self::decode', $text);
    -		$text = preg_replace_callback('/(.*?)<\/code>/ism', 'self::decode', $text);
    +		$text = preg_replace_callback('/<(code)>(.*?)<\/code>/ism', 'self::decode', $text);
    +		$text = preg_replace_callback('/<(pre)>(.*?)<\/pre>/ism', 'self::decode', $text);
     
     		return $text;
     	}
    @@ -244,7 +244,7 @@ class Smilies
     	 */
     	private static function encode($m)
     	{
    -		return(str_replace($m[1], Strings::base64UrlEncode($m[1]), $m[0]));
    +		return '<' . $m[1] . '>' . Strings::base64UrlEncode($m[2]) . '';
     	}
     
     	/**
    @@ -255,7 +255,7 @@ class Smilies
     	 */
     	private static function decode($m)
     	{
    -		return(str_replace($m[1], Strings::base64UrlDecode($m[1]), $m[0]));
    +		return '<' . $m[1] . '>' . Strings::base64UrlDecode($m[2]) . '';
     	}
     
     
    diff --git a/src/Content/Text/BBCode.php b/src/Content/Text/BBCode.php
    index e417b38a4..61fe707b4 100644
    --- a/src/Content/Text/BBCode.php
    +++ b/src/Content/Text/BBCode.php
    @@ -72,9 +72,7 @@ class BBCode extends BaseObject
     
     				$attacheddata = $data[2];
     
    -				$URLSearchString = "^\[\]";
    -
    -				if (preg_match("/\[img\]([$URLSearchString]*)\[\/img\]/ism", $attacheddata, $matches)) {
    +				if (preg_match("/\[img\](.*?)\[\/img\]/ism", $attacheddata, $matches)) {
     
     					$picturedata = Image::getInfoFromURL($matches[1]);
     
    @@ -87,12 +85,12 @@ class BBCode extends BaseObject
     					}
     				}
     
    -				if (preg_match("/\[bookmark\=([$URLSearchString]*)\](.*?)\[\/bookmark\]/ism", $attacheddata, $matches)) {
    +				if (preg_match("/\[bookmark\=(.*?)\](.*?)\[\/bookmark\]/ism", $attacheddata, $matches)) {
     					$post["url"] = $matches[1];
     					$post["title"] = $matches[2];
     				}
     				if (!empty($post["url"]) && (in_array($post["type"], ["link", "video"]))
    -					&& preg_match("/\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", $attacheddata, $matches)) {
    +					&& preg_match("/\[url\=(.*?)\](.*?)\[\/url\]/ism", $attacheddata, $matches)) {
     					$post["url"] = $matches[1];
     				}
     
    @@ -245,14 +243,18 @@ class BBCode extends BaseObject
     			// Simplify image codes
     			$body = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", '[img]$3[/img]', $body);
     
    -			$URLSearchString = "^\[\]";
    +			$body = preg_replace("/\[img\=(.*?)\](.*?)\[\/img\]/ism", '[img]$1[/img]', $body);
     
    -			$body = preg_replace("/\[img\=([$URLSearchString]*)\](.*?)\[\/img\]/ism", '[img]$1[/img]', $body);
    -
    -			if (preg_match_all("(\[url=([$URLSearchString]*)\]\s*\[img\]([$URLSearchString]*)\[\/img\]\s*\[\/url\])ism", $body, $pictures, PREG_SET_ORDER)) {
    +			if (preg_match_all("(\[url=(.*?)\]\s*\[img\](.*?)\[\/img\]\s*\[\/url\])ism", $body, $pictures, PREG_SET_ORDER)) {
     				if ((count($pictures) == 1) && !$has_title) {
    -					// Checking, if the link goes to a picture
    -					$data = ParseUrl::getSiteinfoCached($pictures[0][1], true);
    +					if (!empty($item['object-type']) && ($item['object-type'] == ACTIVITY_OBJ_IMAGE)) {
    +						// Replace the preview picture with the real picture
    +						$url = str_replace('-1.', '-0.', $pictures[0][2]);
    +						$data = ['url' => $url, 'type' => 'photo'];
    +					} else {
    +						// Checking, if the link goes to a picture
    +						$data = ParseUrl::getSiteinfoCached($pictures[0][1], true);
    +					}
     
     					// Workaround:
     					// Sometimes photo posts to the own album are not detected at the start.
    @@ -271,14 +273,14 @@ class BBCode extends BaseObject
     						}
     
     						$post["preview"] = $pictures[0][2];
    -						$post["text"] = str_replace($pictures[0][0], "", $body);
    +						$post["text"] = trim(str_replace($pictures[0][0], "", $body));
     					} else {
     						$imgdata = Image::getInfoFromURL($pictures[0][1]);
     						if ($imgdata && substr($imgdata["mime"], 0, 6) == "image/") {
     							$post["type"] = "photo";
     							$post["image"] = $pictures[0][1];
     							$post["preview"] = $pictures[0][2];
    -							$post["text"] = str_replace($pictures[0][0], "", $body);
    +							$post["text"] = trim(str_replace($pictures[0][0], "", $body));
     						}
     					}
     				} elseif (count($pictures) > 0) {
    @@ -287,7 +289,7 @@ class BBCode extends BaseObject
     					$post["image"] = $pictures[0][2];
     					$post["text"] = $body;
     				}
    -			} elseif (preg_match_all("(\[img\]([$URLSearchString]*)\[\/img\])ism", $body, $pictures, PREG_SET_ORDER)) {
    +			} elseif (preg_match_all("(\[img\](.*?)\[\/img\])ism", $body, $pictures, PREG_SET_ORDER)) {
     				if ((count($pictures) == 1) && !$has_title) {
     					$post["type"] = "photo";
     					$post["image"] = $pictures[0][1];
    @@ -301,8 +303,8 @@ class BBCode extends BaseObject
     			}
     
     			// Test for the external links
    -			preg_match_all("(\[url\]([$URLSearchString]*)\[\/url\])ism", $body, $links1, PREG_SET_ORDER);
    -			preg_match_all("(\[url\=([$URLSearchString]*)\].*?\[\/url\])ism", $body, $links2, PREG_SET_ORDER);
    +			preg_match_all("(\[url\](.*?)\[\/url\])ism", $body, $links1, PREG_SET_ORDER);
    +			preg_match_all("(\[url\=(.*?)\].*?\[\/url\])ism", $body, $links2, PREG_SET_ORDER);
     
     			$links = array_merge($links1, $links2);
     
    @@ -563,7 +565,7 @@ class BBCode extends BaseObject
     		}
     
     		$return = '';
    -		if ($simplehtml == 7) {
    +		if (in_array($simplehtml, [7, 9])) {
     			$return = self::convertUrlForOStatus($data["url"]);
     		} elseif (($simplehtml != 4) && ($simplehtml != 0)) {
     			$return = sprintf('%s
    ', $data["url"], $data["title"]); @@ -979,16 +981,9 @@ class BBCode extends BaseObject $text = ($is_quote_share? '
    ' : '') . '

    ' . html_entity_decode('♲ ', ENT_QUOTES, 'UTF-8') . ' ' . $author_contact['addr'] . ':

    ' . "\n" . $content; break; case 7: // statusnet/GNU Social + case 9: // ActivityPub $text = ($is_quote_share? '
    ' : '') . '

    ' . html_entity_decode('♲ ', ENT_QUOTES, 'UTF-8') . ' @' . $author_contact['addr'] . ': ' . $content . '

    ' . "\n"; break; - case 9: // Google+ - $text = ($is_quote_share? '
    ' : '') . '

    ' . html_entity_decode('♲ ', ENT_QUOTES, 'UTF-8') . ' ' . $author_contact['addr'] . ':

    ' . "\n"; - $text .= '

    ' . $content . '

    ' . "\n"; - - if ($attributes['link'] != '') { - $text .= '

    ' . $attributes['link'] . '

    '; - } - break; default: // Transforms quoted tweets in rich attachments to avoid nested tweets if (stripos(Strings::normaliseLink($attributes['link']), 'http://twitter.com/') === 0 && OEmbed::isAllowedURL($attributes['link'])) { @@ -1140,13 +1135,14 @@ class BBCode extends BaseObject * Simple HTML values meaning: * - 0: Friendica display * - 1: Unused - * - 2: Used for Google+, Windows Phone push, Friendica API + * - 2: Used for Windows Phone push, Friendica API * - 3: Used before converting to Markdown in bb2diaspora.php * - 4: Used for WordPress, Libertree (before Markdown), pump.io and tumblr * - 5: Unused * - 6: Unused * - 7: Used for dfrn, OStatus * - 8: Used for WP backlink text setting + * - 9: ActivityPub * * @param string $text * @param bool $try_oembed @@ -1250,6 +1246,25 @@ class BBCode extends BaseObject $text = trim($text); $text = str_replace("\r\n", "\n", $text); + // Remove linefeeds inside of the table elements. See issue #6799 + $search = ["\n[th]", "[th]\n", " [th]", "\n[/th]", "[/th]\n", "[/th] ", + "\n[td]", "[td]\n", " [td]", "\n[/td]", "[/td]\n", "[/td] ", + "\n[tr]", "[tr]\n", " [tr]", "[tr] ", "\n[/tr]", "[/tr]\n", " [/tr]", "[/tr] ", + "[table]\n", "[table] ", " [table]", "\n[/table]", " [/table]", "[/table] "]; + $replace = ["[th]", "[th]", "[th]", "[/th]", "[/th]", "[/th]", + "[td]", "[td]", "[td]", "[/td]", "[/td]", "[/td]", + "[tr]", "[tr]", "[tr]", "[tr]", "[/tr]", "[/tr]", "[/tr]", "[/tr]", + "[table]", "[table]", "[table]", "[/table]", "[/table]", "[/table]"]; + do { + $oldtext = $text; + $text = str_replace($search, $replace, $text); + } while ($oldtext != $text); + + // Replace these here only once + $search = ["\n[table]", "[/table]\n"]; + $replace = ["[table]", "[/table]"]; + $text = str_replace($search, $replace, $text); + // removing multiplicated newlines if (Config::get("system", "remove_multiplicated_lines")) { $search = ["\n\n\n", "\n ", " \n", "[/quote]\n\n", "\n[/quote]", "[/li]\n", "\n[li]", "\n[ul]", "[/ul]\n", "\n\n[share ", "[/attachment]\n", @@ -1262,131 +1277,11 @@ class BBCode extends BaseObject } while ($oldtext != $text); } - // Set up the parameters for a URL search string - $URLSearchString = "^\[\]"; - // Set up the parameters for a MAIL search string - $MAILSearchString = $URLSearchString; - // Handle attached links or videos $text = self::convertAttachment($text, $simple_html, $try_oembed); - // if the HTML is used to generate plain text, then don't do this search, but replace all URL of that kind to text - if (!$for_plaintext) { - $text = preg_replace(Strings::autoLinkRegEx(), '[url]$1[/url]', $text); - if ($simple_html == 7) { - $text = preg_replace_callback("/\[url\]([$URLSearchString]*)\[\/url\]/ism", 'self::convertUrlForOStatusCallback', $text); - $text = preg_replace_callback("/\[url\=([$URLSearchString]*)\]([$URLSearchString]*)\[\/url\]/ism", 'self::convertUrlForOStatusCallback', $text); - } - } else { - $text = preg_replace("(\[url\]([$URLSearchString]*)\[\/url\])ism", " $1 ", $text); - $text = preg_replace_callback("&\[url=([^\[\]]*)\]\[img\](.*)\[\/img\]\[\/url\]&Usi", 'self::removePictureLinksCallback', $text); - } - - $text = str_replace(["\r","\n"], ['
    ', '
    '], $text); - - // Remove all hashtag addresses - if ((!$try_oembed || $simple_html) && !in_array($simple_html, [3, 7])) { - $text = preg_replace("/([#@!])\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", '$1$3', $text); - } elseif ($simple_html == 3) { - // The ! is converted to @ since Diaspora only understands the @ - $text = preg_replace("/([@!])\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", - '@$3', - $text); - } elseif ($simple_html == 7) { - $text = preg_replace("/([@!])\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", - '$1$3', - $text); - } elseif (!$simple_html) { - $text = preg_replace("/([@!])\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", - '$1$3', - $text); - } - - // Bookmarks in red - will be converted to bookmarks in friendica - $text = preg_replace("/#\^\[url\]([$URLSearchString]*)\[\/url\]/ism", '[bookmark=$1]$1[/bookmark]', $text); - $text = preg_replace("/#\^\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", '[bookmark=$1]$2[/bookmark]', $text); - $text = preg_replace("/#\[url\=[$URLSearchString]*\]\^\[\/url\]\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/i", - "[bookmark=$1]$2[/bookmark]", $text); - - if (in_array($simple_html, [2, 6, 7, 8, 9])) { - $text = preg_replace_callback("/([^#@!])\[url\=([^\]]*)\](.*?)\[\/url\]/ism", "self::expandLinksCallback", $text); - //$Text = preg_replace("/[^#@!]\[url\=([^\]]*)\](.*?)\[\/url\]/ism", ' $2 [url]$1[/url]', $Text); - $text = preg_replace("/\[bookmark\=([^\]]*)\](.*?)\[\/bookmark\]/ism", ' $2 [url]$1[/url]',$text); - } - - if ($simple_html == 5) { - $text = preg_replace("/[^#@!]\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", '[url]$1[/url]', $text); - } - - // Perform URL Search - if ($try_oembed) { - $text = preg_replace_callback("/\[bookmark\=([^\]]*)\](.*?)\[\/bookmark\]/ism", $try_oembed_callback, $text); - } - - if ($simple_html == 5) { - $text = preg_replace("/\[bookmark\=([^\]]*)\](.*?)\[\/bookmark\]/ism", '[url]$1[/url]', $text); - } else { - $text = preg_replace("/\[bookmark\=([^\]]*)\](.*?)\[\/bookmark\]/ism", '[url=$1]$2[/url]', $text); - } - - // Handle Diaspora posts - $text = preg_replace_callback( - "&\[url=/?posts/([^\[\]]*)\](.*)\[\/url\]&Usi", - function ($match) { - return "[url=" . System::baseUrl() . "/display/" . $match[1] . "]" . $match[2] . "[/url]"; - }, $text - ); - - $text = preg_replace_callback( - "&\[url=/people\?q\=(.*)\](.*)\[\/url\]&Usi", - function ($match) { - return "[url=" . System::baseUrl() . "/search?search=%40" . $match[1] . "]" . $match[2] . "[/url]"; - }, $text - ); - - // Server independent link to posts and comments - // See issue: https://github.com/diaspora/diaspora_federation/issues/75 - $expression = "=diaspora://.*?/post/([0-9A-Za-z\-_@.:]{15,254}[0-9A-Za-z])=ism"; - $text = preg_replace($expression, System::baseUrl()."/display/$1", $text); - - /* Tag conversion - * Supports: - * - #[url=][/url] - * - [url=]#[/url] - */ - $text = preg_replace_callback("/(?:#\[url\=[$URLSearchString]*\]|\[url\=[$URLSearchString]*\]#)(.*?)\[\/url\]/ism", function($matches) { - return '#' - . XML::escape($matches[1]) - . ''; - }, $text); - - // We need no target="_blank" for local links - // convert links start with System::baseUrl() as local link without the target="_blank" attribute - $escapedBaseUrl = preg_quote(System::baseUrl(), '/'); - $text = preg_replace("/\[url\](".$escapedBaseUrl."[$URLSearchString]*)\[\/url\]/ism", '$1', $text); - $text = preg_replace("/\[url\=(".$escapedBaseUrl."[$URLSearchString]*)\](.*?)\[\/url\]/ism", '$2', $text); - - $text = preg_replace("/\[url\]([$URLSearchString]*)\[\/url\]/ism", '$1', $text); - $text = preg_replace("/\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", '$2', $text); - - // Red compatibility, though the link can't be authenticated on Friendica - $text = preg_replace("/\[zrl\=([$URLSearchString]*)\](.*?)\[\/zrl\]/ism", '$2', $text); - - - // we may need to restrict this further if it picks up too many strays - // link acct:user@host to a webfinger profile redirector - - $text = preg_replace('/acct:([^@]+)@((?!\-)(?:[a-zA-Z\d\-]{0,62}[a-zA-Z\d]\.){1,126}(?!\d+)[a-zA-Z\d]{1,63})/', 'acct:$1@$2', $text); - - // Perform MAIL Search - $text = preg_replace("/\[mail\]([$MAILSearchString]*)\[\/mail\]/", '$1', $text); - $text = preg_replace("/\[mail\=([$MAILSearchString]*)\](.*?)\[\/mail\]/", '$2', $text); - // leave open the posibility of [map=something] // this is replaced in Item::prepareBody() which has knowledge of the item location - if (strpos($text, '[/map]') !== false) { $text = preg_replace_callback( "/\[map\](.*?)\[\/map\]/ism", @@ -1396,6 +1291,7 @@ class BBCode extends BaseObject $text ); } + if (strpos($text, '[map=') !== false) { $text = preg_replace_callback( "/\[map=(.*?)\]/ism", @@ -1405,6 +1301,7 @@ class BBCode extends BaseObject $text ); } + if (strpos($text, '[map]') !== false) { $text = preg_replace("/\[map\]/", '

    ', $text); } @@ -1471,9 +1368,9 @@ class BBCode extends BaseObject $endlessloop = 0; while ((((strpos($text, "[/list]") !== false) && (strpos($text, "[list") !== false)) || - ((strpos($text, "[/ol]") !== false) && (strpos($text, "[ol]") !== false)) || - ((strpos($text, "[/ul]") !== false) && (strpos($text, "[ul]") !== false)) || - ((strpos($text, "[/li]") !== false) && (strpos($text, "[li]") !== false))) && (++$endlessloop < 20)) { + ((strpos($text, "[/ol]") !== false) && (strpos($text, "[ol]") !== false)) || + ((strpos($text, "[/ul]") !== false) && (strpos($text, "[ul]") !== false)) || + ((strpos($text, "[/li]") !== false) && (strpos($text, "[li]") !== false))) && (++$endlessloop < 20)) { $text = preg_replace("/\[list\](.*?)\[\/list\]/ism", '
      $1
    ', $text); $text = preg_replace("/\[list=\](.*?)\[\/list\]/ism", '
      $1
    ', $text); $text = preg_replace("/\[list=1\](.*?)\[\/list\]/ism", '
      $1
    ', $text); @@ -1521,8 +1418,8 @@ class BBCode extends BaseObject $endlessloop = 0; while ((strpos($text, "[/spoiler]")!== false) && (strpos($text, "[spoiler=") !== false) && (++$endlessloop < 20)) { $text = preg_replace("/\[spoiler=[\"\']*(.*?)[\"\']*\](.*?)\[\/spoiler\]/ism", - "
    " . $t_wrote . "
    $2
    ", - $text); + "
    " . $t_wrote . "
    $2
    ", + $text); } // Declare the format for [quote] layout @@ -1543,8 +1440,8 @@ class BBCode extends BaseObject $endlessloop = 0; while ((strpos($text, "[/quote]")!== false) && (strpos($text, "[quote=") !== false) && (++$endlessloop < 20)) { $text = preg_replace("/\[quote=[\"\']*(.*?)[\"\']*\](.*?)\[\/quote\]/ism", - "

    " . $t_wrote . "

    $2
    ", - $text); + "

    " . $t_wrote . "

    $2
    ", + $text); } @@ -1565,7 +1462,7 @@ class BBCode extends BaseObject $text = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", '', $text); $text = preg_replace("/\[zmg\=([0-9]*)x([0-9]*)\](.*?)\[\/zmg\]/ism", '', $text); - $text = preg_replace_callback("/\[img\=([$URLSearchString]*)\](.*?)\[\/img\]/ism", + $text = preg_replace_callback("/\[img\=(.*?)\](.*?)\[\/img\]/ism", function ($matches) use ($simple_html) { $matches[1] = self::proxyUrl($matches[1], $simple_html); $matches[2] = htmlspecialchars($matches[2], ENT_COMPAT); @@ -1591,14 +1488,6 @@ class BBCode extends BaseObject $text = preg_replace("/\[img\](.*?)\[\/img\]/ism", '' . L10n::t('Image/photo') . '', $text); $text = preg_replace("/\[zmg\](.*?)\[\/zmg\]/ism", '' . L10n::t('Image/photo') . '', $text); - // Shared content - $text = self::convertShare( - $text, - function (array $attributes, array $author_contact, $content, $is_quote_share) use ($simple_html) { - return self::convertShareCallback($attributes, $author_contact, $content, $is_quote_share, $simple_html); - } - ); - $text = preg_replace("/\[crypt\](.*?)\[\/crypt\]/ism", '
    ' . L10n::t('Encrypted content') . '
    ', $text); $text = preg_replace("/\[crypt(.*?)\](.*?)\[\/crypt\]/ism", '
    ' . L10n::t('Encrypted content') . '
    ', $text); //$Text = preg_replace("/\[crypt=(.*?)\](.*?)\[\/crypt\]/ism", '
    ' . L10n::t('Encrypted content') . '
    ', $Text); @@ -1612,9 +1501,9 @@ class BBCode extends BaseObject $text = preg_replace_callback("/\[audio\](.*?)\[\/audio\]/ism", $try_oembed_callback, $text); } else { $text = preg_replace("/\[video\](.*?)\[\/video\]/ism", - '$1', $text); + '$1', $text); $text = preg_replace("/\[audio\](.*?)\[\/audio\]/ism", - '$1', $text); + '$1', $text); } // html5 video and audio @@ -1641,7 +1530,7 @@ class BBCode extends BaseObject $text = preg_replace("/\[youtube\]([A-Za-z0-9\-_=]+)(.*?)\[\/youtube\]/ism", '', $text); } else { $text = preg_replace("/\[youtube\]([A-Za-z0-9\-_=]+)(.*?)\[\/youtube\]/ism", - 'https://www.youtube.com/watch?v=$1', $text); + 'https://www.youtube.com/watch?v=$1', $text); } if ($try_oembed) { @@ -1656,7 +1545,7 @@ class BBCode extends BaseObject $text = preg_replace("/\[vimeo\]([0-9]+)(.*?)\[\/vimeo\]/ism", '', $text); } else { $text = preg_replace("/\[vimeo\]([0-9]+)(.*?)\[\/vimeo\]/ism", - 'https://vimeo.com/$1', $text); + 'https://vimeo.com/$1', $text); } // oembed tag @@ -1687,6 +1576,120 @@ class BBCode extends BaseObject $text = Smilies::replace($text); } + // if the HTML is used to generate plain text, then don't do this search, but replace all URL of that kind to text + if (!$for_plaintext) { + $text = preg_replace(Strings::autoLinkRegEx(), '[url]$1[/url]', $text); + if (in_array($simple_html, [7, 9])) { + $text = preg_replace_callback("/\[url\](.*?)\[\/url\]/ism", 'self::convertUrlForOStatusCallback', $text); + $text = preg_replace_callback("/\[url\=(.*?)\](.*?)\[\/url\]/ism", 'self::convertUrlForOStatusCallback', $text); + } + } else { + $text = preg_replace("(\[url\](.*?)\[\/url\])ism", " $1 ", $text); + $text = preg_replace_callback("&\[url=([^\[\]]*)\]\[img\](.*)\[\/img\]\[\/url\]&Usi", 'self::removePictureLinksCallback', $text); + } + + $text = str_replace(["\r","\n"], ['
    ', '
    '], $text); + + // Remove all hashtag addresses + if ((!$try_oembed || $simple_html) && !in_array($simple_html, [3, 7, 9])) { + $text = preg_replace("/([#@!])\[url\=(.*?)\](.*?)\[\/url\]/ism", '$1$3', $text); + } elseif ($simple_html == 3) { + // The ! is converted to @ since Diaspora only understands the @ + $text = preg_replace("/([@!])\[url\=(.*?)\](.*?)\[\/url\]/ism", + '@$3', + $text); + } elseif (in_array($simple_html, [7, 9])) { + $text = preg_replace("/([@!])\[url\=(.*?)\](.*?)\[\/url\]/ism", + '$1$3', + $text); + } elseif (!$simple_html) { + $text = preg_replace("/([@!])\[url\=(.*?)\](.*?)\[\/url\]/ism", + '$1$3', + $text); + } + + // Bookmarks in red - will be converted to bookmarks in friendica + $text = preg_replace("/#\^\[url\](.*?)\[\/url\]/ism", '[bookmark=$1]$1[/bookmark]', $text); + $text = preg_replace("/#\^\[url\=(.*?)\](.*?)\[\/url\]/ism", '[bookmark=$1]$2[/bookmark]', $text); + $text = preg_replace("/#\[url\=.*?\]\^\[\/url\]\[url\=(.*?)\](.*?)\[\/url\]/i", + "[bookmark=$1]$2[/bookmark]", $text); + + if (in_array($simple_html, [2, 6, 7, 8])) { + $text = preg_replace_callback("/([^#@!])\[url\=([^\]]*)\](.*?)\[\/url\]/ism", "self::expandLinksCallback", $text); + //$Text = preg_replace("/[^#@!]\[url\=([^\]]*)\](.*?)\[\/url\]/ism", ' $2 [url]$1[/url]', $Text); + $text = preg_replace("/\[bookmark\=([^\]]*)\](.*?)\[\/bookmark\]/ism", ' $2 [url]$1[/url]',$text); + } + + if ($simple_html == 5) { + $text = preg_replace("/[^#@!]\[url\=(.*?)\](.*?)\[\/url\]/ism", '[url]$1[/url]', $text); + } + + // Perform URL Search + if ($try_oembed) { + $text = preg_replace_callback("/\[bookmark\=([^\]]*)\](.*?)\[\/bookmark\]/ism", $try_oembed_callback, $text); + } + + if ($simple_html == 5) { + $text = preg_replace("/\[bookmark\=([^\]]*)\](.*?)\[\/bookmark\]/ism", '[url]$1[/url]', $text); + } else { + $text = preg_replace("/\[bookmark\=([^\]]*)\](.*?)\[\/bookmark\]/ism", '[url=$1]$2[/url]', $text); + } + + // Handle Diaspora posts + $text = preg_replace_callback( + "&\[url=/?posts/([^\[\]]*)\](.*)\[\/url\]&Usi", + function ($match) { + return "[url=" . System::baseUrl() . "/display/" . $match[1] . "]" . $match[2] . "[/url]"; + }, $text + ); + + $text = preg_replace_callback( + "&\[url=/people\?q\=(.*)\](.*)\[\/url\]&Usi", + function ($match) { + return "[url=" . System::baseUrl() . "/search?search=%40" . $match[1] . "]" . $match[2] . "[/url]"; + }, $text + ); + + // Server independent link to posts and comments + // See issue: https://github.com/diaspora/diaspora_federation/issues/75 + $expression = "=diaspora://.*?/post/([0-9A-Za-z\-_@.:]{15,254}[0-9A-Za-z])=ism"; + $text = preg_replace($expression, System::baseUrl()."/display/$1", $text); + + /* Tag conversion + * Supports: + * - #[url=][/url] + * - [url=]#[/url] + */ + $text = preg_replace_callback("/(?:#\[url\=.*?\]|\[url\=.*?\]#)(.*?)\[\/url\]/ism", function($matches) { + return '#' + . XML::escape($matches[1]) + . ''; + }, $text); + + // We need no target="_blank" for local links + // convert links start with System::baseUrl() as local link without the target="_blank" attribute + $escapedBaseUrl = preg_quote(System::baseUrl(), '/'); + $text = preg_replace("/\[url\](".$escapedBaseUrl.".*?)\[\/url\]/ism", '$1', $text); + $text = preg_replace("/\[url\=(".$escapedBaseUrl.".*?)\](.*?)\[\/url\]/ism", '$2', $text); + + $text = preg_replace("/\[url\](.*?)\[\/url\]/ism", '$1', $text); + $text = preg_replace("/\[url\=(.*?)\](.*?)\[\/url\]/ism", '$2', $text); + + // Red compatibility, though the link can't be authenticated on Friendica + $text = preg_replace("/\[zrl\=(.*?)\](.*?)\[\/zrl\]/ism", '$2', $text); + + + // we may need to restrict this further if it picks up too many strays + // link acct:user@host to a webfinger profile redirector + + $text = preg_replace('/acct:([^@]+)@((?!\-)(?:[a-zA-Z\d\-]{0,62}[a-zA-Z\d]\.){1,126}(?!\d+)[a-zA-Z\d]{1,63})/', 'acct:$1@$2', $text); + + // Perform MAIL Search + $text = preg_replace("/\[mail\](.*?)\[\/mail\]/", '$1', $text); + $text = preg_replace("/\[mail\=(.*?)\](.*?)\[\/mail\]/", '$2', $text); + // Unhide all [noparse] contained bbtags unspacefying them // and triming the [noparse] tag. @@ -1720,6 +1723,14 @@ class BBCode extends BaseObject $regex = '#<([^>]*?)(href)="(?!' . implode('|', $allowed_link_protocols) . ')(.*?)"(.*?)>#ism'; $text = preg_replace($regex, '<$1$2="javascript:void(0)"$4 data-original-href="$3" class="invalid-href" title="' . L10n::t('Invalid link protocol') . '">', $text); + // Shared content + $text = self::convertShare( + $text, + function (array $attributes, array $author_contact, $content, $is_quote_share) use ($simple_html) { + return self::convertShareCallback($attributes, $author_contact, $content, $is_quote_share, $simple_html); + } + ); + if ($saved_image) { $text = self::interpolateSavedImagesIntoItemBody($text, $saved_image); } diff --git a/src/Content/Widget.php b/src/Content/Widget.php index b7fa5ab4e..dcfc1d0e3 100644 --- a/src/Content/Widget.php +++ b/src/Content/Widget.php @@ -15,9 +15,12 @@ use Friendica\Database\DBA; use Friendica\Model\Contact; use Friendica\Model\FileTag; use Friendica\Model\GContact; +use Friendica\Model\Item; use Friendica\Model\Profile; +use Friendica\Util\DateTimeFormat; use Friendica\Util\Proxy as ProxyUtils; use Friendica\Util\Strings; +use Friendica\Util\Temporal; use Friendica\Util\XML; class Widget @@ -31,7 +34,7 @@ class Widget */ public static function follow($value = "") { - return Renderer::replaceMacros(Renderer::getMarkupTemplate('follow.tpl'), array( + return Renderer::replaceMacros(Renderer::getMarkupTemplate('widget/follow.tpl'), array( '$connect' => L10n::t('Add New Contact'), '$desc' => L10n::t('Enter address or web location'), '$hint' => L10n::t('Example: bob@example.com, http://example.com/barbara'), @@ -74,7 +77,7 @@ class Widget $aside = []; $aside['$nv'] = $nv; - return Renderer::replaceMacros(Renderer::getMarkupTemplate('peoplefind.tpl'), $aside); + return Renderer::replaceMacros(Renderer::getMarkupTemplate('widget/peoplefind.tpl'), $aside); } /** @@ -120,6 +123,88 @@ class Widget return $network_filter; } + /** + * Display a generic filter widget based on a list of options + * + * The options array must be the following format: + * [ + * [ + * 'ref' => {filter value}, + * 'name' => {option name} + * ], + * ... + * ] + * + * @param string $type The filter query string key + * @param string $title + * @param string $desc + * @param string $all The no filter label + * @param string $baseUrl The full page request URI + * @param array $options + * @param string $selected The currently selected filter option value + * @return string + * @throws \Exception + */ + private static function filter($type, $title, $desc, $all, $baseUrl, array $options, $selected = null) + { + $queryString = parse_url($baseUrl, PHP_URL_QUERY); + $queryArray = []; + + if ($queryString) { + parse_str($queryString, $queryArray); + unset($queryArray[$type]); + + if (count($queryArray)) { + $baseUrl = substr($baseUrl, 0, strpos($baseUrl, '?')) . '?' . http_build_query($queryArray) . '&'; + } else { + $baseUrl = substr($baseUrl, 0, strpos($baseUrl, '?')) . '?'; + } + } else { + $baseUrl = trim($baseUrl, '?') . '?'; + } + + return Renderer::replaceMacros(Renderer::getMarkupTemplate('widget/filter.tpl'), [ + '$type' => $type, + '$title' => $title, + '$desc' => $desc, + '$selected' => $selected, + '$all_label' => $all, + '$options' => $options, + '$base' => $baseUrl, + ]); + } + + /** + * Return networks widget + * + * @param string $baseurl baseurl + * @param string $selected optional, default empty + * @return string + * @throws \Exception + */ + public static function contactRels($baseurl, $selected = '') + { + if (!local_user()) { + return ''; + } + + $options = [ + ['ref' => 'followers', 'name' => L10n::t('Followers')], + ['ref' => 'following', 'name' => L10n::t('Following')], + ['ref' => 'mutuals', 'name' => L10n::t('Mutual friends')], + ]; + + return self::filter( + 'rel', + L10n::t('Relationships'), + '', + L10n::t('All Contacts'), + $baseurl, + $options, + $selected + ); + } + /** * Return networks widget * @@ -146,7 +231,7 @@ class Widget $nets = array(); while ($rr = DBA::fetch($r)) { - $nets[] = array('ref' => $rr['network'], 'name' => ContactSelector::networkToName($rr['network']), 'selected' => (($selected == $rr['network']) ? 'selected' : '' )); + $nets[] = ['ref' => $rr['network'], 'name' => ContactSelector::networkToName($rr['network'])]; } DBA::close($r); @@ -154,14 +239,15 @@ class Widget return ''; } - return Renderer::replaceMacros(Renderer::getMarkupTemplate('nets.tpl'), array( - '$title' => L10n::t('Protocols'), - '$desc' => '', - '$sel_all' => (($selected == '') ? 'selected' : ''), - '$all' => L10n::t('All Protocols'), - '$nets' => $nets, - '$base' => $baseurl, - )); + return self::filter( + 'nets', + L10n::t('Protocols'), + '', + L10n::t('All Protocols'), + $baseurl, + $nets, + $selected + ); } /** @@ -170,7 +256,7 @@ class Widget * @param string $baseurl baseurl * @param string $selected optional, default empty * @return string|void - * @throws \Friendica\Network\HTTPException\InternalServerErrorException + * @throws \Exception */ public static function fileAs($baseurl, $selected = '') { @@ -183,25 +269,20 @@ class Widget return; } - $matches = false; - $terms = array(); - $cnt = preg_match_all('/\[(.*?)\]/', $saved, $matches, PREG_SET_ORDER); - if ($cnt) { - foreach ($matches as $mtch) - { - $unescaped = XML::escape(FileTag::decode($mtch[1])); - $terms[] = array('name' => $unescaped, 'selected' => (($selected == $unescaped) ? 'selected' : '')); - } + $terms = []; + foreach (FileTag::fileToArray($saved) as $savedFolderName) { + $terms[] = ['ref' => $savedFolderName, 'name' => $savedFolderName]; } - return Renderer::replaceMacros(Renderer::getMarkupTemplate('fileas_widget.tpl'), array( - '$title' => L10n::t('Saved Folders'), - '$desc' => '', - '$sel_all' => (($selected == '') ? 'selected' : ''), - '$all' => L10n::t('Everything'), - '$terms' => $terms, - '$base' => $baseurl, - )); + return self::filter( + 'file', + L10n::t('Saved Folders'), + '', + L10n::t('Everything'), + $baseurl, + $terms, + $selected + ); } /** @@ -225,25 +306,20 @@ class Widget return; } - $matches = false; $terms = array(); - $cnt = preg_match_all('/<(.*?)>/', $saved, $matches, PREG_SET_ORDER); - - if ($cnt) { - foreach ($matches as $mtch) { - $unescaped = XML::escape(FileTag::decode($mtch[1])); - $terms[] = array('name' => $unescaped, 'selected' => (($selected == $unescaped) ? 'selected' : '')); - } + foreach (FileTag::fileToArray($saved, 'category') as $savedFolderName) { + $terms[] = ['ref' => $savedFolderName, 'name' => $savedFolderName]; } - return Renderer::replaceMacros(Renderer::getMarkupTemplate('categories_widget.tpl'), array( - '$title' => L10n::t('Categories'), - '$desc' => '', - '$sel_all' => (($selected == '') ? 'selected' : ''), - '$all' => L10n::t('Everything'), - '$terms' => $terms, - '$base' => $baseurl, - )); + return self::filter( + 'category', + L10n::t('Categories'), + '', + L10n::t('Everything'), + $baseurl, + $terms, + $selected + ); } /** @@ -319,7 +395,7 @@ class Widget $entries[] = $entry; } - $tpl = Renderer::getMarkupTemplate('remote_friends_common.tpl'); + $tpl = Renderer::getMarkupTemplate('widget/remote_friends_common.tpl'); return Renderer::replaceMacros($tpl, [ '$desc' => L10n::tt("%d contact in common", "%d contacts in common", $t), '$base' => System::baseUrl(), @@ -359,4 +435,74 @@ class Widget return ''; } + + /** + * @param string $url Base page URL + * @param int $uid User ID consulting/publishing posts + * @param bool $wall True: Posted by User; False: Posted to User (network timeline) + * @return string + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + */ + public static function postedByYear(string $url, int $uid, bool $wall) + { + $o = ''; + + if (!Feature::isEnabled($uid, 'archives')) { + return $o; + } + + $visible_years = PConfig::get($uid, 'system', 'archive_visible_years', 5); + + /* arrange the list in years */ + $dnow = DateTimeFormat::localNow('Y-m-d'); + + $ret = []; + + $dthen = Item::firstPostDate($uid, $wall); + if ($dthen) { + // Set the start and end date to the beginning of the month + $dnow = substr($dnow, 0, 8) . '01'; + $dthen = substr($dthen, 0, 8) . '01'; + + /* + * Starting with the current month, get the first and last days of every + * month down to and including the month of the first post + */ + while (substr($dnow, 0, 7) >= substr($dthen, 0, 7)) { + $dyear = intval(substr($dnow, 0, 4)); + $dstart = substr($dnow, 0, 8) . '01'; + $dend = substr($dnow, 0, 8) . Temporal::getDaysInMonth(intval($dnow), intval(substr($dnow, 5))); + $start_month = DateTimeFormat::utc($dstart, 'Y-m-d'); + $end_month = DateTimeFormat::utc($dend, 'Y-m-d'); + $str = L10n::getDay(DateTimeFormat::utc($dnow, 'F')); + + if (empty($ret[$dyear])) { + $ret[$dyear] = []; + } + + $ret[$dyear][] = [$str, $end_month, $start_month]; + $dnow = DateTimeFormat::utc($dnow . ' -1 month', 'Y-m-d'); + } + } + + if (!DBA::isResult($ret)) { + return $o; + } + + + $cutoff_year = intval(DateTimeFormat::localNow('Y')) - $visible_years; + $cutoff = array_key_exists($cutoff_year, $ret); + + $o = Renderer::replaceMacros(Renderer::getMarkupTemplate('widget/posted_date.tpl'),[ + '$title' => L10n::t('Archives'), + '$size' => $visible_years, + '$cutoff_year' => $cutoff_year, + '$cutoff' => $cutoff, + '$url' => $url, + '$dates' => $ret, + '$showmore' => L10n::t('show more') + ]); + + return $o; + } } diff --git a/src/Content/Widget/CalendarExport.php b/src/Content/Widget/CalendarExport.php index 120af99d0..84482f638 100644 --- a/src/Content/Widget/CalendarExport.php +++ b/src/Content/Widget/CalendarExport.php @@ -59,7 +59,7 @@ class CalendarExport // of the profile page it should be the personal /events page. So we can use $a->user. $user = defaults($a->data['user'], 'nickname', $a->user['nickname']); - $tpl = Renderer::getMarkupTemplate("events_aside.tpl"); + $tpl = Renderer::getMarkupTemplate("widget/events.tpl"); $return = Renderer::replaceMacros($tpl, [ '$etitle' => L10n::t("Export"), '$export_ical' => L10n::t("Export calendar as ical"), diff --git a/src/Content/Widget/ContactBlock.php b/src/Content/Widget/ContactBlock.php index 6c77e7f3f..bc33b9c9c 100644 --- a/src/Content/Widget/ContactBlock.php +++ b/src/Content/Widget/ContactBlock.php @@ -26,7 +26,7 @@ class ContactBlock /** * Get HTML for contact block * - * @template contact_block.tpl + * @template widget/contacts.tpl * @hook contact_block_end (contacts=>array, output=>string) * @return string */ @@ -52,7 +52,7 @@ class ContactBlock 'pending' => false, 'hidden' => false, 'archive' => false, - 'network' => [Protocol::DFRN, Protocol::ACTIVITYPUB, Protocol::OSTATUS, Protocol::DIASPORA], + 'network' => [Protocol::DFRN, Protocol::ACTIVITYPUB, Protocol::OSTATUS, Protocol::DIASPORA, Protocol::FEED], ]); $contacts_title = L10n::t('No contacts'); @@ -102,7 +102,7 @@ class ContactBlock DBA::close($contact_ids_stmt); } - $tpl = Renderer::getMarkupTemplate('contact_block.tpl'); + $tpl = Renderer::getMarkupTemplate('widget/contacts.tpl'); $o = Renderer::replaceMacros($tpl, [ '$contacts' => $contacts_title, '$nickname' => $profile['nickname'], diff --git a/src/Content/Widget/TagCloud.php b/src/Content/Widget/TagCloud.php index 8413eccd1..bbf8c3885 100644 --- a/src/Content/Widget/TagCloud.php +++ b/src/Content/Widget/TagCloud.php @@ -49,7 +49,7 @@ class TagCloud $tags[] = $tag; } - $tpl = Renderer::getMarkupTemplate('tagblock_widget.tpl'); + $tpl = Renderer::getMarkupTemplate('widget/tagcloud.tpl'); $o = Renderer::replaceMacros($tpl, [ '$title' => L10n::t('Tags'), '$tags' => $tags diff --git a/src/Core/ACL.php b/src/Core/ACL.php index 19015714e..ec31ddb7c 100644 --- a/src/Core/ACL.php +++ b/src/Core/ACL.php @@ -259,21 +259,19 @@ class ACL extends BaseObject * @return string * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - public static function getFullSelectorHTML(array $user, $show_jotnets = false, array $default_permissions = []) + public static function getFullSelectorHTML(array $user = null, $show_jotnets = false, array $default_permissions = []) { // Defaults user permissions if (empty($default_permissions)) { $default_permissions = self::getDefaultUserPermissions($user); } - $jotnets = ''; + $jotnets_fields = []; if ($show_jotnets) { - $imap_disabled = !function_exists('imap_open') || Config::get('system', 'imap_disabled'); - $mail_enabled = false; $pubmail_enabled = false; - if (!$imap_disabled) { + if (function_exists('imap_open') && !Config::get('system', 'imap_disabled')) { $mailacct = DBA::selectFirst('mailacct', ['pubmail'], ['`uid` = ? AND `server` != ""', local_user()]); if (DBA::isResult($mailacct)) { $mail_enabled = true; @@ -283,17 +281,20 @@ class ACL extends BaseObject if (empty($default_permissions['hidewall'])) { if ($mail_enabled) { - $selected = $pubmail_enabled ? ' checked="checked"' : ''; - $jotnets .= '
    ' . L10n::t("Post to Email") . '
    '; + $jotnets_fields[] = [ + 'type' => 'checkbox', + 'field' => [ + 'pubmail_enable', + L10n::t('Post to Email'), + $pubmail_enabled + ] + ]; } - Hook::callAll('jot_networks', $jotnets); - } else { - $jotnets .= L10n::t('Connectors disabled, since "%s" is enabled.', - L10n::t('Hide your profile details from unknown viewers?')); + Hook::callAll('jot_networks', $jotnets_fields); } } - + $tpl = Renderer::getMarkupTemplate('acl_selector.tpl'); $o = Renderer::replaceMacros($tpl, [ '$showall' => L10n::t('Visible to everybody'), @@ -306,11 +307,14 @@ class ACL extends BaseObject '$networks' => $show_jotnets, '$emailcc' => L10n::t('CC: email addresses'), '$emtitle' => L10n::t('Example: bob@example.com, mary@example.com'), - '$jotnets' => $jotnets, + '$jotnets_enabled' => empty($default_permissions['hidewall']), + '$jotnets_summary' => L10n::t('Connectors'), + '$jotnets_fields' => $jotnets_fields, + '$jotnets_disabled_label' => L10n::t('Connectors disabled, since "%s" is enabled.', L10n::t('Hide your profile details from unknown viewers?')), '$aclModalTitle' => L10n::t('Permissions'), '$aclModalDismiss' => L10n::t('Close'), '$features' => [ - 'aclautomention' => Feature::isEnabled($user['uid'], 'aclautomention') ? 'true' : 'false' + 'aclautomention' => !empty($user['uid']) && Feature::isEnabled($user['uid'], 'aclautomention') ? 'true' : 'false' ], ]); diff --git a/src/Core/Addon.php b/src/Core/Addon.php index 7957e0835..2ec46b71c 100644 --- a/src/Core/Addon.php +++ b/src/Core/Addon.php @@ -2,10 +2,12 @@ /** * @file src/Core/Addon.php */ + namespace Friendica\Core; use Friendica\BaseObject; use Friendica\Database\DBA; +use Friendica\Util\Strings; /** * Some functions to handle addons @@ -25,6 +27,61 @@ class Addon extends BaseObject */ private static $addons = []; + /** + * Returns the list of available addons with their current status and info. + * This list is made from scanning the addon/ folder. + * Unsupported addons are excluded unless they already are enabled or system.show_unsupported_addon is set. + * + * @return array + * @throws \Exception + */ + public static function getAvailableList() + { + $addons = []; + $files = glob('addon/*/'); + if (is_array($files)) { + foreach ($files as $file) { + if (is_dir($file)) { + list($tmp, $addon) = array_map('trim', explode('/', $file)); + $info = self::getInfo($addon); + + if (Config::get('system', 'show_unsupported_addons') + || strtolower($info['status']) != 'unsupported' + || self::isEnabled($addon) + ) { + $addons[] = [$addon, (self::isEnabled($addon) ? 'on' : 'off'), $info]; + } + } + } + } + + return $addons; + } + + /** + * Returns a list of addons that can be configured at the node level. + * The list is formatted for display in the admin panel aside. + * + * @return array + * @throws \Exception + */ + public static function getAdminList() + { + $addons_admin = []; + $addonsAdminStmt = DBA::select('addon', ['name'], ['plugin_admin' => 1], ['order' => ['name']]); + while ($addon = DBA::fetch($addonsAdminStmt)) { + $addons_admin[$addon['name']] = [ + 'url' => 'admin/addons/' . $addon['name'], + 'name' => $addon['name'], + 'class' => 'addon' + ]; + } + DBA::close($addonsAdminStmt); + + return $addons_admin; + } + + /** * @brief Synchronize addons: * @@ -81,6 +138,8 @@ class Addon extends BaseObject */ public static function uninstall($addon) { + $addon = Strings::sanitizeFilePathItem($addon); + Logger::notice("Addon {addon}: {action}", ['action' => 'uninstall', 'addon' => $addon]); DBA::delete('addon', ['name' => $addon]); @@ -90,7 +149,11 @@ class Addon extends BaseObject $func(); } + DBA::delete('hook', ['file' => 'addon/' . $addon . '/' . $addon . '.php']); + unset(self::$addons[array_search($addon, self::$addons)]); + + Addon::saveEnabledList(); } /** @@ -102,11 +165,13 @@ class Addon extends BaseObject */ public static function install($addon) { - // silently fail if addon was removed + $addon = Strings::sanitizeFilePathItem($addon); + // silently fail if addon was removed of if $addon is funky if (!file_exists('addon/' . $addon . '/' . $addon . '.php')) { return false; } + Logger::notice("Addon {addon}: {action}", ['action' => 'install', 'addon' => $addon]); $t = @filemtime('addon/' . $addon . '/' . $addon . '.php'); @include_once('addon/' . $addon . '/' . $addon . '.php'); @@ -130,9 +195,12 @@ class Addon extends BaseObject if (!self::isEnabled($addon)) { self::$addons[] = $addon; } + + Addon::saveEnabledList(); + return true; } else { - Logger::error("Addon {addon}: {action} failed", ['action' => 'uninstall', 'addon' => $addon]); + Logger::error("Addon {addon}: {action} failed", ['action' => 'install', 'addon' => $addon]); return false; } } @@ -153,29 +221,26 @@ class Addon extends BaseObject $addon_list = explode(',', $addons); - if (count($addon_list)) { - foreach ($addon_list as $addon) { - $addon = trim($addon); - $fname = 'addon/' . $addon . '/' . $addon . '.php'; + foreach ($addon_list as $addon) { + $addon = Strings::sanitizeFilePathItem(trim($addon)); + $fname = 'addon/' . $addon . '/' . $addon . '.php'; + if (file_exists($fname)) { + $t = @filemtime($fname); + foreach ($installed as $i) { + if (($i['name'] == $addon) && ($i['timestamp'] != $t)) { - if (file_exists($fname)) { - $t = @filemtime($fname); - foreach ($installed as $i) { - if (($i['name'] == $addon) && ($i['timestamp'] != $t)) { + Logger::notice("Addon {addon}: {action}", ['action' => 'reload', 'addon' => $i['name']]); + @include_once($fname); - Logger::notice("Addon {addon}: {action}", ['action' => 'reload', 'addon' => $i['name']]); - @include_once($fname); - - if (function_exists($addon . '_uninstall')) { - $func = $addon . '_uninstall'; - $func(self::getApp()); - } - if (function_exists($addon . '_install')) { - $func = $addon . '_install'; - $func(self::getApp()); - } - DBA::update('addon', ['timestamp' => $t], ['id' => $i['id']]); + if (function_exists($addon . '_uninstall')) { + $func = $addon . '_uninstall'; + $func(self::getApp()); } + if (function_exists($addon . '_install')) { + $func = $addon . '_install'; + $func(self::getApp()); + } + DBA::update('addon', ['timestamp' => $t], ['id' => $i['id']]); } } } @@ -204,6 +269,8 @@ class Addon extends BaseObject { $a = self::getApp(); + $addon = Strings::sanitizeFilePathItem($addon); + $info = [ 'name' => $addon, 'description' => "", @@ -278,11 +345,10 @@ class Addon extends BaseObject * Saves the current enabled addon list in the system.addon config key * * @return boolean - * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ public static function saveEnabledList() { - return Config::set("system", "addon", implode(", ", self::$addons)); + return Config::set('system', 'addon', implode(',', self::$addons)); } /** diff --git a/src/Core/Authentication.php b/src/Core/Authentication.php index 106ba2a60..646729c43 100644 --- a/src/Core/Authentication.php +++ b/src/Core/Authentication.php @@ -5,10 +5,9 @@ namespace Friendica\Core; +use Friendica\App; use Friendica\BaseObject; -use Friendica\Database\DBA; -use Friendica\Model\User; -use Friendica\Util\DateTimeFormat; +use Friendica\Util\BaseURL; /** * Handle Authentification, Session and Cookies @@ -51,113 +50,7 @@ class Authentication extends BaseObject $value = ""; } - setcookie("Friendica", $value, $time, "/", "", (Config::get('system', 'ssl_policy') == SSL_POLICY_FULL), true); - } - - /** - * @brief Sets the provided user's authenticated session - * - * @todo Should be moved to Friendica\Core\Session once it's created - * - * @param array $user_record - * @param bool $login_initial - * @param bool $interactive - * @param bool $login_refresh - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - */ - public static function setAuthenticatedSessionForUser($user_record, $login_initial = false, $interactive = false, $login_refresh = false) - { - $a = self::getApp(); - - $_SESSION['uid'] = $user_record['uid']; - $_SESSION['theme'] = $user_record['theme']; - $_SESSION['mobile-theme'] = PConfig::get($user_record['uid'], 'system', 'mobile_theme'); - $_SESSION['authenticated'] = 1; - $_SESSION['page_flags'] = $user_record['page-flags']; - $_SESSION['my_url'] = $a->getbaseUrl() . '/profile/' . $user_record['nickname']; - $_SESSION['my_address'] = $user_record['nickname'] . '@' . substr($a->getbaseUrl(), strpos($a->getbaseUrl(), '://') + 3); - $_SESSION['addr'] = defaults($_SERVER, 'REMOTE_ADDR', '0.0.0.0'); - - $a->user = $user_record; - - if ($interactive) { - if ($a->user['login_date'] <= DBA::NULL_DATETIME) { - $_SESSION['return_path'] = 'profile_photo/new'; - $a->module = 'profile_photo'; - info(L10n::t("Welcome ") . $a->user['username'] . EOL); - info(L10n::t('Please upload a profile photo.') . EOL); - } else { - info(L10n::t("Welcome back ") . $a->user['username'] . EOL); - } - } - - $member_since = strtotime($a->user['register_date']); - if (time() < ($member_since + ( 60 * 60 * 24 * 14))) { - $_SESSION['new_member'] = true; - } else { - $_SESSION['new_member'] = false; - } - if (strlen($a->user['timezone'])) { - date_default_timezone_set($a->user['timezone']); - $a->timezone = $a->user['timezone']; - } - - $masterUid = $user_record['uid']; - - if (!empty($_SESSION['submanage'])) { - $user = DBA::selectFirst('user', ['uid'], ['uid' => $_SESSION['submanage']]); - if (DBA::isResult($user)) { - $masterUid = $user['uid']; - } - } - - $a->identities = User::identities($masterUid); - - if ($login_initial) { - Logger::log('auth_identities: ' . print_r($a->identities, true), Logger::DEBUG); - } - if ($login_refresh) { - Logger::log('auth_identities refresh: ' . print_r($a->identities, true), Logger::DEBUG); - } - - $contact = DBA::selectFirst('contact', [], ['uid' => $_SESSION['uid'], 'self' => true]); - if (DBA::isResult($contact)) { - $a->contact = $contact; - $a->cid = $contact['id']; - $_SESSION['cid'] = $a->cid; - } - - header('X-Account-Management-Status: active; name="' . $a->user['username'] . '"; id="' . $a->user['nickname'] . '"'); - - if ($login_initial || $login_refresh) { - DBA::update('user', ['login_date' => DateTimeFormat::utcNow()], ['uid' => $_SESSION['uid']]); - - // Set the login date for all identities of the user - DBA::update('user', ['login_date' => DateTimeFormat::utcNow()], - ['parent-uid' => $masterUid, 'account_removed' => false]); - } - - if ($login_initial) { - /* - * If the user specified to remember the authentication, then set a cookie - * that expires after one week (the default is when the browser is closed). - * The cookie will be renewed automatically. - * The week ensures that sessions will expire after some inactivity. - */ - if (!empty($_SESSION['remember'])) { - Logger::log('Injecting cookie for remembered user ' . $a->user['nickname']); - self::setCookie(604800, $user_record); - unset($_SESSION['remember']); - } - } - - if ($login_initial) { - Hook::callAll('logged_in', $a->user); - - if (($a->module !== 'home') && isset($_SESSION['return_path'])) { - $a->internalRedirect($_SESSION['return_path']); - } - } + setcookie("Friendica", $value, $time, "/", "", (Config::get('system', 'ssl_policy') == BaseUrl::SSL_POLICY_FULL), true); } /** @@ -169,5 +62,26 @@ class Authentication extends BaseObject session_unset(); session_destroy(); } + + public static function twoFactorCheck($uid, App $a) + { + // Check user setting, if 2FA disabled return + if (!PConfig::get($uid, '2fa', 'verified')) { + return; + } + + // Check current path, if 2fa authentication module return + if ($a->argc > 0 && in_array($a->argv[0], ['ping', '2fa', 'view', 'help', 'api', 'proxy', 'logout'])) { + return; + } + + // Case 1: 2FA session present and valid: return + if (Session::get('2fa')) { + return; + } + + // Case 2: No valid 2FA session: redirect to code verification page + $a->internalRedirect('2fa'); + } } diff --git a/src/Core/Cache.php b/src/Core/Cache.php index cadb2444b..7a8f7367e 100644 --- a/src/Core/Cache.php +++ b/src/Core/Cache.php @@ -4,7 +4,7 @@ */ namespace Friendica\Core; -use Friendica\Core\Cache\CacheDriverFactory; +use Friendica\Factory\CacheDriverFactory; /** * @brief Class for storing data for a short time diff --git a/src/Core/Cache/APCuCache.php b/src/Core/Cache/APCuCache.php new file mode 100644 index 000000000..f658424cd --- /dev/null +++ b/src/Core/Cache/APCuCache.php @@ -0,0 +1,154 @@ + + */ +class APCuCache extends AbstractCacheDriver implements IMemoryCacheDriver +{ + use TraitCompareSet; + use TraitCompareDelete; + + /** + * @throws Exception + */ + public function __construct() + { + if (!self::isAvailable()) { + throw new Exception('APCu is not available.'); + } + } + + /** + * (@inheritdoc) + */ + public function getAllKeys($prefix = null) + { + $ns = $this->getCacheKey($prefix); + $ns = preg_quote($ns, '/'); + + if (class_exists('\APCIterator')) { + $iterator = new \APCIterator('user', '/^' . $ns. '/', APC_ITER_KEY); + } else { + $iterator = new \APCUIterator('/^' . $ns . '/', APC_ITER_KEY); + } + + $keys = []; + foreach ($iterator as $item) { + array_push($keys, $item['key']); + } + + return $this->getOriginalKeys($keys); + } + + /** + * (@inheritdoc) + */ + public function get($key) + { + $return = null; + $cachekey = $this->getCacheKey($key); + + $cached = apcu_fetch($cachekey, $success); + if (!$success) { + return null; + } + + $value = unserialize($cached); + + // Only return a value if the serialized value is valid. + // We also check if the db entry is a serialized + // boolean 'false' value (which we want to return). + if ($cached === serialize(false) || $value !== false) { + $return = $value; + } + + return $return; + } + + /** + * (@inheritdoc) + */ + public function set($key, $value, $ttl = Cache::FIVE_MINUTES) + { + $cachekey = $this->getCacheKey($key); + + $cached = serialize($value); + + if ($ttl > 0) { + return apcu_store( + $cachekey, + $cached, + $ttl + ); + } else { + return apcu_store( + $cachekey, + $cached + ); + } + } + + /** + * (@inheritdoc) + */ + public function delete($key) + { + $cachekey = $this->getCacheKey($key); + return apcu_delete($cachekey); + } + + /** + * (@inheritdoc) + */ + public function clear($outdated = true) + { + if ($outdated) { + return true; + } else { + $prefix = $this->getPrefix(); + $prefix = preg_quote($prefix, '/'); + + if (class_exists('\APCIterator')) { + $iterator = new \APCIterator('user', '/^' . $prefix . '/', APC_ITER_KEY); + } else { + $iterator = new \APCUIterator('/^' . $prefix . '/', APC_ITER_KEY); + } + + return apcu_delete($iterator); + } + } + + /** + * (@inheritdoc) + */ + public function add($key, $value, $ttl = Cache::FIVE_MINUTES) + { + $cachekey = $this->getCacheKey($key); + $cached = serialize($value); + + return apcu_add($cachekey, $cached); + } + + public static function isAvailable() + { + if (!extension_loaded('apcu')) { + return false; + } elseif (!ini_get('apc.enabled') && !ini_get('apc.enable_cli')) { + return false; + } elseif ( + version_compare(phpversion('apc') ?: '0.0.0', '4.0.6') === -1 && + version_compare(phpversion('apcu') ?: '0.0.0', '5.1.0') === -1 + ) { + return false; + } + + return true; + } +} diff --git a/src/Core/Cache/AbstractCacheDriver.php b/src/Core/Cache/AbstractCacheDriver.php index 13993385a..f238a7819 100644 --- a/src/Core/Cache/AbstractCacheDriver.php +++ b/src/Core/Cache/AbstractCacheDriver.php @@ -13,6 +13,18 @@ use Friendica\BaseObject; */ abstract class AbstractCacheDriver extends BaseObject { + /** + * Returns the prefix (to avoid namespace conflicts) + * + * @return string + * @throws \Exception + */ + protected function getPrefix() + { + // We fetch with the hostname as key to avoid problems with other applications + return self::getApp()->getHostName(); + } + /** * @param string $key The original key * @return string The cache key used for the cache @@ -20,8 +32,7 @@ abstract class AbstractCacheDriver extends BaseObject */ protected function getCacheKey($key) { - // We fetch with the hostname as key to avoid problems with other applications - return self::getApp()->getHostName() . ":" . $key; + return $this->getPrefix() . ":" . $key; } /** diff --git a/src/Core/Cache/RedisCacheDriver.php b/src/Core/Cache/RedisCacheDriver.php index 6559cf6a7..ea4eb4390 100644 --- a/src/Core/Cache/RedisCacheDriver.php +++ b/src/Core/Cache/RedisCacheDriver.php @@ -20,11 +20,13 @@ class RedisCacheDriver extends AbstractCacheDriver implements IMemoryCacheDriver private $redis; /** - * @param string $redis_host - * @param int $redis_port + * @param string $redis_host + * @param int $redis_port + * @param int $redis_db (Default = 0, maximum is 15) + * @param string? $redis_pw * @throws Exception */ - public function __construct($redis_host, $redis_port) + public function __construct($redis_host, $redis_port, $redis_db = 0, $redis_pw = null) { if (!class_exists('Redis', false)) { throw new Exception('Redis class isn\'t available'); @@ -35,6 +37,14 @@ class RedisCacheDriver extends AbstractCacheDriver implements IMemoryCacheDriver if (!$this->redis->connect($redis_host, $redis_port)) { throw new Exception('Expected Redis server at ' . $redis_host . ':' . $redis_port . ' isn\'t available'); } + + if (isset($redis_pw) && !$this->redis->auth($redis_pw)) { + throw new Exception('Cannot authenticate redis server at ' . $redis_host . ':' . $redis_port); + } + + if ($redis_db !== 0 && !$this->redis->select($redis_db)) { + throw new Exception('Cannot switch to redis db ' . $redis_db . ' at ' . $redis_host . ':' . $redis_port); + } } /** diff --git a/src/Core/Config/Cache/ConfigCache.php b/src/Core/Config/Cache/ConfigCache.php index cb299eb33..441cdee81 100644 --- a/src/Core/Config/Cache/ConfigCache.php +++ b/src/Core/Config/Cache/ConfigCache.php @@ -2,10 +2,12 @@ namespace Friendica\Core\Config\Cache; +use ParagonIE\HiddenString\HiddenString; + /** * The Friendica config cache for the application * Initial, all *.config.php files are loaded into this cache with the - * ConfigCacheLoader ( @see ConfigCacheLoader ) + * ConfigFileLoader ( @see ConfigFileLoader ) */ class ConfigCache implements IConfigCache, IPConfigCache { @@ -15,10 +17,17 @@ class ConfigCache implements IConfigCache, IPConfigCache private $config; /** - * @param array $config A initial config array + * @var bool */ - public function __construct(array $config = []) + private $hidePasswordOutput; + + /** + * @param array $config A initial config array + * @param bool $hidePasswordOutput True, if cache variables should take extra care of password values + */ + public function __construct(array $config = [], $hidePasswordOutput = true) { + $this->hidePasswordOutput = $hidePasswordOutput; $this->load($config); } @@ -84,8 +93,13 @@ class ConfigCache implements IConfigCache, IPConfigCache $this->config[$cat] = []; } - $this->config[$cat][$key] = $value; - + if ($this->hidePasswordOutput && + $key == 'password' && + !empty($value) && is_string($value)) { + $this->config[$cat][$key] = new HiddenString((string) $value); + } else { + $this->config[$cat][$key] = $value; + } return true; } @@ -188,4 +202,32 @@ class ConfigCache implements IConfigCache, IPConfigCache { return $this->config; } + + /** + * Returns an array with missing categories/Keys + * + * @param array $config The array to check + * + * @return array + */ + public function keyDiff(array $config) + { + $return = []; + + $categories = array_keys($config); + + foreach ($categories as $category) { + if (is_array($config[$category])) { + $keys = array_keys($config[$category]); + + foreach ($keys as $key) { + if (!isset($this->config[$category][$key])) { + $return[$category][$key] = $config[$category][$key]; + } + } + } + } + + return $return; + } } diff --git a/src/Core/Config/Configuration.php b/src/Core/Config/Configuration.php index 532ed982a..18191d042 100644 --- a/src/Core/Config/Configuration.php +++ b/src/Core/Config/Configuration.php @@ -88,7 +88,7 @@ class Configuration if (isset($dbvalue)) { $this->configCache->set($cat, $key, $dbvalue); - return $dbvalue; + unset($dbvalue); } } diff --git a/src/Core/Console.php b/src/Core/Console.php index c60f36296..2893c27b2 100644 --- a/src/Core/Console.php +++ b/src/Core/Console.php @@ -2,6 +2,8 @@ namespace Friendica\Core; +use Friendica; + /** * Description of Console * @@ -13,26 +15,6 @@ class Console extends \Asika\SimpleConsole\Console protected $helpOptions = []; protected $customHelpOptions = ['h', 'help', '?']; - protected $subConsoles = [ - 'cache' => __NAMESPACE__ . '\Console\Cache', - 'config' => __NAMESPACE__ . '\Console\Config', - 'createdoxygen' => __NAMESPACE__ . '\Console\CreateDoxygen', - 'docbloxerrorchecker' => __NAMESPACE__ . '\Console\DocBloxErrorChecker', - 'dbstructure' => __NAMESPACE__ . '\Console\DatabaseStructure', - 'extract' => __NAMESPACE__ . '\Console\Extract', - 'globalcommunityblock' => __NAMESPACE__ . '\Console\GlobalCommunityBlock', - 'globalcommunitysilence' => __NAMESPACE__ . '\Console\GlobalCommunitySilence', - 'archivecontact' => __NAMESPACE__ . '\Console\ArchiveContact', - 'autoinstall' => __NAMESPACE__ . '\Console\AutomaticInstallation', - 'maintenance' => __NAMESPACE__ . '\Console\Maintenance', - 'newpassword' => __NAMESPACE__ . '\Console\NewPassword', - 'php2po' => __NAMESPACE__ . '\Console\PhpToPo', - 'po2php' => __NAMESPACE__ . '\Console\PoToPhp', - 'typo' => __NAMESPACE__ . '\Console\Typo', - 'postupdate' => __NAMESPACE__ . '\Console\PostUpdate', - 'storage' => __NAMESPACE__ . '\Console\Storage', - ]; - protected function getHelp() { $help = << Friendica\Console\Cache::class, + 'config' => Friendica\Console\Config::class, + 'createdoxygen' => Friendica\Console\CreateDoxygen::class, + 'docbloxerrorchecker' => Friendica\Console\DocBloxErrorChecker::class, + 'dbstructure' => Friendica\Console\DatabaseStructure::class, + 'extract' => Friendica\Console\Extract::class, + 'globalcommunityblock' => Friendica\Console\GlobalCommunityBlock::class, + 'globalcommunitysilence' => Friendica\Console\GlobalCommunitySilence::class, + 'archivecontact' => Friendica\Console\ArchiveContact::class, + 'autoinstall' => Friendica\Console\AutomaticInstallation::class, + 'maintenance' => Friendica\Console\Maintenance::class, + 'newpassword' => Friendica\Console\NewPassword::class, + 'php2po' => Friendica\Console\PhpToPo::class, + 'po2php' => Friendica\Console\PoToPhp::class, + 'typo' => Friendica\Console\Typo::class, + 'postupdate' => Friendica\Console\PostUpdate::class, + 'serverblock' => Friendica\Console\ServerBlock::class, + 'storage' => Friendica\Console\Storage::class, + ]; + protected function doExecute() { if ($this->getOption('v')) { diff --git a/src/Core/Hook.php b/src/Core/Hook.php index 7f0c015b3..5caa54319 100644 --- a/src/Core/Hook.php +++ b/src/Core/Hook.php @@ -7,6 +7,7 @@ namespace Friendica\Core; use Friendica\App; use Friendica\BaseObject; use Friendica\Database\DBA; +use Friendica\Util\Strings; /** * Some functions to handle hooks @@ -215,6 +216,8 @@ class Hook extends BaseObject */ public static function isAddonApp($name) { + $name = Strings::sanitizeFilePathItem($name); + if (array_key_exists('app_menu', self::$hooks)) { foreach (self::$hooks['app_menu'] as $hook) { if ($hook[0] == 'addon/' . $name . '/' . $name . '.php') { diff --git a/src/Core/Installer.php b/src/Core/Installer.php index b6090bddb..046b34ea6 100644 --- a/src/Core/Installer.php +++ b/src/Core/Installer.php @@ -10,6 +10,7 @@ use Friendica\Core\Config\Cache\IConfigCache; use Friendica\Database\DBA; use Friendica\Database\DBStructure; use Friendica\Object\Image; +use Friendica\Util\Logger\VoidLogger; use Friendica\Util\Network; use Friendica\Util\Profiler; use Friendica\Util\Strings; @@ -129,33 +130,32 @@ class Installer * - Creates `config/local.config.php` * - Installs Database Structure * - * @param string $phppath Path to the PHP-Binary (optional, if not set e.g. 'php' or '/usr/bin/php') - * @param string $urlpath Path based on the URL of Friendica (e.g. '/friendica') - * @param string $dbhost Hostname/IP of the Friendica Database - * @param string $dbuser Username of the Database connection credentials - * @param string $dbpass Password of the Database connection credentials - * @param string $dbdata Name of the Database - * @param string $timezone Timezone of the Friendica Installaton (e.g. 'Europe/Berlin') - * @param string $language 2-letter ISO 639-1 code (eg. 'en') - * @param string $adminmail Mail-Adress of the administrator - * @param string $basepath The basepath of Friendica + * @param IConfigCache $configCache The config cache with all config relevant information * * @return bool true if the config was created, otherwise false * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - public function createConfig($phppath, $urlpath, $dbhost, $dbuser, $dbpass, $dbdata, $timezone, $language, $adminmail, $basepath) + public function createConfig(IConfigCache $configCache) { + $basepath = $configCache->get('system', 'basepath'); + $tpl = Renderer::getMarkupTemplate('local.config.tpl'); $txt = Renderer::replaceMacros($tpl, [ - '$phpath' => $phppath, - '$dbhost' => $dbhost, - '$dbuser' => $dbuser, - '$dbpass' => $dbpass, - '$dbdata' => $dbdata, - '$timezone' => $timezone, - '$language' => $language, - '$urlpath' => $urlpath, - '$adminmail' => $adminmail, + '$dbhost' => $configCache->get('database', 'hostname'), + '$dbuser' => $configCache->get('database', 'username'), + '$dbpass' => $configCache->get('database', 'password'), + '$dbdata' => $configCache->get('database', 'database'), + + '$phpath' => $configCache->get('config', 'php_path'), + '$adminmail' => $configCache->get('config', 'admin_email'), + '$hostname' => $configCache->get('config', 'hostname'), + + '$urlpath' => $configCache->get('system', 'urlpath'), + '$baseurl' => $configCache->get('system', 'url'), + '$sslpolicy' => $configCache->get('system', 'ssl_policy'), + '$basepath' => $basepath, + '$timezone' => $configCache->get('system', 'default_timezone'), + '$language' => $configCache->get('system', 'language'), ]); $result = file_put_contents($basepath . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'local.config.php', $txt); @@ -246,8 +246,9 @@ class Installer $help .= L10n::t("If you don't have a command line version of PHP installed on your server, you will not be able to run the background processing. See 'Setup the worker'") . EOL; $help .= EOL . EOL; $tpl = Renderer::getMarkupTemplate('field_input.tpl'); + /// @todo Separate backend Installer class and presentation layer/view $help .= Renderer::replaceMacros($tpl, [ - '$field' => ['phpath', L10n::t('PHP executable path'), $phppath, L10n::t('Enter full path to php executable. You can leave this blank to continue the installation.')], + '$field' => ['config-php_path', L10n::t('PHP executable path'), $phppath, L10n::t('Enter full path to php executable. You can leave this blank to continue the installation.')], ]); $phppath = ""; } @@ -591,20 +592,20 @@ class Installer /** * Checking the Database connection and if it is available for the current installation * - * @param string $basePath The basepath of this call * @param IConfigCache $configCache The configuration cache * @param Profiler $profiler The profiler of this app - * @param string $dbhost Hostname/IP of the Friendica Database - * @param string $dbuser Username of the Database connection credentials - * @param string $dbpass Password of the Database connection credentials - * @param string $dbdata Name of the Database * * @return bool true if the check was successful, otherwise false * @throws Exception */ - public function checkDB($basePath, IConfigCache $configCache, Profiler $profiler, $dbhost, $dbuser, $dbpass, $dbdata) + public function checkDB(IConfigCache $configCache, Profiler $profiler) { - if (!DBA::connect($basePath, $configCache, $profiler, $dbhost, $dbuser, $dbpass, $dbdata)) { + $dbhost = $configCache->get('database', 'hostname'); + $dbuser = $configCache->get('database', 'username'); + $dbpass = $configCache->get('database', 'password'); + $dbdata = $configCache->get('database', 'database'); + + if (!DBA::connect($configCache, $profiler, new VoidLogger(), $dbhost, $dbuser, $dbpass, $dbdata)) { $this->addCheck(L10n::t('Could not connect to database.'), false, true, ''); return false; @@ -620,4 +621,18 @@ class Installer return true; } + + /** + * Setup the default cache for a new installation + * + * @param IConfigCache $configCache The configuration cache + * @param string $basePath The determined basepath + * + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + */ + public function setUpCache(IConfigCache $configCache, $basePath) + { + $configCache->set('config', 'php_path' , $this->getPHPPath()); + $configCache->set('system', 'basepath' , $basePath); + } } diff --git a/src/Core/L10n.php b/src/Core/L10n.php index f7ed9918c..ae0ed18c3 100644 --- a/src/Core/L10n.php +++ b/src/Core/L10n.php @@ -6,6 +6,7 @@ namespace Friendica\Core; use Friendica\BaseObject; use Friendica\Database\DBA; +use Friendica\Util\Strings; /** * Provide Language, Translation, and Localization functions to the application @@ -193,6 +194,8 @@ class L10n extends BaseObject */ private static function loadTranslationTable($lang) { + $lang = Strings::sanitizeFilePathItem($lang); + if ($lang === self::$lang) { return; } @@ -203,7 +206,7 @@ class L10n extends BaseObject // load enabled addons strings $addons = DBA::select('addon', ['name'], ['installed' => true]); while ($p = DBA::fetch($addons)) { - $name = $p['name']; + $name = Strings::sanitizeFilePathItem($p['name']); if (file_exists("addon/$name/lang/$lang/strings.php")) { include "addon/$name/lang/$lang/strings.php"; } diff --git a/src/Core/Lock.php b/src/Core/Lock.php index 8bc2c242d..a45490bf3 100644 --- a/src/Core/Lock.php +++ b/src/Core/Lock.php @@ -7,7 +7,7 @@ namespace Friendica\Core; -use Friendica\Core\Cache\CacheDriverFactory; +use Friendica\Factory\CacheDriverFactory; use Friendica\Core\Cache\IMemoryCacheDriver; /** diff --git a/src/Core/Logger.php b/src/Core/Logger.php index 3cb22e1e4..fc2dde1df 100644 --- a/src/Core/Logger.php +++ b/src/Core/Logger.php @@ -4,14 +4,13 @@ */ namespace Friendica\Core; -use Friendica\BaseObject; use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; /** * @brief Logger functions */ -class Logger extends BaseObject +class Logger { /** * @see Logger::error() @@ -96,13 +95,7 @@ class Logger extends BaseObject */ public static function emergency($message, $context = []) { - if (!isset(self::$logger)) { - return; - } - - $stamp1 = microtime(true); self::$logger->emergency($message, $context); - self::getApp()->GetProfiler()->saveTimestamp($stamp1, 'file', System::callstack()); } /** @@ -120,13 +113,7 @@ class Logger extends BaseObject */ public static function alert($message, $context = []) { - if (!isset(self::$logger)) { - return; - } - - $stamp1 = microtime(true); self::$logger->alert($message, $context); - self::getApp()->getProfiler()->saveTimestamp($stamp1, 'file', System::callstack()); } /** @@ -143,13 +130,7 @@ class Logger extends BaseObject */ public static function critical($message, $context = []) { - if (!isset(self::$logger)) { - return; - } - - $stamp1 = microtime(true); self::$logger->critical($message, $context); - self::getApp()->getProfiler()->saveTimestamp($stamp1, 'file', System::callstack()); } /** @@ -165,14 +146,7 @@ class Logger extends BaseObject */ public static function error($message, $context = []) { - if (!isset(self::$logger)) { - echo "not set!?\n"; - return; - } - - $stamp1 = microtime(true); self::$logger->error($message, $context); - self::getApp()->getProfiler()->saveTimestamp($stamp1, 'file', System::callstack()); } /** @@ -190,13 +164,7 @@ class Logger extends BaseObject */ public static function warning($message, $context = []) { - if (!isset(self::$logger)) { - return; - } - - $stamp1 = microtime(true); self::$logger->warning($message, $context); - self::getApp()->getProfiler()->saveTimestamp($stamp1, 'file', System::callstack()); } /** @@ -211,13 +179,7 @@ class Logger extends BaseObject */ public static function notice($message, $context = []) { - if (!isset(self::$logger)) { - return; - } - - $stamp1 = microtime(true); self::$logger->notice($message, $context); - self::getApp()->getProfiler()->saveTimestamp($stamp1, 'file', System::callstack()); } /** @@ -234,13 +196,7 @@ class Logger extends BaseObject */ public static function info($message, $context = []) { - if (!isset(self::$logger)) { - return; - } - - $stamp1 = microtime(true); self::$logger->info($message, $context); - self::getApp()->getProfiler()->saveTimestamp($stamp1, 'file', System::callstack()); } /** @@ -255,13 +211,7 @@ class Logger extends BaseObject */ public static function debug($message, $context = []) { - if (!isset(self::$logger)) { - return; - } - - $stamp1 = microtime(true); self::$logger->debug($message, $context); - self::getApp()->getProfiler()->saveTimestamp($stamp1, 'file', System::callstack()); } /** @@ -275,13 +225,7 @@ class Logger extends BaseObject */ public static function log($msg, $level = LogLevel::INFO) { - if (!isset(self::$logger)) { - return; - } - - $stamp1 = microtime(true); self::$logger->log($level, $msg); - self::getApp()->getProfiler()->saveTimestamp($stamp1, "file", System::callstack()); } /** @@ -296,12 +240,10 @@ class Logger extends BaseObject */ public static function devLog($msg, $level = LogLevel::DEBUG) { - if (!isset(self::$logger)) { + if (!isset(self::$devLogger)) { return; } - $stamp1 = microtime(true); self::$devLogger->log($level, $msg); - self::getApp()->getProfiler()->saveTimestamp($stamp1, "file", System::callstack()); } } diff --git a/src/Core/NotificationsManager.php b/src/Core/NotificationsManager.php index d582f2159..5b2e97005 100644 --- a/src/Core/NotificationsManager.php +++ b/src/Core/NotificationsManager.php @@ -36,7 +36,7 @@ class NotificationsManager extends BaseObject * - msg_plain: message as plain text string * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - private function _set_extra($notes) + private function _set_extra(array $notes) { $rets = []; foreach ($notes as $n) { @@ -55,50 +55,28 @@ class NotificationsManager extends BaseObject * @brief Get all notifications for local_user() * * @param array $filter optional Array "column name"=>value: filter query by columns values - * @param string $order optional Space separated list of column to sort by. - * Prepend name with "+" to sort ASC, "-" to sort DESC. Default to "-date" + * @param array $order optional Array to order by * @param string $limit optional Query limits * - * @return array of results or false on errors + * @return array|bool of results or false on errors * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - public function getAll($filter = [], $order = "-date", $limit = "") + public function getAll($filter = [], $order = ['date' => 'DESC'], $limit = "") { - $filter_str = []; - $filter_sql = ""; - foreach ($filter as $column => $value) { - $filter_str[] = sprintf("`%s` = '%s'", $column, DBA::escape($value)); - } - if (count($filter_str) > 0) { - $filter_sql = "AND " . implode(" AND ", $filter_str); + $params = []; + + $params['order'] = $order; + + if (!empty($limit)) { + $params['limit'] = $limit; } - $aOrder = explode(" ", $order); - $asOrder = []; - foreach ($aOrder as $o) { - $dir = "asc"; - if ($o[0] === "-") { - $dir = "desc"; - $o = substr($o, 1); - } - if ($o[0] === "+") { - $dir = "asc"; - $o = substr($o, 1); - } - $asOrder[] = "$o $dir"; - } - $order_sql = implode(", ", $asOrder); + $dbFilter = array_merge($filter, ['uid' => local_user()]); - if ($limit != "") { - $limit = " LIMIT " . $limit; - } - $r = q( - "SELECT * FROM `notify` WHERE `uid` = %d $filter_sql ORDER BY $order_sql $limit", - intval(local_user()) - ); + $stmtNotifies = DBA::select('notify', [], $dbFilter, $params); - if (DBA::isResult($r)) { - return $this->_set_extra($r); + if (DBA::isResult($stmtNotifies)) { + return $this->_set_extra(DBA::toArray($stmtNotifies)); } return false; @@ -113,13 +91,9 @@ class NotificationsManager extends BaseObject */ public function getByID($id) { - $r = q( - "SELECT * FROM `notify` WHERE `id` = %d AND `uid` = %d LIMIT 1", - intval($id), - intval(local_user()) - ); - if (DBA::isResult($r)) { - return $this->_set_extra($r)[0]; + $stmtNotify = DBA::selectFirst('notify', [], ['id' => $id, 'uid' => local_user()]); + if (DBA::isResult($stmtNotify)) { + return $this->_set_extra([$stmtNotify])[0]; } return null; } @@ -134,14 +108,13 @@ class NotificationsManager extends BaseObject */ public function setSeen($note, $seen = true) { - return q( - "UPDATE `notify` SET `seen` = %d WHERE (`link` = '%s' OR (`parent` != 0 AND `parent` = %d AND `otype` = '%s')) AND `uid` = %d", - intval($seen), - DBA::escape($note['link']), - intval($note['parent']), - DBA::escape($note['otype']), - intval(local_user()) - ); + return DBA::update('notify', ['seen' => $seen], [ + '(`link` = ? OR (`parent` != 0 AND `parent` = ? AND `otype` = ?)) AND `uid` = ?', + $note['link'], + $note['parent'], + $note['otype'], + local_user() + ]); } /** @@ -153,11 +126,7 @@ class NotificationsManager extends BaseObject */ public function setAllSeen($seen = true) { - return q( - "UPDATE `notify` SET `seen` = %d WHERE `uid` = %d", - intval($seen), - intval(local_user()) - ); + return DBA::update('notify', ['seen' => $seen], ['uid' => local_user()]); } /** @@ -460,20 +429,22 @@ class NotificationsManager extends BaseObject $notifs = []; $sql_seen = ""; + $filter = ['uid' => local_user()]; if ($seen === 0) { - $sql_seen = " AND NOT `seen` "; + $filter['seen'] = false; } - $r = q( - "SELECT `id`, `url`, `photo`, `msg`, `date`, `seen`, `verb` FROM `notify` - WHERE `uid` = %d $sql_seen ORDER BY `date` DESC LIMIT %d, %d ", - intval(local_user()), - intval($start), - intval($limit) - ); + $params = []; + $params['order'] = ['date' => 'DESC']; + $params['limit'] = [$start, $limit]; - if (DBA::isResult($r)) { - $notifs = $this->formatNotifs($r, $ident); + $stmtNotifies = DBA::select('notify', + ['id', 'url', 'photo', 'msg', 'date', 'seen', 'verb'], + $filter, + $params); + + if (DBA::isResult($stmtNotifies)) { + $notifs = $this->formatNotifs(DBA::toArray($stmtNotifies), $ident); } $arr = [ @@ -592,11 +563,11 @@ class NotificationsManager extends BaseObject $sql_extra = ""; if (!$all) { - $sql_extra = " AND `ignore` = 0 "; + $sql_extra = " AND NOT `ignore` "; } /// @todo Fetch contact details by "Contact::getDetailsByUrl" instead of queries to contact, fcontact and gcontact - $r = q( + $stmtNotifies = DBA::p( "SELECT `intro`.`id` AS `intro_id`, `intro`.*, `contact`.*, `fcontact`.`name` AS `fname`, `fcontact`.`url` AS `furl`, `fcontact`.`addr` AS `faddr`, `fcontact`.`photo` AS `fphoto`, `fcontact`.`request` AS `frequest`, @@ -607,14 +578,14 @@ class NotificationsManager extends BaseObject LEFT JOIN `contact` ON `contact`.`id` = `intro`.`contact-id` LEFT JOIN `gcontact` ON `gcontact`.`nurl` = `contact`.`nurl` LEFT JOIN `fcontact` ON `intro`.`fid` = `fcontact`.`id` - WHERE `intro`.`uid` = %d $sql_extra AND `intro`.`blocked` = 0 - LIMIT %d, %d", - intval($_SESSION['uid']), - intval($start), - intval($limit) + WHERE `intro`.`uid` = ? $sql_extra AND `intro`.`blocked` = 0 + LIMIT ?, ?", + $_SESSION['uid'], + $start, + $limit ); - if (DBA::isResult($r)) { - $notifs = $this->formatIntros($r); + if (DBA::isResult($stmtNotifies)) { + $notifs = $this->formatIntros(DBA::toArray($stmtNotifies)); } $arr = [ diff --git a/src/Core/README.md b/src/Core/README.md new file mode 100644 index 000000000..8a5a3788b --- /dev/null +++ b/src/Core/README.md @@ -0,0 +1,4 @@ +## Friendica\Core + +The Core namespace contains classes, which are essential to Friendica. + \ No newline at end of file diff --git a/src/Core/Renderer.php b/src/Core/Renderer.php index 8844f2688..fd5e73302 100644 --- a/src/Core/Renderer.php +++ b/src/Core/Renderer.php @@ -15,17 +15,17 @@ use Friendica\Render\ITemplateEngine; */ class Renderer extends BaseObject { - /** + /** * @brief An array of registered template engines ('name'=>'class name') */ - public static $template_engines = []; + public static $template_engines = []; - /** + /** * @brief An array of instanced template engines ('name'=>'instance') */ public static $template_engine_instance = []; - /** + /** * @brief An array for all theme-controllable parameters * * Mostly unimplemented yet. Only options 'template_engine' and @@ -39,45 +39,46 @@ class Renderer extends BaseObject 'stylesheet' => '', 'template_engine' => 'smarty3', ]; - - private static $ldelim = [ + + private static $ldelim = [ 'internal' => '', 'smarty3' => '{{' ]; private static $rdelim = [ 'internal' => '', 'smarty3' => '}}' - ]; + ]; /** * @brief This is our template processor * * @param string|FriendicaSmarty $s The string requiring macro substitution or an instance of FriendicaSmarty - * @param array $vars key value pairs (search => replace) + * @param array $vars Key value pairs (search => replace) * * @return string substituted string - * @throws \Friendica\Network\HTTPException\InternalServerErrorException + * @throws Exception */ - public static function replaceMacros($s, $vars) - { - $stamp1 = microtime(true); - $a = self::getApp(); + public static function replaceMacros($s, array $vars = []) + { + $stamp1 = microtime(true); + $a = self::getApp(); - // pass $baseurl to all templates - $vars['$baseurl'] = System::baseUrl(); - $t = self::getTemplateEngine(); + // pass $baseurl to all templates if it isn't set + $vars = array_merge(['$baseurl' => $a->getBaseURL()], $vars); - try { - $output = $t->replaceMacros($s, $vars); - } catch (Exception $e) { - echo "
    " . __FUNCTION__ . ": " . $e->getMessage() . "
    "; - exit(); - } + $t = self::getTemplateEngine(); + + try { + $output = $t->replaceMacros($s, $vars); + } catch (Exception $e) { + echo "
    " . __FUNCTION__ . ": " . $e->getMessage() . "
    "; + exit(); + } $a->getProfiler()->saveTimestamp($stamp1, "rendering", System::callstack()); - return $output; - } + return $output; + } /** * @brief Load a given template $s @@ -88,35 +89,35 @@ class Renderer extends BaseObject * @return string template. * @throws Exception */ - public static function getMarkupTemplate($s, $root = '') - { - $stamp1 = microtime(true); - $a = self::getApp(); - $t = self::getTemplateEngine(); + public static function getMarkupTemplate($s, $root = '') + { + $stamp1 = microtime(true); + $a = self::getApp(); + $t = self::getTemplateEngine(); - try { - $template = $t->getTemplateFile($s, $root); - } catch (Exception $e) { - echo "
    " . __FUNCTION__ . ": " . $e->getMessage() . "
    "; - exit(); - } + try { + $template = $t->getTemplateFile($s, $root); + } catch (Exception $e) { + echo "
    " . __FUNCTION__ . ": " . $e->getMessage() . "
    "; + exit(); + } - $a->getProfiler()->saveTimestamp($stamp1, "file", System::callstack()); + $a->getProfiler()->saveTimestamp($stamp1, "file", System::callstack()); - return $template; - } + return $template; + } - /** + /** * @brief Register template engine class * * @param string $class */ public static function registerTemplateEngine($class) { - $v = get_class_vars($class); - - if (!empty($v['name'])) - { + $v = get_class_vars($class); + + if (!empty($v['name'])) + { $name = $v['name']; self::$template_engines[$name] = $class; } else { @@ -150,9 +151,9 @@ class Renderer extends BaseObject echo "template engine $template_engine is not registered!\n"; exit(); - } - - /** + } + + /** * @brief Returns the active template engine. * * @return string the active template engine @@ -172,7 +173,7 @@ class Renderer extends BaseObject self::$theme['template_engine'] = $engine; } - /** + /** * Gets the right delimiter for a template engine * * Currently: diff --git a/src/Core/Search.php b/src/Core/Search.php new file mode 100644 index 000000000..e26cc0edc --- /dev/null +++ b/src/Core/Search.php @@ -0,0 +1,244 @@ +getConfig(); + $server = $config->get('system', 'directory', self::DEFAULT_DIRECTORY); + + $searchUrl = $server . '/search'; + + switch ($type) { + case self::TYPE_FORUM: + $searchUrl .= '/forum'; + break; + case self::TYPE_PEOPLE: + $searchUrl .= '/people'; + break; + } + $searchUrl .= '?q=' . urlencode($search); + + if ($page > 1) { + $searchUrl .= '&page=' . $page; + } + + $resultJson = Network::fetchUrl($searchUrl, false, 0, 'application/json'); + + $results = json_decode($resultJson, true); + + $resultList = new ResultList( + defaults($results, 'page', 1), + defaults($results, 'count', 0), + defaults($results, 'itemsperpage', 30) + ); + + $profiles = defaults($results, 'profiles', []); + + foreach ($profiles as $profile) { + $contactDetails = Contact::getDetailsByURL(defaults($profile, 'profile_url', ''), local_user()); + $itemUrl = defaults($contactDetails, 'addr', defaults($profile, 'profile_url', '')); + + $result = new ContactResult( + defaults($profile, 'name', ''), + defaults($profile, 'addr', ''), + $itemUrl, + defaults($profile, 'profile_url', ''), + defaults($profile, 'photo', ''), + Protocol::DFRN, + defaults($contactDetails, 'cid', 0), + 0, + defaults($profile, 'tags', '')); + + $resultList->addResult($result); + } + + return $resultList; + } + + /** + * Search in the local database for occurrences of the search string + * + * @param string $search + * @param int $type + * @param int $start + * @param int $itemPage + * + * @return ResultList + * @throws HTTPException\InternalServerErrorException + */ + public static function getContactsFromLocalDirectory($search, $type = self::TYPE_ALL, $start = 0, $itemPage = 80) + { + $config = self::getApp()->getConfig(); + + $diaspora = $config->get('system', 'diaspora_enabled') ? Protocol::DIASPORA : Protocol::DFRN; + $ostatus = !$config->get('system', 'ostatus_disabled') ? Protocol::OSTATUS : Protocol::DFRN; + + $wildcard = Strings::escapeHtml('%' . $search . '%'); + + $count = DBA::count('gcontact', [ + 'NOT `hide` + AND `network` IN (?, ?, ?, ?) + AND ((`last_contact` >= `last_failure`) OR (`updated` >= `last_failure`)) + AND (`url` LIKE ? OR `name` LIKE ? OR `location` LIKE ? + OR `addr` LIKE ? OR `about` LIKE ? OR `keywords` LIKE ?) + AND `community` = ?', + Protocol::ACTIVITYPUB, Protocol::DFRN, $ostatus, $diaspora, + $wildcard, $wildcard, $wildcard, + $wildcard, $wildcard, $wildcard, + ($type === self::TYPE_FORUM), + ]); + + $resultList = new ResultList($start, $itemPage, $count); + + if (empty($count)) { + return $resultList; + } + + $data = DBA::select('gcontact', ['nurl'], [ + 'NOT `hide` + AND `network` IN (?, ?, ?, ?) + AND ((`last_contact` >= `last_failure`) OR (`updated` >= `last_failure`)) + AND (`url` LIKE ? OR `name` LIKE ? OR `location` LIKE ? + OR `addr` LIKE ? OR `about` LIKE ? OR `keywords` LIKE ?) + AND `community` = ?', + Protocol::ACTIVITYPUB, Protocol::DFRN, $ostatus, $diaspora, + $wildcard, $wildcard, $wildcard, + $wildcard, $wildcard, $wildcard, + ($type === self::TYPE_FORUM), + ], [ + 'group_by' => ['nurl', 'updated'], + 'limit' => [$start, $itemPage], + 'order' => ['updated' => 'DESC'] + ]); + + if (!DBA::isResult($data)) { + return $resultList; + } + + while ($row = DBA::fetch($data)) { + if (PortableContact::alternateOStatusUrl($row["nurl"])) { + continue; + } + + $urlParts = parse_url($row["nurl"]); + + // Ignore results that look strange. + // For historic reasons the gcontact table does contain some garbage. + if (!empty($urlParts['query']) || !empty($urlParts['fragment'])) { + continue; + } + + $contact = Contact::getDetailsByURL($row["nurl"], local_user()); + + if ($contact["name"] == "") { + $contact["name"] = end(explode("/", $urlParts["path"])); + } + + $result = new ContactResult( + $contact["name"], + $contact["addr"], + $contact["addr"], + $contact["url"], + $contact["photo"], + $contact["network"], + $contact["cid"], + $contact["zid"], + $contact["keywords"] + ); + + $resultList->addResult($result); + } + + DBA::close($data); + + // Add found profiles from the global directory to the local directory + Worker::add(PRIORITY_LOW, 'DiscoverPoCo', "dirsearch", urlencode($search)); + + return $resultList; + } +} diff --git a/src/Core/Session.php b/src/Core/Session.php index d9143c880..8b6c26bc5 100644 --- a/src/Core/Session.php +++ b/src/Core/Session.php @@ -5,8 +5,14 @@ */ namespace Friendica\Core; +use Friendica\App; use Friendica\Core\Session\CacheSessionHandler; use Friendica\Core\Session\DatabaseSessionHandler; +use Friendica\Database\DBA; +use Friendica\Model\Contact; +use Friendica\Model\User; +use Friendica\Util\BaseURL; +use Friendica\Util\DateTimeFormat; /** * High-level Session service class @@ -24,7 +30,7 @@ class Session ini_set('session.use_only_cookies', 1); ini_set('session.cookie_httponly', 1); - if (Config::get('system', 'ssl_policy') == SSL_POLICY_FULL) { + if (Config::get('system', 'ssl_policy') == BaseURL::SSL_POLICY_FULL) { ini_set('session.cookie_secure', 1); } @@ -65,8 +71,142 @@ class Session return $return; } + /** + * Sets a single session variable. + * Overrides value of existing key. + * + * @param string $name + * @param mixed $value + */ public static function set($name, $value) { $_SESSION[$name] = $value; } + + /** + * Sets multiple session variables. + * Overrides values for existing keys. + * + * @param array $values + */ + public static function setMultiple(array $values) + { + $_SESSION = $values + $_SESSION; + } + + /** + * Removes a session variable. + * Ignores missing keys. + * + * @param $name + */ + public static function remove($name) + { + unset($_SESSION[$name]); + } + + /** + * @brief Sets the provided user's authenticated session + * + * @param App $a + * @param array $user_record + * @param bool $login_initial + * @param bool $interactive + * @param bool $login_refresh + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + */ + public static function setAuthenticatedForUser(App $a, array $user_record, $login_initial = false, $interactive = false, $login_refresh = false) + { + self::setMultiple([ + 'uid' => $user_record['uid'], + 'theme' => $user_record['theme'], + 'mobile-theme' => PConfig::get($user_record['uid'], 'system', 'mobile_theme'), + 'authenticated' => 1, + 'page_flags' => $user_record['page-flags'], + 'my_url' => $a->getBaseURL() . '/profile/' . $user_record['nickname'], + 'my_address' => $user_record['nickname'] . '@' . substr($a->getBaseURL(), strpos($a->getBaseURL(), '://') + 3), + 'addr' => defaults($_SERVER, 'REMOTE_ADDR', '0.0.0.0'), + ]); + + $member_since = strtotime($user_record['register_date']); + self::set('new_member', time() < ($member_since + ( 60 * 60 * 24 * 14))); + + if (strlen($user_record['timezone'])) { + date_default_timezone_set($user_record['timezone']); + $a->timezone = $user_record['timezone']; + } + + $masterUid = $user_record['uid']; + + if (self::get('submanage')) { + $user = DBA::selectFirst('user', ['uid'], ['uid' => self::get('submanage')]); + if (DBA::isResult($user)) { + $masterUid = $user['uid']; + } + } + + $a->identities = User::identities($masterUid); + + if ($login_initial) { + $a->getLogger()->info('auth_identities: ' . print_r($a->identities, true)); + } + + if ($login_refresh) { + $a->getLogger()->info('auth_identities refresh: ' . print_r($a->identities, true)); + } + + $contact = DBA::selectFirst('contact', [], ['uid' => $user_record['uid'], 'self' => true]); + if (DBA::isResult($contact)) { + $a->contact = $contact; + $a->cid = $contact['id']; + self::set('cid', $a->cid); + } + + header('X-Account-Management-Status: active; name="' . $user_record['username'] . '"; id="' . $user_record['nickname'] . '"'); + + if ($login_initial || $login_refresh) { + DBA::update('user', ['login_date' => DateTimeFormat::utcNow()], ['uid' => $user_record['uid']]); + + // Set the login date for all identities of the user + DBA::update('user', ['login_date' => DateTimeFormat::utcNow()], + ['parent-uid' => $masterUid, 'account_removed' => false]); + } + + if ($login_initial) { + /* + * If the user specified to remember the authentication, then set a cookie + * that expires after one week (the default is when the browser is closed). + * The cookie will be renewed automatically. + * The week ensures that sessions will expire after some inactivity. + */ + ; + if (self::get('remember')) { + $a->getLogger()->info('Injecting cookie for remembered user ' . $user_record['nickname']); + Authentication::setCookie(604800, $user_record); + self::remove('remember'); + } + } + + Authentication::twoFactorCheck($user_record['uid'], $a); + + if ($interactive) { + if ($user_record['login_date'] <= DBA::NULL_DATETIME) { + info(L10n::t('Welcome %s', $user_record['username'])); + info(L10n::t('Please upload a profile photo.')); + $a->internalRedirect('profile_photo/new'); + } else { + info(L10n::t("Welcome back %s", $user_record['username'])); + } + } + + $a->user = $user_record; + + if ($login_initial) { + Hook::callAll('logged_in', $a->user); + + if ($a->module !== 'home' && self::exists('return_path')) { + $a->internalRedirect(self::get('return_path')); + } + } + } } diff --git a/src/Core/Session/CacheSessionHandler.php b/src/Core/Session/CacheSessionHandler.php index c0a5896f4..6a1b32bfb 100644 --- a/src/Core/Session/CacheSessionHandler.php +++ b/src/Core/Session/CacheSessionHandler.php @@ -31,7 +31,9 @@ class CacheSessionHandler extends BaseObject implements SessionHandlerInterface Session::$exists = true; return $data; } - Logger::log("no data for session $session_id", Logger::TRACE); + + Logger::notice('no data for session', ['session_id' => $session_id, 'uri' => $_SERVER['REQUEST_URI']]); + return ''; } diff --git a/src/Core/Session/DatabaseSessionHandler.php b/src/Core/Session/DatabaseSessionHandler.php index 9bb1180ec..e3e95f9ed 100644 --- a/src/Core/Session/DatabaseSessionHandler.php +++ b/src/Core/Session/DatabaseSessionHandler.php @@ -31,7 +31,8 @@ class DatabaseSessionHandler extends BaseObject implements SessionHandlerInterfa Session::$exists = true; return $session['data']; } - Logger::log("no data for session $session_id", Logger::TRACE); + + Logger::notice('no data for session', ['session_id' => $session_id, 'uri' => $_SERVER['REQUEST_URI']]); return ''; } diff --git a/src/Core/StorageManager.php b/src/Core/StorageManager.php index 0a8b35ce2..8cd7d4395 100644 --- a/src/Core/StorageManager.php +++ b/src/Core/StorageManager.php @@ -30,6 +30,7 @@ class StorageManager /** * @brief Return current storage backend class + * * @return string * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ diff --git a/src/Core/System.php b/src/Core/System.php index 45a88fe09..42587577d 100644 --- a/src/Core/System.php +++ b/src/Core/System.php @@ -23,7 +23,7 @@ class System extends BaseObject /** * @brief Retrieves the Friendica instance base URL * - * @param bool $ssl Whether to append http or https under SSL_POLICY_SELFSIGN + * @param bool $ssl Whether to append http or https under BaseURL::SSL_POLICY_SELFSIGN * @return string Friendica server base URL * @throws InternalServerErrorException */ @@ -120,58 +120,17 @@ class System extends BaseObject /** * @brief Send HTTP status header and exit. * - * @param integer $val HTTP status result value - * @param array $description optional message - * 'title' => header title - * 'description' => optional message - * @throws InternalServerErrorException + * @param integer $val HTTP status result value + * @param string $message Error message. Optional. + * @param string $content Response body. Optional. + * @throws \Exception */ - public static function httpExit($val, $description = []) + public static function httpExit($val, $message = '', $content = '') { - $err = ''; - if ($val >= 400) { - if (!empty($description['title'])) { - $err = $description['title']; - } else { - $title = [ - '400' => L10n::t('Error 400 - Bad Request'), - '401' => L10n::t('Error 401 - Unauthorized'), - '403' => L10n::t('Error 403 - Forbidden'), - '404' => L10n::t('Error 404 - Not Found'), - '500' => L10n::t('Error 500 - Internal Server Error'), - '503' => L10n::t('Error 503 - Service Unavailable'), - ]; - $err = defaults($title, $val, 'Error ' . $val); - $description['title'] = $err; - } - if (empty($description['description'])) { - // Explanations are taken from https://en.wikipedia.org/wiki/List_of_HTTP_status_codes - $explanation = [ - '400' => L10n::t('The server cannot or will not process the request due to an apparent client error.'), - '401' => L10n::t('Authentication is required and has failed or has not yet been provided.'), - '403' => L10n::t('The request was valid, but the server is refusing action. The user might not have the necessary permissions for a resource, or may need an account.'), - '404' => L10n::t('The requested resource could not be found but may be available in the future.'), - '500' => L10n::t('An unexpected condition was encountered and no more specific message is suitable.'), - '503' => L10n::t('The server is currently unavailable (because it is overloaded or down for maintenance). Please try again later.'), - ]; - if (!empty($explanation[$val])) { - $description['description'] = $explanation[$val]; - } - } - } - - if ($val >= 200 && $val < 300) { - $err = 'OK'; - } - Logger::log('http_status_exit ' . $val); - header($_SERVER["SERVER_PROTOCOL"] . ' ' . $val . ' ' . $err); + header($_SERVER["SERVER_PROTOCOL"] . ' ' . $val . ' ' . $message); - if (isset($description["title"])) { - $tpl = Renderer::getMarkupTemplate('http_status.tpl'); - echo Renderer::replaceMacros($tpl, ['$title' => $description["title"], - '$description' => defaults($description, 'description', '')]); - } + echo $content; exit(); } @@ -264,15 +223,30 @@ class System extends BaseObject * Redirects to an external URL (fully qualified URL) * If you want to route relative to the current Friendica base, use App->internalRedirect() * - * @param string $url The new Location to redirect + * @param string $url The new Location to redirect + * @param int $code The redirection code, which is used (Default is 302) + * * @throws InternalServerErrorException If the URL is not fully qualified */ - public static function externalRedirect($url) + public static function externalRedirect($url, $code = 302) { if (empty(parse_url($url, PHP_URL_SCHEME))) { throw new InternalServerErrorException("'$url' is not a fully qualified URL, please use App->internalRedirect() instead"); } + switch ($code) { + case 302: + // this is the default code for a REDIRECT + // We don't need a extra header here + break; + case 301: + header('HTTP/1.1 301 Moved Permanently'); + break; + case 307: + header('HTTP/1.1 307 Temporary Redirect'); + break; + } + header("Location: $url"); exit(); } diff --git a/src/Core/Theme.php b/src/Core/Theme.php index 5479f8f46..61798a396 100644 --- a/src/Core/Theme.php +++ b/src/Core/Theme.php @@ -8,6 +8,7 @@ namespace Friendica\Core; use Friendica\BaseObject; use Friendica\Model\Profile; +use Friendica\Util\Strings; require_once 'boot.php'; @@ -16,6 +17,28 @@ require_once 'boot.php'; */ class Theme { + public static function getAllowedList() + { + $allowed_themes_str = Config::get('system', 'allowed_themes'); + $allowed_themes_raw = explode(',', str_replace(' ', '', $allowed_themes_str)); + $allowed_themes = []; + if (count($allowed_themes_raw)) { + foreach ($allowed_themes_raw as $theme) { + $theme = Strings::sanitizeFilePathItem(trim($theme)); + if (strlen($theme) && is_dir("view/theme/$theme")) { + $allowed_themes[] = $theme; + } + } + } + + return $allowed_themes; + } + + public static function setAllowedList(array $allowed_themes) + { + Config::set('system', 'allowed_themes', implode(',', $allowed_themes)); + } + /** * @brief Parse theme comment in search of theme infos. * @@ -33,6 +56,8 @@ class Theme */ public static function getInfo($theme) { + $theme = Strings::sanitizeFilePathItem($theme); + $info = [ 'name' => $theme, 'description' => "", @@ -96,45 +121,63 @@ class Theme */ public static function getScreenshot($theme) { + $theme = Strings::sanitizeFilePathItem($theme); + $exts = ['.png', '.jpg']; foreach ($exts as $ext) { if (file_exists('view/theme/' . $theme . '/screenshot' . $ext)) { - return(System::baseUrl() . '/view/theme/' . $theme . '/screenshot' . $ext); + return System::baseUrl() . '/view/theme/' . $theme . '/screenshot' . $ext; } } - return(System::baseUrl() . '/images/blank.png'); + return System::baseUrl() . '/images/blank.png'; } - // install and uninstall theme public static function uninstall($theme) { - Logger::log("Addons: uninstalling theme " . $theme); + $theme = Strings::sanitizeFilePathItem($theme); + + // silently fail if theme was removed or if $theme is funky + if (file_exists("view/theme/$theme/theme.php")) { + include_once "view/theme/$theme/theme.php"; - include_once "view/theme/$theme/theme.php"; - if (function_exists("{$theme}_uninstall")) { $func = "{$theme}_uninstall"; - $func(); + if (function_exists($func)) { + $func(); + } + } + + $allowed_themes = Theme::getAllowedList(); + $key = array_search($theme, $allowed_themes); + if ($key !== false) { + unset($allowed_themes[$key]); + Theme::setAllowedList($allowed_themes); } } public static function install($theme) { - // silently fail if theme was removed + $theme = Strings::sanitizeFilePathItem($theme); + // silently fail if theme was removed or if $theme is funky if (!file_exists("view/theme/$theme/theme.php")) { return false; } - Logger::log("Addons: installing theme $theme"); + try { + include_once "view/theme/$theme/theme.php"; - include_once "view/theme/$theme/theme.php"; - - if (function_exists("{$theme}_install")) { $func = "{$theme}_install"; - $func(); + if (function_exists($func)) { + $func(); + } + + $allowed_themes = Theme::getAllowedList(); + $allowed_themes[] = $theme; + Theme::setAllowedList($allowed_themes); + return true; - } else { - Logger::log("Addons: FAILED installing theme $theme"); + } catch (\Exception $e) { + Logger::error('Theme installation failed', ['theme' => $theme, 'error' => $e->getMessage()]); return false; } } @@ -166,10 +209,10 @@ class Theme $parent = 'NOPATH'; } $theme = \get_app()->getCurrentTheme(); - $thname = $theme; + $parent = Strings::sanitizeFilePathItem($parent); $ext = substr($file, strrpos($file, '.') + 1); $paths = [ - "{$root}view/theme/$thname/$ext/$file", + "{$root}view/theme/$theme/$ext/$file", "{$root}view/theme/$parent/$ext/$file", "{$root}view/$ext/$file", ]; @@ -195,6 +238,8 @@ class Theme */ public static function getStylesheetPath($theme) { + $theme = Strings::sanitizeFilePathItem($theme); + if (!file_exists('view/theme/' . $theme . '/style.php')) { return 'view/theme/' . $theme . '/style.css'; } diff --git a/src/Core/Update.php b/src/Core/Update.php index bb2513d38..a52ef903b 100644 --- a/src/Core/Update.php +++ b/src/Core/Update.php @@ -2,6 +2,7 @@ namespace Friendica\Core; +use Friendica\App; use Friendica\Database\DBA; use Friendica\Database\DBStructure; use Friendica\Util\Strings; @@ -14,16 +15,23 @@ class Update /** * @brief Function to check if the Database structure needs an update. * - * @param string $basePath The base path of this application - * @param boolean $via_worker boolean Is the check run via the worker? + * @param string $basePath The base path of this application + * @param boolean $via_worker Is the check run via the worker? + * @param App\Mode $mode The current app mode + * * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - public static function check($basePath, $via_worker) + public static function check($basePath, $via_worker, App\Mode $mode) { if (!DBA::connected()) { return; } + // Don't check the status if the last update was failed + if (Config::get('system', 'update', Update::SUCCESS, true) == Update::FAILED) { + return; + } + $build = Config::get('system', 'build'); if (empty($build)) { @@ -101,7 +109,9 @@ class Update for ($x = $stored + 1; $x <= $current; $x++) { $r = self::runUpdateFunction($x, 'pre_update'); if (!$r) { - break; + Config::set('system', 'update', Update::FAILED); + Lock::release('dbupdate'); + return $r; } } @@ -115,6 +125,7 @@ class Update ); } Logger::error('Update ERROR.', ['from' => $stored, 'to' => $current, 'retval' => $retval]); + Config::set('system', 'update', Update::FAILED); Lock::release('dbupdate'); return $retval; } else { @@ -127,7 +138,9 @@ class Update for ($x = $stored + 1; $x <= $current; $x++) { $r = self::runUpdateFunction($x, 'update'); if (!$r) { - break; + Config::set('system', 'update', Update::FAILED); + Lock::release('dbupdate'); + return $r; } } @@ -136,6 +149,7 @@ class Update self::updateSuccessfull($stored, $current); } + Config::set('system', 'update', Update::SUCCESS); Lock::release('dbupdate'); } } @@ -252,6 +266,7 @@ class Update 'uid' => $admin['uid'], 'type' => SYSTEM_EMAIL, 'to_email' => $admin['email'], + 'subject' => l10n::t('[Friendica Notify] Database update'), 'preamble' => $preamble, 'body' => $body, 'language' => $lang] @@ -290,6 +305,7 @@ class Update 'uid' => $admin['uid'], 'type' => SYSTEM_EMAIL, 'to_email' => $admin['email'], + 'subject' => l10n::t('[Friendica Notify] Database update'), 'preamble' => $preamble, 'body' => $preamble, 'language' => $lang] diff --git a/src/Core/UserImport.php b/src/Core/UserImport.php index 1e103c1f5..71767e8ce 100644 --- a/src/Core/UserImport.php +++ b/src/Core/UserImport.php @@ -6,9 +6,11 @@ namespace Friendica\Core; use Friendica\App; use Friendica\Database\DBA; +use Friendica\Database\DBStructure; use Friendica\Model\Photo; use Friendica\Object\Image; use Friendica\Util\Strings; +use Friendica\Worker\Delivery; /** * @brief UserImport class @@ -35,18 +37,24 @@ class UserImport */ private static function checkCols($table, &$arr) { - $query = sprintf("SHOW COLUMNS IN `%s`", DBA::escape($table)); - Logger::log("uimport: $query", Logger::DEBUG); - $r = q($query); + $tableColumns = DBStructure::getColumns($table); + $tcols = []; + $ttype = []; // get a plain array of column names - foreach ($r as $tcol) { + foreach ($tableColumns as $tcol) { $tcols[] = $tcol['Field']; + $ttype[$tcol['Field']] = $tcol['Type']; } // remove inexistent columns foreach ($arr as $icol => $ival) { if (!in_array($icol, $tcols)) { unset($arr[$icol]); + continue; + } + + if ($ttype[$icol] === 'datetime') { + $arr[$icol] = $ival ?? DBA::NULL_DATETIME; } } } @@ -66,16 +74,12 @@ class UserImport } self::checkCols($table, $arr); - $cols = implode("`,`", array_map(['Friendica\Database\DBA', 'escape'], array_keys($arr))); - $vals = implode("','", array_map(['Friendica\Database\DBA', 'escape'], array_values($arr))); - $query = "INSERT INTO `$table` (`$cols`) VALUES ('$vals')"; - Logger::log("uimport: $query", Logger::TRACE); if (self::IMPORT_DEBUG) { return true; } - return q($query); + return DBA::insert($table, $arr); } /** @@ -275,7 +279,7 @@ class UserImport } // send relocate messages - Worker::add(PRIORITY_HIGH, 'Notifier', 'relocate', $newuid); + Worker::add(PRIORITY_HIGH, 'Notifier', Delivery::RELOCATION, $newuid); info(L10n::t("Done. You can now login with your username and password")); $a->internalRedirect('login'); diff --git a/src/Core/Worker.php b/src/Core/Worker.php index ac5c12fdd..8fdd60c2a 100644 --- a/src/Core/Worker.php +++ b/src/Core/Worker.php @@ -27,6 +27,9 @@ class Worker const STATE_REFETCH = 3; // Worker had refetched jobs in the execution loop. const STATE_SHORT_LOOP = 4; // Worker is processing preassigned jobs, thus saving much time. + const FAST_COMMANDS = ['APDelivery', 'Delivery', 'CreateShadowEntry']; + + private static $up_start; private static $db_duration = 0; private static $db_duration_count = 0; @@ -783,23 +786,24 @@ class Worker return []; } - if ($priority <= PRIORITY_MEDIUM) { - $limit = Config::get('system', 'worker_fetch_limit', 1); - } else { - $limit = 1; - } + $limit = Config::get('system', 'worker_fetch_limit', 1); $ids = []; $stamp = (float)microtime(true); $condition = ["`priority` = ? AND `pid` = 0 AND NOT `done` AND `next_try` < ?", $priority, DateTimeFormat::utcNow()]; - $tasks = DBA::select('workerqueue', ['id'], $condition, ['limit' => $limit, 'order' => ['created']]); + $tasks = DBA::select('workerqueue', ['id', 'parameter'], $condition, ['limit' => $limit, 'order' => ['created']]); self::$db_duration += (microtime(true) - $stamp); while ($task = DBA::fetch($tasks)) { $ids[] = $task['id']; + // Only continue that loop while we are storing commands that can be processed quickly + $command = json_decode($task['parameter'])[0]; + if (!in_array($command, self::FAST_COMMANDS)) { + break; + } } DBA::close($tasks); - Logger::info('Found:', ['id' => $ids, 'priority' => $priority]); + Logger::info('Found:', ['priority' => $priority, 'id' => $ids]); return $ids; } @@ -890,15 +894,22 @@ class Worker // If there is no result we check without priority limit if (empty($ids)) { + $limit = Config::get('system', 'worker_fetch_limit', 1); + $stamp = (float)microtime(true); $condition = ["`pid` = 0 AND NOT `done` AND `next_try` < ?", DateTimeFormat::utcNow()]; - $result = DBA::select('workerqueue', ['id'], $condition, ['limit' => 1, 'order' => ['priority', 'created']]); + $tasks = DBA::select('workerqueue', ['id', 'parameter'], $condition, ['limit' => $limit, 'order' => ['priority', 'created']]); self::$db_duration += (microtime(true) - $stamp); - while ($id = DBA::fetch($result)) { - $ids[] = $id["id"]; + while ($task = DBA::fetch($tasks)) { + $ids[] = $task['id']; + // Only continue that loop while we are storing commands that can be processed quickly + $command = json_decode($task['parameter'])[0]; + if (!in_array($command, self::FAST_COMMANDS)) { + break; + } } - DBA::close($result); + DBA::close($tasks); } if (!empty($ids)) { @@ -972,7 +983,7 @@ class Worker } $url = System::baseUrl()."/worker"; - Network::fetchUrl($url, false, $redirects, 1); + Network::fetchUrl($url, false, 1); } /** @@ -1089,7 +1100,7 @@ class Worker * @param (integer|array) priority or parameter array, strings are deprecated and are ignored * * next args are passed as $cmd command line - * or: Worker::add(PRIORITY_HIGH, "Notifier", "drop", $drop_id); + * or: Worker::add(PRIORITY_HIGH, "Notifier", Delivery::DELETION, $drop_id); * or: Worker::add(array('priority' => PRIORITY_HIGH, 'dont_fork' => true), "CreateShadowEntry", $post_id); * * @return boolean "false" if proc_run couldn't be executed diff --git a/src/Database/DBA.php b/src/Database/DBA.php index 5211c0910..72769dca9 100644 --- a/src/Database/DBA.php +++ b/src/Database/DBA.php @@ -3,7 +3,6 @@ namespace Friendica\Database; use Friendica\Core\Config\Cache\IConfigCache; -use Friendica\Core\Logger; use Friendica\Core\System; use Friendica\Util\DateTimeFormat; use Friendica\Util\Profiler; @@ -13,6 +12,7 @@ use mysqli_stmt; use PDO; use PDOException; use PDOStatement; +use Psr\Log\LoggerInterface; /** * @class MySQL database class @@ -41,10 +41,11 @@ class DBA */ private static $profiler; /** - * @var string + * @var LoggerInterface */ - private static $basePath; + private static $logger; private static $server_info = ''; + /** @var PDO|mysqli */ private static $connection; private static $driver; private static $error = false; @@ -59,16 +60,16 @@ class DBA private static $db_name = ''; private static $db_charset = ''; - public static function connect($basePath, IConfigCache $configCache, Profiler $profiler, $serveraddr, $user, $pass, $db, $charset = null) + public static function connect(IConfigCache $configCache, Profiler $profiler, LoggerInterface $logger, $serveraddr, $user, $pass, $db, $charset = null) { if (!is_null(self::$connection) && self::connected()) { return true; } // We are storing these values for being able to perform a reconnect - self::$basePath = $basePath; self::$configCache = $configCache; self::$profiler = $profiler; + self::$logger = $logger; self::$db_serveraddr = $serveraddr; self::$db_user = $user; self::$db_pass = $pass; @@ -143,6 +144,21 @@ class DBA return self::$connected; } + /** + * Sets the logger for DBA + * + * @note this is necessary because if we want to load the logger configuration + * from the DB, but there's an error, we would print out an exception. + * So the logger gets updated after the logger configuration can be retrieved + * from the database + * + * @param LoggerInterface $logger + */ + public static function setLogger(LoggerInterface $logger) + { + self::$logger = $logger; + } + /** * Disconnects the current database connection */ @@ -169,7 +185,7 @@ class DBA public static function reconnect() { self::disconnect(); - $ret = self::connect(self::$basePath, self::$configCache, self::$profiler, self::$db_serveraddr, self::$db_user, self::$db_pass, self::$db_name, self::$db_charset); + $ret = self::connect(self::$configCache, self::$profiler, self::$logger, self::$db_serveraddr, self::$db_user, self::$db_pass, self::$db_name, self::$db_charset); return $ret; } @@ -273,6 +289,19 @@ class DBA } } + /** + * Removes every not whitelisted character from the identifier string + * + * @param string $identifier + * + * @return string sanitized identifier + * @throws \Exception + */ + private static function sanitizeIdentifier($identifier) + { + return preg_replace('/[^A-Za-z0-9_\-]+/', '', $identifier); + } + public static function escape($str) { if (self::$connected) { switch (self::$driver) { @@ -425,7 +454,7 @@ class DBA if ((substr_count($sql, '?') != count($args)) && (count($args) > 0)) { // Question: Should we continue or stop the query here? - Logger::warning('Query parameters mismatch.', ['query' => $sql, 'args' => $args, 'callstack' => System::callstack()]); + self::$logger->warning('Query parameters mismatch.', ['query' => $sql, 'args' => $args, 'callstack' => System::callstack()]); } $sql = self::cleanQuery($sql); @@ -468,6 +497,7 @@ class DBA break; } + /** @var $stmt mysqli_stmt|PDOStatement */ if (!$stmt = self::$connection->prepare($sql)) { $errorInfo = self::$connection->errorInfo(); self::$error = $errorInfo[2]; @@ -565,22 +595,35 @@ class DBA $error = self::$error; $errorno = self::$errorno; - Logger::log('DB Error '.self::$errorno.': '.self::$error."\n". - System::callstack(8)."\n".self::replaceParameters($sql, $args)); + self::$logger->error('DB Error', [ + 'code' => self::$errorno, + 'error' => self::$error, + 'callstack' => System::callstack(8), + 'params' => self::replaceParameters($sql, $args), + ]); // On a lost connection we try to reconnect - but only once. if ($errorno == 2006) { if (self::$in_retrial || !self::reconnect()) { // It doesn't make sense to continue when the database connection was lost if (self::$in_retrial) { - Logger::log('Giving up retrial because of database error '.$errorno.': '.$error); + self::$logger->notice('Giving up retrial because of database error', [ + 'code' => self::$errorno, + 'error' => self::$error, + ]); } else { - Logger::log("Couldn't reconnect after database error ".$errorno.': '.$error); + self::$logger->notice('Couldn\'t reconnect after database error', [ + 'code' => self::$errorno, + 'error' => self::$error, + ]); } exit(1); } else { // We try it again - Logger::log('Reconnected after database error '.$errorno.': '.$error); + self::$logger->notice('Reconnected after database error', [ + 'code' => self::$errorno, + 'error' => self::$error, + ]); self::$in_retrial = true; $ret = self::p($sql, $args); self::$in_retrial = false; @@ -649,13 +692,20 @@ class DBA $error = self::$error; $errorno = self::$errorno; - Logger::log('DB Error '.self::$errorno.': '.self::$error."\n". - System::callstack(8)."\n".self::replaceParameters($sql, $params)); + self::$logger->error('DB Error', [ + 'code' => self::$errorno, + 'error' => self::$error, + 'callstack' => System::callstack(8), + 'params' => self::replaceParameters($sql, $params), + ]); // On a lost connection we simply quit. // A reconnect like in self::p could be dangerous with modifications if ($errorno == 2006) { - Logger::log('Giving up because of database error '.$errorno.': '.$error); + self::$logger->notice('Giving up because of database error', [ + 'code' => self::$errorno, + 'error' => self::$error, + ]); exit(1); } @@ -837,6 +887,29 @@ class DBA return $columns; } + /** + * @brief Insert a row into a table + * + * @param string/array $table Table name + * + * @return string formatted and sanitzed table name + * @throws \Exception + */ + public static function formatTableName($table) + { + if (is_string($table)) { + return "`" . self::sanitizeIdentifier($table) . "`"; + } + + if (!is_array($table)) { + return ''; + } + + $scheme = key($table); + + return "`" . self::sanitizeIdentifier($scheme) . "`.`" . self::sanitizeIdentifier($table[$scheme]) . "`"; + } + /** * @brief Insert a row into a table * @@ -850,11 +923,11 @@ class DBA public static function insert($table, $param, $on_duplicate_update = false) { if (empty($table) || empty($param)) { - Logger::log('Table and fields have to be set'); + self::$logger->info('Table and fields have to be set'); return false; } - $sql = "INSERT INTO `".self::escape($table)."` (`".implode("`, `", array_keys($param))."`) VALUES (". + $sql = "INSERT INTO " . self::formatTableName($table) . " (`".implode("`, `", array_keys($param))."`) VALUES (". substr(str_repeat("?, ", count($param)), 0, -2).")"; if ($on_duplicate_update) { @@ -903,7 +976,7 @@ class DBA self::$connection->autocommit(false); } - $success = self::e("LOCK TABLES `".self::escape($table)."` WRITE"); + $success = self::e("LOCK TABLES " . self::formatTableName($table) ." WRITE"); if (self::$driver == 'pdo') { self::$connection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); @@ -1039,7 +1112,7 @@ class DBA * This process must only be started once, since the value is cached. */ private static function buildRelationData() { - $definition = DBStructure::definition(self::$basePath); + $definition = DBStructure::definition(self::$configCache->get('system', 'basepath')); foreach ($definition AS $table => $structure) { foreach ($structure['fields'] AS $field => $field_struct) { @@ -1068,7 +1141,7 @@ class DBA public static function delete($table, array $conditions, array $options = [], array &$callstack = []) { if (empty($table) || empty($conditions)) { - Logger::log('Table and conditions have to be set'); + self::$logger->info('Table and conditions have to be set'); return false; } @@ -1084,7 +1157,7 @@ class DBA $callstack[$key] = true; - $table = self::escape($table); + $table = self::sanitizeIdentifier($table); $commands[$key] = ['table' => $table, 'conditions' => $conditions]; @@ -1154,7 +1227,7 @@ class DBA if ((count($command['conditions']) > 1) || is_int($first_key)) { $sql = "DELETE FROM `" . $command['table'] . "`" . $condition_string; - Logger::log(self::replaceParameters($sql, $conditions), Logger::DATA); + self::$logger->debug(self::replaceParameters($sql, $conditions)); if (!self::e($sql, $conditions)) { if ($do_transaction) { @@ -1184,7 +1257,7 @@ class DBA $sql = "DELETE FROM `" . $table . "` WHERE `" . $field . "` IN (" . substr(str_repeat("?, ", count($field_values)), 0, -2) . ");"; - Logger::log(self::replaceParameters($sql, $field_values), Logger::DATA); + self::$logger->debug(self::replaceParameters($sql, $field_values)); if (!self::e($sql, $field_values)) { if ($do_transaction) { @@ -1233,12 +1306,10 @@ class DBA public static function update($table, $fields, $condition, $old_fields = []) { if (empty($table) || empty($fields) || empty($condition)) { - Logger::log('Table, fields and condition have to be set'); + self::$logger->info('Table, fields and condition have to be set'); return false; } - $table = self::escape($table); - $condition_string = self::buildCondition($condition); if (is_bool($old_fields)) { @@ -1271,7 +1342,7 @@ class DBA return true; } - $sql = "UPDATE `".$table."` SET `". + $sql = "UPDATE ". self::formatTableName($table) . " SET `". implode("` = ?, `", array_keys($fields))."` = ?".$condition_string; $params1 = array_values($fields); @@ -1332,12 +1403,10 @@ class DBA */ public static function select($table, array $fields = [], array $condition = [], array $params = []) { - if ($table == '') { + if (empty($table)) { return false; } - $table = self::escape($table); - if (count($fields) > 0) { $select_fields = "`" . implode("`, `", array_values($fields)) . "`"; } else { @@ -1348,7 +1417,7 @@ class DBA $param_string = self::buildParameter($params); - $sql = "SELECT " . $select_fields . " FROM `" . $table . "`" . $condition_string . $param_string; + $sql = "SELECT " . $select_fields . " FROM " . self::formatTableName($table) . $condition_string . $param_string; $result = self::p($sql, $condition); @@ -1375,13 +1444,13 @@ class DBA */ public static function count($table, array $condition = []) { - if ($table == '') { + if (empty($table)) { return false; } $condition_string = self::buildCondition($condition); - $sql = "SELECT COUNT(*) AS `count` FROM `".$table."`".$condition_string; + $sql = "SELECT COUNT(*) AS `count` FROM " . self::formatTableName($table) . $condition_string; $row = self::fetchFirst($sql, $condition); @@ -1449,6 +1518,8 @@ class DBA $new_values = array_merge($new_values, array_values($value)); $placeholders = substr(str_repeat("?, ", count($value)), 0, -2); $condition_string .= "`" . $field . "` IN (" . $placeholders . ")"; + } elseif (is_null($value)) { + $condition_string .= "`" . $field . "` IS NULL"; } else { $new_values[$field] = $value; $condition_string .= "`" . $field . "` = ?"; @@ -1470,11 +1541,22 @@ class DBA */ public static function buildParameter(array $params = []) { + $groupby_string = ''; + if (isset($params['group_by'])) { + $groupby_string = " GROUP BY "; + foreach ($params['group_by'] as $fields) { + $groupby_string .= "`" . $fields . "`, "; + } + $groupby_string = substr($groupby_string, 0, -2); + } + $order_string = ''; if (isset($params['order'])) { $order_string = " ORDER BY "; foreach ($params['order'] AS $fields => $order) { - if (!is_int($fields)) { + if ($order === 'RAND()') { + $order_string .= "RAND(), "; + } elseif (!is_int($fields)) { $order_string .= "`" . $fields . "` " . ($order ? "DESC" : "ASC") . ", "; } else { $order_string .= "`" . $order . "`, "; @@ -1492,7 +1574,7 @@ class DBA $limit_string = " LIMIT " . intval($params['limit'][0]) . ", " . intval($params['limit'][1]); } - return $order_string.$limit_string; + return $groupby_string . $order_string . $limit_string; } /** diff --git a/src/Database/DBStructure.php b/src/Database/DBStructure.php index 75a5c8624..abbac4e78 100644 --- a/src/Database/DBStructure.php +++ b/src/Database/DBStructure.php @@ -844,4 +844,18 @@ class DBStructure return $retval; } + + /** + * Returns the columns of a table + * + * @param string $table Table name + * + * @return array An array of the table columns + * @throws Exception + */ + public static function getColumns($table) + { + $stmtColumns = DBA::p("SHOW COLUMNS FROM `" . $table . "`"); + return DBA::toArray($stmtColumns); + } } diff --git a/src/Core/Cache/CacheDriverFactory.php b/src/Factory/CacheDriverFactory.php similarity index 63% rename from src/Core/Cache/CacheDriverFactory.php rename to src/Factory/CacheDriverFactory.php index 307f52a2e..390534a70 100644 --- a/src/Core/Cache/CacheDriverFactory.php +++ b/src/Factory/CacheDriverFactory.php @@ -1,7 +1,9 @@ loadConfigFiles($configCache); + $loader->setupCache($configCache); return $configCache; } diff --git a/src/Factory/DBFactory.php b/src/Factory/DBFactory.php index b4f0c9e3c..7caa63ec4 100644 --- a/src/Factory/DBFactory.php +++ b/src/Factory/DBFactory.php @@ -4,23 +4,22 @@ namespace Friendica\Factory; use Friendica\Core\Config\Cache; use Friendica\Database; +use Friendica\Util\Logger\VoidLogger; use Friendica\Util\Profiler; +use ParagonIE\HiddenString\HiddenString; class DBFactory { /** * Initialize the DBA connection * - * @param string $basePath The basepath of the application * @param Cache\IConfigCache $configCache The configuration cache * @param Profiler $profiler The profiler * @param array $server The $_SERVER variables * * @throws \Exception if connection went bad - * - * @todo refactor basedir during https://github.com/friendica/friendica/issues/6720 */ - public static function init($basePath, Cache\IConfigCache $configCache, Profiler $profiler, array $server) + public static function init(Cache\IConfigCache $configCache, Profiler $profiler, array $server) { if (Database\DBA::connected()) { return; @@ -47,13 +46,13 @@ class DBFactory } else { $db_user = $server['MYSQL_USER']; } - $db_pass = (string) $server['MYSQL_PASSWORD']; + $db_pass = new HiddenString((string) $server['MYSQL_PASSWORD']); $db_data = $server['MYSQL_DATABASE']; } - if (Database\DBA::connect($basePath, $configCache, $profiler, $db_host, $db_user, $db_pass, $db_data, $charset)) { + if (Database\DBA::connect($configCache, $profiler, new VoidLogger(), $db_host, $db_user, $db_pass, $db_data, $charset)) { // Loads DB_UPDATE_VERSION constant - Database\DBStructure::definition($basePath, false); + Database\DBStructure::definition($configCache->get('system', 'basepath'), false); } unset($db_host, $db_user, $db_pass, $db_data, $charset); diff --git a/src/Factory/DependencyFactory.php b/src/Factory/DependencyFactory.php index 9d84e324a..aacd15506 100644 --- a/src/Factory/DependencyFactory.php +++ b/src/Factory/DependencyFactory.php @@ -3,9 +3,11 @@ namespace Friendica\Factory; use Friendica\App; -use Friendica\Core\Config\Cache; +use Friendica\Database\DBA; use Friendica\Factory; use Friendica\Util\BasePath; +use Friendica\Util\BaseURL; +use Friendica\Util\Config; class DependencyFactory { @@ -23,16 +25,20 @@ class DependencyFactory public static function setUp($channel, $directory, $isBackend = true) { $basePath = BasePath::create($directory, $_SERVER); - $configLoader = new Cache\ConfigCacheLoader($basePath); + $mode = new App\Mode($basePath); + $router = new App\Router(); + $configLoader = new Config\ConfigFileLoader($basePath, $mode); $configCache = Factory\ConfigFactory::createCache($configLoader); $profiler = Factory\ProfilerFactory::create($configCache); - Factory\DBFactory::init($basePath, $configCache, $profiler, $_SERVER); + Factory\DBFactory::init($configCache, $profiler, $_SERVER); $config = Factory\ConfigFactory::createConfig($configCache); // needed to call PConfig::init() Factory\ConfigFactory::createPConfig($configCache); - $logger = Factory\LoggerFactory::create($channel, $config); - Factory\LoggerFactory::createDev($channel, $config); + $logger = Factory\LoggerFactory::create($channel, $config, $profiler); + DBA::setLogger($logger); + Factory\LoggerFactory::createDev($channel, $config, $profiler); + $baseURL = new BaseURL($config, $_SERVER); - return new App($basePath, $config, $logger, $profiler, $isBackend); + return new App($config, $mode, $router, $baseURL, $logger, $profiler, $isBackend); } } diff --git a/src/Factory/LoggerFactory.php b/src/Factory/LoggerFactory.php index 81c15bdb5..bdd85cf3a 100644 --- a/src/Factory/LoggerFactory.php +++ b/src/Factory/LoggerFactory.php @@ -5,9 +5,13 @@ namespace Friendica\Factory; use Friendica\Core\Config\Configuration; use Friendica\Core\Logger; use Friendica\Network\HTTPException\InternalServerErrorException; -use Friendica\Util\Logger\FriendicaDevelopHandler; -use Friendica\Util\Logger\FriendicaIntrospectionProcessor; -use Friendica\Util\Logger\WorkerLogger; +use Friendica\Util\Introspection; +use Friendica\Util\Logger\Monolog\DevelopHandler; +use Friendica\Util\Logger\Monolog\IntrospectionProcessor; +use Friendica\Util\Logger\ProfilerLogger; +use Friendica\Util\Logger\StreamLogger; +use Friendica\Util\Logger\SyslogLogger; +use Friendica\Util\Logger\VoidLogger; use Friendica\Util\Profiler; use Monolog; use Psr\Log\LoggerInterface; @@ -22,42 +26,79 @@ class LoggerFactory { /** * A list of classes, which shouldn't get logged + * * @var array */ private static $ignoreClassList = [ Logger::class, Profiler::class, - WorkerLogger::class + 'Friendica\\Util\\Logger', ]; /** * Creates a new PSR-3 compliant logger instances * - * @param string $channel The channel of the logger instance - * @param Configuration $config The config + * @param string $channel The channel of the logger instance + * @param Configuration $config The config + * @param Profiler $profiler The profiler of the app * * @return LoggerInterface The PSR-3 compliant logger instance + * + * @throws \Exception + * @throws InternalServerErrorException */ - public static function create($channel, Configuration $config) + public static function create($channel, Configuration $config, Profiler $profiler) { - $loggerTimeZone = new \DateTimeZone('UTC'); - Monolog\Logger::setTimezone($loggerTimeZone); + if (empty($config->get('system', 'debugging', false))) { + $logger = new VoidLogger(); + Logger::init($logger); + return $logger; + } - $logger = new Monolog\Logger($channel); - $logger->pushProcessor(new Monolog\Processor\PsrLogMessageProcessor()); - $logger->pushProcessor(new Monolog\Processor\ProcessIdProcessor()); - $logger->pushProcessor(new Monolog\Processor\UidProcessor()); - $logger->pushProcessor(new FriendicaIntrospectionProcessor(LogLevel::DEBUG, self::$ignoreClassList)); + $introspection = new Introspection(self::$ignoreClassList); + $level = $config->get('system', 'loglevel'); + $loglevel = self::mapLegacyConfigDebugLevel((string)$level); - $debugging = $config->get('system', 'debugging'); - $stream = $config->get('system', 'logfile'); - $level = $config->get('system', 'loglevel'); + switch ($config->get('system', 'logger_config', 'stream')) { + case 'monolog': + $loggerTimeZone = new \DateTimeZone('UTC'); + Monolog\Logger::setTimezone($loggerTimeZone); - if ($debugging) { - $loglevel = self::mapLegacyConfigDebugLevel((string)$level); - static::addStreamHandler($logger, $stream, $loglevel); - } else { - static::addVoidHandler($logger); + $logger = new Monolog\Logger($channel); + $logger->pushProcessor(new Monolog\Processor\PsrLogMessageProcessor()); + $logger->pushProcessor(new Monolog\Processor\ProcessIdProcessor()); + $logger->pushProcessor(new Monolog\Processor\UidProcessor()); + $logger->pushProcessor(new IntrospectionProcessor($introspection, LogLevel::DEBUG)); + + $stream = $config->get('system', 'logfile'); + + // just add a stream in case it's either writable or not file + if (!is_file($stream) || is_writable($stream)) { + static::addStreamHandler($logger, $stream, $loglevel); + } + break; + + case 'syslog': + $logger = new SyslogLogger($channel, $introspection, $loglevel); + break; + + case 'stream': + default: + $stream = $config->get('system', 'logfile'); + // just add a stream in case it's either writable or not file + if (!is_file($stream) || is_writable($stream)) { + $logger = new StreamLogger($channel, $stream, $introspection, $loglevel); + } else { + $logger = new VoidLogger(); + } + break; + } + + $profiling = $config->get('system', 'profiling', false); + + // In case profiling is enabled, wrap the ProfilerLogger around the current logger + if (isset($profiling) && $profiling !== false) { + $logger = new ProfilerLogger($logger, $profiler); } Logger::init($logger); @@ -73,33 +114,66 @@ class LoggerFactory * * It should never get filled during normal usage of Friendica * - * @param string $channel The channel of the logger instance - * @param Configuration $config The config + * @param string $channel The channel of the logger instance + * @param Configuration $config The config + * @param Profiler $profiler The profiler of the app * * @return LoggerInterface The PSR-3 compliant logger instance + * + * @throws InternalServerErrorException + * @throws \Exception */ - public static function createDev($channel, Configuration $config) + public static function createDev($channel, Configuration $config, Profiler $profiler) { $debugging = $config->get('system', 'debugging'); $stream = $config->get('system', 'dlogfile'); $developerIp = $config->get('system', 'dlogip'); - if (!isset($developerIp) || !$debugging) { - return null; + if ((!isset($developerIp) || !$debugging) && + (!is_file($stream) || is_writable($stream))) { + $logger = new VoidLogger(); + Logger::setDevLogger($logger); + return $logger; } $loggerTimeZone = new \DateTimeZone('UTC'); Monolog\Logger::setTimezone($loggerTimeZone); - $logger = new Monolog\Logger($channel); - $logger->pushProcessor(new Monolog\Processor\PsrLogMessageProcessor()); - $logger->pushProcessor(new Monolog\Processor\ProcessIdProcessor()); - $logger->pushProcessor(new Monolog\Processor\UidProcessor()); - $logger->pushProcessor(new FriendicaIntrospectionProcessor(LogLevel::DEBUG, self::$ignoreClassList)); + $introspection = new Introspection(self::$ignoreClassList); - $logger->pushHandler(new FriendicaDevelopHandler($developerIp)); + switch ($config->get('system', 'logger_config', 'stream')) { - static::addStreamHandler($logger, $stream, LogLevel::DEBUG); + case 'monolog': + $loggerTimeZone = new \DateTimeZone('UTC'); + Monolog\Logger::setTimezone($loggerTimeZone); + + $logger = new Monolog\Logger($channel); + $logger->pushProcessor(new Monolog\Processor\PsrLogMessageProcessor()); + $logger->pushProcessor(new Monolog\Processor\ProcessIdProcessor()); + $logger->pushProcessor(new Monolog\Processor\UidProcessor()); + $logger->pushProcessor(new IntrospectionProcessor($introspection, LogLevel::DEBUG)); + + $logger->pushHandler(new DevelopHandler($developerIp)); + + static::addStreamHandler($logger, $stream, LogLevel::DEBUG); + break; + + case 'syslog': + $logger = new SyslogLogger($channel, $introspection, LogLevel::DEBUG); + break; + + case 'stream': + default: + $logger = new StreamLogger($channel, $stream, $introspection, LogLevel::DEBUG); + break; + } + + $profiling = $config->get('system', 'profiling', false); + + // In case profiling is enabled, wrap the ProfilerLogger around the current logger + if (isset($profiling) && $profiling !== false) { + $logger = new ProfilerLogger($logger, $profiler); + } Logger::setDevLogger($logger); @@ -108,6 +182,7 @@ class LoggerFactory /** * Mapping a legacy level to the PSR-3 compliant levels + * * @see https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md#5-psrlogloglevel * * @param string $level the level to be mapped @@ -144,9 +219,9 @@ class LoggerFactory /** * Adding a handler to a given logger instance * - * @param LoggerInterface $logger The logger instance - * @param mixed $stream The stream which handles the logger output - * @param string $level The level, for which this handler at least should handle logging + * @param LoggerInterface $logger The logger instance + * @param mixed $stream The stream which handles the logger output + * @param string $level The level, for which this handler at least should handle logging * * @return void * diff --git a/src/Factory/README.md b/src/Factory/README.md new file mode 100644 index 000000000..572d9403c --- /dev/null +++ b/src/Factory/README.md @@ -0,0 +1,9 @@ +## Friendica\Factory + +This namespace contains Factories. +A Factory is used to create specific objects based on its configuration. + +See [Factory Method](https://designpatternsphp.readthedocs.io/en/latest/Creational/FactoryMethod/README.html) + +Use the classes inside this directory if you want to change the way how new objects should get created. +Don't use the classes to change the behaviour of the concrete objects. diff --git a/src/Model/APContact.php b/src/Model/APContact.php index bf36306b4..b027d6c47 100644 --- a/src/Model/APContact.php +++ b/src/Model/APContact.php @@ -9,6 +9,7 @@ namespace Friendica\Model; use Friendica\BaseObject; use Friendica\Content\Text\HTML; use Friendica\Core\Logger; +use Friendica\Core\Config; use Friendica\Database\DBA; use Friendica\Protocol\ActivityPub; use Friendica\Util\Network; @@ -22,21 +23,30 @@ class APContact extends BaseObject * Resolves the profile url from the address by using webfinger * * @param string $addr profile address (user@domain.tld) - * @return string url + * @param string $url profile URL. When set then we return "true" when this profile url can be found at the address + * @return string|boolean url * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - private static function addrToUrl($addr) + private static function addrToUrl($addr, $url = null) { $addr_parts = explode('@', $addr); if (count($addr_parts) != 2) { return false; } + $xrd_timeout = Config::get('system', 'xrd_timeout'); + $webfinger = 'https://' . $addr_parts[1] . '/.well-known/webfinger?resource=acct:' . urlencode($addr); - $curlResult = Network::curl($webfinger, false, $redirects, ['accept_content' => 'application/jrd+json,application/json']); + $curlResult = Network::curl($webfinger, false, ['timeout' => $xrd_timeout, 'accept_content' => 'application/jrd+json,application/json']); if (!$curlResult->isSuccess() || empty($curlResult->getBody())) { - return false; + $webfinger = Strings::normaliseLink($webfinger); + + $curlResult = Network::curl($webfinger, false, ['timeout' => $xrd_timeout, 'accept_content' => 'application/jrd+json,application/json']); + + if (!$curlResult->isSuccess() || empty($curlResult->getBody())) { + return false; + } } $data = json_decode($curlResult->getBody(), true); @@ -46,11 +56,15 @@ class APContact extends BaseObject } foreach ($data['links'] as $link) { + if (!empty($url) && !empty($link['href']) && ($link['href'] == $url)) { + return true; + } + if (empty($link['href']) || empty($link['rel']) || empty($link['type'])) { continue; } - if (($link['rel'] == 'self') && ($link['type'] == 'application/activity+json')) { + if (empty($url) && ($link['rel'] == 'self') && ($link['type'] == 'application/activity+json')) { return $link['href']; } } @@ -73,6 +87,8 @@ class APContact extends BaseObject return false; } + $fetched_contact = false; + if (empty($update)) { if (is_null($update)) { $ref_update = DateTimeFormat::utc('now - 1 month'); @@ -89,55 +105,62 @@ class APContact extends BaseObject $apcontact = DBA::selectFirst('apcontact', [], ['addr' => $url]); } - if (DBA::isResult($apcontact) && ($apcontact['updated'] > $ref_update)) { + if (DBA::isResult($apcontact) && ($apcontact['updated'] > $ref_update) && !empty($apcontact['pubkey'])) { return $apcontact; } if (!is_null($update)) { - return false; + return DBA::isResult($apcontact) ? $apcontact : false; + } + + if (DBA::isResult($apcontact)) { + $fetched_contact = $apcontact; } } if (empty(parse_url($url, PHP_URL_SCHEME))) { $url = self::addrToUrl($url); if (empty($url)) { - return false; + return $fetched_contact; } } $data = ActivityPub::fetchContent($url); if (empty($data)) { - return false; + return $fetched_contact; } $compacted = JsonLD::compact($data); if (empty($compacted['@id'])) { - return false; + return $fetched_contact; } $apcontact = []; $apcontact['url'] = $compacted['@id']; - $apcontact['uuid'] = JsonLD::fetchElement($compacted, 'diaspora:guid'); + $apcontact['uuid'] = JsonLD::fetchElement($compacted, 'diaspora:guid', '@value'); $apcontact['type'] = str_replace('as:', '', JsonLD::fetchElement($compacted, '@type')); $apcontact['following'] = JsonLD::fetchElement($compacted, 'as:following', '@id'); $apcontact['followers'] = JsonLD::fetchElement($compacted, 'as:followers', '@id'); $apcontact['inbox'] = JsonLD::fetchElement($compacted, 'ldp:inbox', '@id'); + self::unarchiveInbox($apcontact['inbox'], false); + $apcontact['outbox'] = JsonLD::fetchElement($compacted, 'as:outbox', '@id'); $apcontact['sharedinbox'] = ''; if (!empty($compacted['as:endpoints'])) { $apcontact['sharedinbox'] = JsonLD::fetchElement($compacted['as:endpoints'], 'as:sharedInbox', '@id'); + self::unarchiveInbox($apcontact['sharedinbox'], true); } - $apcontact['nick'] = JsonLD::fetchElement($compacted, 'as:preferredUsername'); - $apcontact['name'] = JsonLD::fetchElement($compacted, 'as:name'); + $apcontact['nick'] = JsonLD::fetchElement($compacted, 'as:preferredUsername', '@value'); + $apcontact['name'] = JsonLD::fetchElement($compacted, 'as:name', '@value'); if (empty($apcontact['name'])) { $apcontact['name'] = $apcontact['nick']; } - $apcontact['about'] = HTML::toBBCode(JsonLD::fetchElement($compacted, 'as:summary')); + $apcontact['about'] = HTML::toBBCode(JsonLD::fetchElement($compacted, 'as:summary', '@value')); $apcontact['photo'] = JsonLD::fetchElement($compacted, 'as:icon', '@id'); if (is_array($apcontact['photo']) || !empty($compacted['as:icon']['as:url']['@id'])) { @@ -149,8 +172,14 @@ class APContact extends BaseObject $apcontact['alias'] = JsonLD::fetchElement($compacted['as:url'], 'as:href', '@id'); } - if (empty($apcontact['url']) || empty($apcontact['inbox'])) { - return false; + // Quit if none of the basic values are set + if (empty($apcontact['url']) || empty($apcontact['inbox']) || empty($apcontact['type'])) { + return $fetched_contact; + } + + // Quit if this doesn't seem to be an account at all + if (!in_array($apcontact['type'], ActivityPub::ACCOUNT_TYPES)) { + return $fetched_contact; } $parts = parse_url($apcontact['url']); @@ -158,14 +187,21 @@ class APContact extends BaseObject unset($parts['path']); $apcontact['addr'] = $apcontact['nick'] . '@' . str_replace('//', '', Network::unparseURL($parts)); - $apcontact['pubkey'] = trim(JsonLD::fetchElement($compacted, 'w3id:publicKey', 'w3id:publicKeyPem')); + if (!empty($compacted['w3id:publicKey'])) { + $apcontact['pubkey'] = trim(JsonLD::fetchElement($compacted['w3id:publicKey'], 'w3id:publicKeyPem', '@value')); + } $apcontact['manually-approve'] = (int)JsonLD::fetchElement($compacted, 'as:manuallyApprovesFollowers'); + if (!empty($compacted['as:generator'])) { + $apcontact['baseurl'] = JsonLD::fetchElement($compacted['as:generator'], 'as:url', '@id'); + $apcontact['generator'] = JsonLD::fetchElement($compacted['as:generator'], 'as:name', '@value'); + } + // To-Do // Unhandled - // @context, tag, attachment, image, nomadicLocations, signature, following, followers, featured, movedTo, liked + // tag, attachment, image, nomadicLocations, signature, featured, movedTo, liked // Unhandled from Misskey // sharedInbox, isCat @@ -173,13 +209,18 @@ class APContact extends BaseObject // Unhandled from Kroeg // kroeg:blocks, updated - // Check if the address is resolvable - if (self::addrToUrl($apcontact['addr']) == $apcontact['url']) { - $parts = parse_url($apcontact['url']); - unset($parts['path']); - $apcontact['baseurl'] = Network::unparseURL($parts); + $parts = parse_url($apcontact['url']); + unset($parts['path']); + $baseurl = Network::unparseURL($parts); + + // Check if the address is resolvable or the profile url is identical with the base url of the system + if (self::addrToUrl($apcontact['addr'], $apcontact['url']) || Strings::compareLink($apcontact['url'], $baseurl)) { + $apcontact['baseurl'] = $baseurl; } else { $apcontact['addr'] = null; + } + + if (empty($apcontact['baseurl'])) { $apcontact['baseurl'] = null; } @@ -191,6 +232,11 @@ class APContact extends BaseObject DBA::update('apcontact', $apcontact, ['url' => $url], true); + // We delete the old entry when the URL is changed + if (($url != $apcontact['url']) && DBA::exists('apcontact', ['url' => $url]) && DBA::exists('apcontact', ['url' => $apcontact['url']])) { + DBA::delete('apcontact', ['url' => $url]); + } + // Update some data in the contact table with various ways to catch them all $contact_fields = ['name' => $apcontact['name'], 'about' => $apcontact['about'], 'alias' => $apcontact['alias']]; @@ -215,11 +261,13 @@ class APContact extends BaseObject DBA::update('contact', $contact_fields, ['nurl' => Strings::normaliseLink($url)]); - $contacts = DBA::select('contact', ['uid', 'id'], ['nurl' => Strings::normaliseLink($url)]); - while ($contact = DBA::fetch($contacts)) { - Contact::updateAvatar($apcontact['photo'], $contact['uid'], $contact['id']); + if (!empty($apcontact['photo'])) { + $contacts = DBA::select('contact', ['uid', 'id'], ['nurl' => Strings::normaliseLink($url)]); + while ($contact = DBA::fetch($contacts)) { + Contact::updateAvatar($apcontact['photo'], $contact['uid'], $contact['id']); + } + DBA::close($contacts); } - DBA::close($contacts); // Update the gcontact table // These two fields don't exist in the gcontact table @@ -231,4 +279,27 @@ class APContact extends BaseObject return $apcontact; } + + /** + * Unarchive inboxes + * + * @param string $url inbox url + */ + private static function unarchiveInbox($url, $shared) + { + if (empty($url)) { + return; + } + + $now = DateTimeFormat::utcNow(); + + $fields = ['archive' => false, 'success' => $now, 'shared' => $shared]; + + if (!DBA::exists('inbox-status', ['url' => $url])) { + $fields = array_merge($fields, ['url' => $url, 'created' => $now]); + DBA::insert('inbox-status', $fields); + } else { + DBA::update('inbox-status', $fields, ['url' => $url]); + } + } } diff --git a/src/Model/Contact.php b/src/Model/Contact.php index d938ad29c..a6026d644 100644 --- a/src/Model/Contact.php +++ b/src/Model/Contact.php @@ -22,6 +22,7 @@ use Friendica\Protocol\Diaspora; use Friendica\Protocol\OStatus; use Friendica\Protocol\PortableContact; use Friendica\Protocol\Salmon; +use Friendica\Util\BaseURL; use Friendica\Util\DateTimeFormat; use Friendica\Util\Network; use Friendica\Util\Strings; @@ -109,6 +110,45 @@ class Contact extends BaseObject * @} */ + /** + * @param array $fields Array of selected fields, empty for all + * @param array $condition Array of fields for condition + * @param array $params Array of several parameters + * @return array + * @throws \Exception + */ + public static function select(array $fields = [], array $condition = [], array $params = []) + { + $statement = DBA::select('contact', $fields, $condition, $params); + + return DBA::toArray($statement); + } + + /** + * @param array $fields Array of selected fields, empty for all + * @param array $condition Array of fields for condition + * @param array $params Array of several parameters + * @return array + * @throws \Exception + */ + public static function selectFirst(array $fields = [], array $condition = [], array $params = []) + { + $contact = DBA::selectFirst('contact', $fields, $condition, $params); + + return $contact; + } + + /** + * @param integer $id Contact ID + * @param array $fields Array of selected fields, empty for all + * @return array|boolean Contact record if it exists, false otherwise + * @throws \Exception + */ + public static function getById($id, $fields = []) + { + return DBA::selectFirst('contact', $fields, ['id' => $id]); + } + /** * @brief Tests if the given contact is a follower * @@ -212,6 +252,28 @@ class Contact extends BaseObject return ['public' => $pcid, 'user' => $ucid]; } + /** + * Returns contact details for a given contact id in combination with a user id + * + * @param int $cid A contact ID + * @param int $uid The User ID + * @param array $fields The selected fields for the contact + * + * @return array The contact details + * + * @throws \Exception + */ + public static function getContactForUser($cid, $uid, array $fields = []) + { + $contact = DBA::selectFirst('contact', $fields, ['id' => $cid, 'uid' => $uid]); + + if (!DBA::isResult($contact)) { + return []; + } else { + return $contact; + } + } + /** * @brief Block contact id for user id * @@ -531,11 +593,13 @@ class Contact extends BaseObject return; } + $file_suffix = 'jpg'; + $fields = ['name' => $profile['name'], 'nick' => $user['nickname'], 'avatar-date' => $self['avatar-date'], 'location' => Profile::formatLocation($profile), 'about' => $profile['about'], 'keywords' => $profile['pub_keywords'], - 'gender' => $profile['gender'], 'avatar' => $profile['photo'], - 'contact-type' => $user['account-type'], 'xmpp' => $profile['xmpp']]; + 'gender' => $profile['gender'], 'contact-type' => $user['account-type'], + 'xmpp' => $profile['xmpp']]; $avatar = Photo::selectFirst(['resource-id', 'type'], ['uid' => $uid, 'profile' => true]); if (DBA::isResult($avatar)) { @@ -547,8 +611,6 @@ class Contact extends BaseObject $types = Image::supportedTypes(); if (isset($types[$avatar['type']])) { $file_suffix = $types[$avatar['type']]; - } else { - $file_suffix = 'jpg'; } // We are adding a timestamp value so that other systems won't use cached content @@ -567,6 +629,7 @@ class Contact extends BaseObject $fields['micro'] = System::baseUrl() . '/images/person-48.jpg'; } + $fields['avatar'] = System::baseUrl() . '/photo/profile/' .$uid . '.' . $file_suffix; $fields['forum'] = $user['page-flags'] == User::PAGE_FLAGS_COMMUNITY; $fields['prv'] = $user['page-flags'] == User::PAGE_FLAGS_PRVGROUP; @@ -589,15 +652,18 @@ class Contact extends BaseObject } if ($update) { - $fields['name-date'] = DateTimeFormat::utcNow(); + if ($fields['name'] != $self['name']) { + $fields['name-date'] = DateTimeFormat::utcNow(); + } + $fields['updated'] = DateTimeFormat::utcNow(); DBA::update('contact', $fields, ['id' => $self['id']]); // Update the public contact as well DBA::update('contact', $fields, ['uid' => 0, 'nurl' => $self['nurl']]); // Update the profile - $fields = ['photo' => System::baseUrl() . '/photo/profile/' .$uid . '.jpg', - 'thumb' => System::baseUrl() . '/photo/avatar/' . $uid .'.jpg']; + $fields = ['photo' => System::baseUrl() . '/photo/profile/' .$uid . '.' . $file_suffix, + 'thumb' => System::baseUrl() . '/photo/avatar/' . $uid .'.' . $file_suffix]; DBA::update('profile', $fields, ['uid' => $uid, 'is-default' => true]); } } @@ -639,9 +705,15 @@ class Contact extends BaseObject if (empty($contact['network'])) { return; } - if (($contact['network'] == Protocol::DFRN) && $dissolve) { + + $protocol = $contact['network']; + if (($protocol == Protocol::DFRN) && !self::isLegacyDFRNContact($contact)) { + $protocol = Protocol::ACTIVITYPUB; + } + + if (($protocol == Protocol::DFRN) && $dissolve) { DFRN::deliver($user, $contact, 'placeholder', true); - } elseif (in_array($contact['network'], [Protocol::OSTATUS, Protocol::DFRN])) { + } elseif (in_array($protocol, [Protocol::OSTATUS, Protocol::DFRN])) { // create an unfollow slap $item = []; $item['verb'] = NAMESPACE_OSTATUS . "/unfollow"; @@ -656,9 +728,9 @@ class Contact extends BaseObject if (!empty($contact['notify'])) { Salmon::slapper($user, $contact['notify'], $slap); } - } elseif ($contact['network'] == Protocol::DIASPORA) { + } elseif ($protocol == Protocol::DIASPORA) { Diaspora::sendUnshare($user, $contact); - } elseif ($contact['network'] == Protocol::ACTIVITYPUB) { + } elseif ($protocol == Protocol::ACTIVITYPUB) { ActivityPub\Transmitter::sendContactUndo($contact['url'], $contact['id'], $user['uid']); if ($dissolve) { @@ -837,7 +909,7 @@ class Contact extends BaseObject // If there is more than one entry we filter out the connector networks if (count($r) > 1) { foreach ($r as $id => $result) { - if ($result["network"] == Protocol::STATUSNET) { + if (!in_array($result["network"], Protocol::NATIVE_SUPPORT)) { unset($r[$id]); } } @@ -1021,7 +1093,7 @@ class Contact extends BaseObject $profile_link = $profile_link . '?tab=profile'; } - if (in_array($contact['network'], [Protocol::DFRN, Protocol::DIASPORA]) && !$contact['self']) { + if (self::canReceivePrivateMessages($contact)) { $pm_url = System::baseUrl() . '/message/new/' . $contact['id']; } @@ -1104,6 +1176,90 @@ class Contact extends BaseObject )", intval($uid), intval($uid)); } + /** + * Have a look at all contact tables for a given profile url. + * This function works as a replacement for probing the contact. + * + * @param string $url Contact URL + * @param integer $cid Contact ID + * + * @return array Contact array in the "probe" structure + */ + private static function getProbeDataFromDatabase($url, $cid = null) + { + // The link could be provided as http although we stored it as https + $ssl_url = str_replace('http://', 'https://', $url); + + $fields = ['id', 'uid', 'url', 'addr', 'alias', 'notify', 'poll', 'name', 'nick', + 'photo', 'keywords', 'location', 'about', 'network', + 'priority', 'batch', 'request', 'confirm', 'poco']; + + if (!empty($cid)) { + $data = DBA::selectFirst('contact', $fields, ['id' => $cid]); + if (DBA::isResult($data)) { + return $data; + } + } + + $data = DBA::selectFirst('contact', $fields, ['nurl' => Strings::normaliseLink($url)]); + + if (!DBA::isResult($data)) { + $condition = ['alias' => [$url, Strings::normaliseLink($url), $ssl_url]]; + $data = DBA::selectFirst('contact', $fields, $condition); + } + + if (DBA::isResult($data)) { + // For security reasons we don't fetch key data from our users + $data["pubkey"] = ''; + return $data; + } + + $fields = ['url', 'addr', 'alias', 'notify', 'name', 'nick', + 'photo', 'keywords', 'location', 'about', 'network']; + $data = DBA::selectFirst('gcontact', $fields, ['nurl' => Strings::normaliseLink($url)]); + + if (!DBA::isResult($data)) { + $condition = ['alias' => [$url, Strings::normaliseLink($url), $ssl_url]]; + $data = DBA::selectFirst('contact', $fields, $condition); + } + + if (DBA::isResult($data)) { + $data["pubkey"] = ''; + $data["poll"] = ''; + $data["priority"] = 0; + $data["batch"] = ''; + $data["request"] = ''; + $data["confirm"] = ''; + $data["poco"] = ''; + return $data; + } + + $data = ActivityPub::probeProfile($url, false); + if (!empty($data)) { + return $data; + } + + $fields = ['url', 'addr', 'alias', 'notify', 'poll', 'name', 'nick', + 'photo', 'network', 'priority', 'batch', 'request', 'confirm']; + $data = DBA::selectFirst('fcontact', $fields, ['url' => $url]); + + if (!DBA::isResult($data)) { + $condition = ['alias' => [$url, Strings::normaliseLink($url), $ssl_url]]; + $data = DBA::selectFirst('contact', $fields, $condition); + } + + if (DBA::isResult($data)) { + $data["pubkey"] = ''; + $data["keywords"] = ''; + $data["location"] = ''; + $data["about"] = ''; + $data["poco"] = ''; + return $data; + } + + return []; + } + /** * @brief Fetch the contact id for a given URL and user * @@ -1144,11 +1300,11 @@ class Contact extends BaseObject /// @todo Verify if we can't use Contact::getDetailsByUrl instead of the following // We first try the nurl (http://server.tld/nick), most common case - $contact = DBA::selectFirst('contact', ['id', 'avatar', 'avatar-date'], ['nurl' => Strings::normaliseLink($url), 'uid' => $uid, 'deleted' => false]); + $contact = DBA::selectFirst('contact', ['id', 'avatar', 'updated', 'network'], ['nurl' => Strings::normaliseLink($url), 'uid' => $uid, 'deleted' => false]); // Then the addr (nick@server.tld) if (!DBA::isResult($contact)) { - $contact = DBA::selectFirst('contact', ['id', 'avatar', 'avatar-date'], ['addr' => $url, 'uid' => $uid, 'deleted' => false]); + $contact = DBA::selectFirst('contact', ['id', 'avatar', 'updated', 'network'], ['addr' => $url, 'uid' => $uid, 'deleted' => false]); } // Then the alias (which could be anything) @@ -1156,19 +1312,25 @@ class Contact extends BaseObject // The link could be provided as http although we stored it as https $ssl_url = str_replace('http://', 'https://', $url); $condition = ['`alias` IN (?, ?, ?) AND `uid` = ? AND NOT `deleted`', $url, Strings::normaliseLink($url), $ssl_url, $uid]; - $contact = DBA::selectFirst('contact', ['id', 'avatar', 'avatar-date'], $condition); + $contact = DBA::selectFirst('contact', ['id', 'avatar', 'updated', 'network'], $condition); } if (DBA::isResult($contact)) { $contact_id = $contact["id"]; // Update the contact every 7 days - $update_contact = ($contact['avatar-date'] < DateTimeFormat::utc('now -7 days')); + $update_contact = ($contact['updated'] < DateTimeFormat::utc('now -7 days')); // We force the update if the avatar is empty if (empty($contact['avatar'])) { $update_contact = true; } + + // Update the contact in the background if needed but it is called by the frontend + if ($update_contact && $no_update && in_array($contact['network'], Protocol::NATIVE_SUPPORT)) { + Worker::add(PRIORITY_LOW, "UpdateContact", $contact_id); + } + if (!$update_contact || $no_update) { return $contact_id; } @@ -1177,19 +1339,13 @@ class Contact extends BaseObject return 0; } - // When we don't want to update, we look if some of our users already know this contact - if ($no_update) { - $fields = ['url', 'addr', 'alias', 'notify', 'poll', 'name', 'nick', - 'photo', 'keywords', 'location', 'about', 'network', - 'priority', 'batch', 'request', 'confirm', 'poco']; - $data = DBA::selectFirst('contact', $fields, ['nurl' => Strings::normaliseLink($url)]); - - if (DBA::isResult($data)) { - // For security reasons we don't fetch key data from our users - $data["pubkey"] = ''; - } + // When we don't want to update, we look if we know this contact in any way + if ($no_update && empty($default)) { + $data = self::getProbeDataFromDatabase($url, $contact_id); + $background_update = true; } else { $data = []; + $background_update = false; } if (empty($data)) { @@ -1207,47 +1363,19 @@ class Contact extends BaseObject return 0; } - // Get data from the gcontact table - $fields = ['name', 'nick', 'url', 'photo', 'addr', 'alias', 'network']; - $contact = DBA::selectFirst('gcontact', $fields, ['nurl' => Strings::normaliseLink($url)]); - if (!DBA::isResult($contact)) { - $contact = DBA::selectFirst('contact', $fields, ['nurl' => Strings::normaliseLink($url)]); - } - - if (!DBA::isResult($contact)) { - $fields = ['url', 'addr', 'alias', 'notify', 'poll', 'name', 'nick', - 'photo', 'keywords', 'location', 'about', 'network', - 'priority', 'batch', 'request', 'confirm', 'poco']; - $contact = DBA::selectFirst('contact', $fields, ['addr' => $url]); - } - - // The link could be provided as http although we stored it as https - $ssl_url = str_replace('http://', 'https://', $url); - - if (!DBA::isResult($contact)) { - $condition = ['alias' => [$url, Strings::normaliseLink($url), $ssl_url]]; - $contact = DBA::selectFirst('contact', $fields, $condition); - } - - if (!DBA::isResult($contact)) { - $fields = ['url', 'addr', 'alias', 'notify', 'poll', 'name', 'nick', - 'photo', 'network', 'priority', 'batch', 'request', 'confirm']; - $condition = ['url' => [$url, Strings::normaliseLink($url), $ssl_url]]; - $contact = DBA::selectFirst('fcontact', $fields, $condition); - } - - if (!empty($default)) { - $contact = $default; - } - - if (!DBA::isResult($contact)) { + $contact = array_merge(self::getProbeDataFromDatabase($url, $contact_id), $default); + if (empty($contact)) { return 0; - } else { - $data = array_merge($data, $contact); } + + $data = array_merge($data, $contact); } - if (!$contact_id && ($data["alias"] != '') && ($data["alias"] != $url) && !$in_loop) { + if (empty($data)) { + return 0; + } + + if (!$contact_id && !empty($data['alias']) && ($data['alias'] != $url) && !$in_loop) { $contact_id = self::getIdForURL($data["alias"], $uid, true, $default, true); } @@ -1255,26 +1383,26 @@ class Contact extends BaseObject $fields = [ 'uid' => $uid, 'created' => DateTimeFormat::utcNow(), - 'url' => $data["url"], - 'nurl' => Strings::normaliseLink($data["url"]), - 'addr' => $data["addr"], - 'alias' => $data["alias"], - 'notify' => $data["notify"], - 'poll' => $data["poll"], - 'name' => $data["name"], - 'nick' => $data["nick"], - 'photo' => $data["photo"], - 'keywords' => $data["keywords"], - 'location' => $data["location"], - 'about' => $data["about"], - 'network' => $data["network"], - 'pubkey' => $data["pubkey"], + 'url' => $data['url'], + 'nurl' => Strings::normaliseLink($data['url']), + 'addr' => defaults($data, 'addr', ''), + 'alias' => defaults($data, 'alias', ''), + 'notify' => defaults($data, 'notify', ''), + 'poll' => defaults($data, 'poll', ''), + 'name' => defaults($data, 'name', ''), + 'nick' => defaults($data, 'nick', ''), + 'photo' => defaults($data, 'photo', ''), + 'keywords' => defaults($data, 'keywords', ''), + 'location' => defaults($data, 'location', ''), + 'about' => defaults($data, 'about', ''), + 'network' => $data['network'], + 'pubkey' => defaults($data, 'pubkey', ''), 'rel' => self::SHARING, - 'priority' => $data["priority"], - 'batch' => $data["batch"], - 'request' => $data["request"], - 'confirm' => $data["confirm"], - 'poco' => $data["poco"], + 'priority' => defaults($data, 'priority', 0), + 'batch' => defaults($data, 'batch', ''), + 'request' => defaults($data, 'request', ''), + 'confirm' => defaults($data, 'confirm', ''), + 'poco' => defaults($data, 'poco', ''), 'name-date' => DateTimeFormat::utcNow(), 'uri-date' => DateTimeFormat::utcNow(), 'avatar-date' => DateTimeFormat::utcNow(), @@ -1295,17 +1423,22 @@ class Contact extends BaseObject $contact_id = $contacts[0]["id"]; + // Update in the background when we fetched the data solely from the database + if ($background_update) { + Worker::add(PRIORITY_LOW, "UpdateContact", $contact_id); + } + // Update the newly created contact from data in the gcontact table $gcontact = DBA::selectFirst('gcontact', ['location', 'about', 'keywords', 'gender'], ['nurl' => Strings::normaliseLink($data["url"])]); if (DBA::isResult($gcontact)) { // Only use the information when the probing hadn't fetched these values - if ($data['keywords'] != '') { + if (!empty($data['keywords'])) { unset($gcontact['keywords']); } - if ($data['location'] != '') { + if (!empty($data['location'])) { unset($gcontact['location']); } - if ($data['about'] != '') { + if (!empty($data['about'])) { unset($gcontact['about']); } DBA::update('contact', $gcontact, ['id' => $contact_id]); @@ -1319,7 +1452,9 @@ class Contact extends BaseObject } } - self::updateAvatar($data["photo"], $uid, $contact_id); + if (!empty($data['photo'])) { + self::updateAvatar($data['photo'], $uid, $contact_id); + } $fields = ['url', 'nurl', 'addr', 'alias', 'name', 'nick', 'keywords', 'location', 'about', 'avatar-date', 'pubkey']; $contact = DBA::selectFirst('contact', $fields, ['id' => $contact_id]); @@ -1329,37 +1464,39 @@ class Contact extends BaseObject return $contact_id; } - $updated = ['addr' => $data['addr'], - 'alias' => $data['alias'], + $updated = [ + 'addr' => $data['addr'] ?? '', + 'alias' => defaults($data, 'alias', ''), 'url' => $data['url'], 'nurl' => Strings::normaliseLink($data['url']), 'name' => $data['name'], - 'nick' => $data['nick']]; + 'nick' => $data['nick'] + ]; - if ($data['keywords'] != '') { + if (!empty($data['keywords'])) { $updated['keywords'] = $data['keywords']; } - if ($data['location'] != '') { + if (!empty($data['location'])) { $updated['location'] = $data['location']; } // Update the technical stuff as well - if filled - if ($data['notify'] != '') { + if (!empty($data['notify'])) { $updated['notify'] = $data['notify']; } - if ($data['poll'] != '') { + if (!empty($data['poll'])) { $updated['poll'] = $data['poll']; } - if ($data['batch'] != '') { + if (!empty($data['batch'])) { $updated['batch'] = $data['batch']; } - if ($data['request'] != '') { + if (!empty($data['request'])) { $updated['request'] = $data['request']; } - if ($data['confirm'] != '') { + if (!empty($data['confirm'])) { $updated['confirm'] = $data['confirm']; } - if ($data['poco'] != '') { + if (!empty($data['poco'])) { $updated['poco'] = $data['poco']; } @@ -1368,14 +1505,14 @@ class Contact extends BaseObject $updated['pubkey'] = $data['pubkey']; } - if (($data["addr"] != $contact["addr"]) || ($data["alias"] != $contact["alias"])) { + if (($updated['addr'] != $contact['addr']) || (!empty($data['alias']) && ($data['alias'] != $contact['alias']))) { $updated['uri-date'] = DateTimeFormat::utcNow(); } if (($data["name"] != $contact["name"]) || ($data["nick"] != $contact["nick"])) { $updated['name-date'] = DateTimeFormat::utcNow(); } - $updated['avatar-date'] = DateTimeFormat::utcNow(); + $updated['updated'] = DateTimeFormat::utcNow(); DBA::update('contact', $updated, ['id' => $contact_id], $contact); @@ -1550,13 +1687,13 @@ class Contact extends BaseObject /** * @brief Blocks a contact * - * @param int $uid + * @param int $cid * @return bool * @throws \Exception */ - public static function block($uid) + public static function block($cid, $reason = null) { - $return = DBA::update('contact', ['blocked' => true], ['id' => $uid]); + $return = DBA::update('contact', ['blocked' => true, 'block_reason' => $reason], ['id' => $cid]); return $return; } @@ -1564,13 +1701,13 @@ class Contact extends BaseObject /** * @brief Unblocks a contact * - * @param int $uid + * @param int $cid * @return bool * @throws \Exception */ - public static function unblock($uid) + public static function unblock($cid) { - $return = DBA::update('contact', ['blocked' => false], ['id' => $uid]); + $return = DBA::update('contact', ['blocked' => false, 'block_reason' => null], ['id' => $cid]); return $return; } @@ -1589,7 +1726,7 @@ class Contact extends BaseObject */ public static function updateAvatar($avatar, $uid, $cid, $force = false) { - $contact = DBA::selectFirst('contact', ['avatar', 'photo', 'thumb', 'micro', 'nurl'], ['id' => $cid]); + $contact = DBA::selectFirst('contact', ['avatar', 'photo', 'thumb', 'micro', 'nurl'], ['id' => $cid, 'self' => false]); if (!DBA::isResult($contact)) { return false; } else { @@ -1600,17 +1737,14 @@ class Contact extends BaseObject $photos = Photo::importProfilePhoto($avatar, $uid, $cid, true); if ($photos) { - DBA::update( - 'contact', - ['avatar' => $avatar, 'photo' => $photos[0], 'thumb' => $photos[1], 'micro' => $photos[2], 'avatar-date' => DateTimeFormat::utcNow()], - ['id' => $cid] - ); + $fields = ['avatar' => $avatar, 'photo' => $photos[0], 'thumb' => $photos[1], 'micro' => $photos[2], 'avatar-date' => DateTimeFormat::utcNow()]; + DBA::update('contact', $fields, ['id' => $cid]); // Update the public contact (contact id = 0) if ($uid != 0) { $pcontact = DBA::selectFirst('contact', ['id'], ['nurl' => $contact['nurl'], 'uid' => 0]); if (DBA::isResult($pcontact)) { - self::updateAvatar($avatar, 0, $pcontact['id'], $force); + DBA::update('contact', $fields, ['id' => $pcontact['id']]); } } @@ -1652,7 +1786,11 @@ class Contact extends BaseObject $ret = Probe::uri($contact['url'], $network, $uid, !$force); // If Probe::uri fails the network code will be different (mostly "feed" or "unkn") - if ((in_array($ret['network'], [Protocol::FEED, Protocol::PHANTOM])) && ($ret['network'] != $contact['network'])) { + if (in_array($ret['network'], [Protocol::FEED, Protocol::PHANTOM]) && ($ret['network'] != $contact['network'])) { + return false; + } + + if (!in_array($ret['network'], Protocol::NATIVE_SUPPORT)) { return false; } @@ -1674,6 +1812,7 @@ class Contact extends BaseObject } $ret['nurl'] = Strings::normaliseLink($ret['url']); + $ret['updated'] = DateTimeFormat::utcNow(); self::updateAvatar($ret['photo'], $uid, $id, true); @@ -1686,6 +1825,40 @@ class Contact extends BaseObject return true; } + /** + * Detects if a given contact array belongs to a legacy DFRN connection + * + * @param array $contact + * @return boolean + */ + public static function isLegacyDFRNContact($contact) + { + // Newer Friendica contacts are connected via AP, then these fields aren't set + return !empty($contact['dfrn-id']) || !empty($contact['issued-id']); + } + + /** + * Detects the communication protocol for a given contact url. + * This is used to detect Friendica contacts that we can communicate via AP. + * + * @param string $url contact url + * @param string $network Network of that contact + * @return string with protocol + */ + public static function getProtocol($url, $network) + { + if ($network != Protocol::DFRN) { + return $network; + } + + $apcontact = APContact::getByURL($url); + if (!empty($apcontact) && !empty($apcontact['generator'])) { + return Protocol::ACTIVITYPUB; + } else { + return $network; + } + } + /** * Takes a $uid and a url/handle and adds a new contact * Currently if the contact is DFRN, interactive needs to be true, to redirect to the @@ -1761,7 +1934,9 @@ class Contact extends BaseObject $contact = DBA::selectFirst('contact', ['id', 'rel'], $condition); } - if (($ret['network'] === Protocol::DFRN) && !DBA::isResult($contact)) { + $protocol = self::getProtocol($url, $ret['network']); + + if (($protocol === Protocol::DFRN) && !DBA::isResult($contact)) { if ($interactive) { if (strlen($a->getURLPath())) { $myaddr = bin2hex(System::baseUrl() . '/profile/' . $a->user['nickname']); @@ -1780,7 +1955,7 @@ class Contact extends BaseObject } // This extra param just confuses things, remove it - if ($ret['network'] === Protocol::DIASPORA) { + if ($protocol === Protocol::DIASPORA) { $ret['url'] = str_replace('?absolute=true', '', $ret['url']); } @@ -1803,7 +1978,7 @@ class Contact extends BaseObject return $result; } - if ($ret['network'] === Protocol::OSTATUS && Config::get('system', 'ostatus_disabled')) { + if ($protocol === Protocol::OSTATUS && Config::get('system', 'ostatus_disabled')) { $result['message'] .= L10n::t('The profile address specified belongs to a network which has been disabled on this site.') . EOL; $ret['notify'] = ''; } @@ -1812,15 +1987,15 @@ class Contact extends BaseObject $result['message'] .= L10n::t('Limited profile. This person will be unable to receive direct/personal notifications from you.') . EOL; } - $writeable = ((($ret['network'] === Protocol::OSTATUS) && ($ret['notify'])) ? 1 : 0); + $writeable = ((($protocol === Protocol::OSTATUS) && ($ret['notify'])) ? 1 : 0); - $subhub = (($ret['network'] === Protocol::OSTATUS) ? true : false); + $subhub = (($protocol === Protocol::OSTATUS) ? true : false); - $hidden = (($ret['network'] === Protocol::MAIL) ? 1 : 0); + $hidden = (($protocol === Protocol::MAIL) ? 1 : 0); - $pending = in_array($ret['network'], [Protocol::ACTIVITYPUB]); + $pending = in_array($protocol, [Protocol::ACTIVITYPUB]); - if (in_array($ret['network'], [Protocol::MAIL, Protocol::DIASPORA, Protocol::ACTIVITYPUB])) { + if (in_array($protocol, [Protocol::MAIL, Protocol::DIASPORA, Protocol::ACTIVITYPUB])) { $writeable = 1; } @@ -1831,7 +2006,7 @@ class Contact extends BaseObject $fields = ['rel' => $new_relation, 'subhub' => $subhub, 'readonly' => false]; DBA::update('contact', $fields, ['id' => $contact['id']]); } else { - $new_relation = (in_array($ret['network'], [Protocol::MAIL]) ? self::FRIEND : self::SHARING); + $new_relation = (in_array($protocol, [Protocol::MAIL]) ? self::FRIEND : self::SHARING); // create contact record DBA::insert('contact', [ @@ -1848,6 +2023,7 @@ class Contact extends BaseObject 'name' => $ret['name'], 'nick' => $ret['nick'], 'network' => $ret['network'], + 'protocol' => $protocol, 'pubkey' => $ret['pubkey'], 'rel' => $new_relation, 'priority'=> $ret['priority'], @@ -1881,7 +2057,7 @@ class Contact extends BaseObject $owner = User::getOwnerDataById($uid); if (DBA::isResult($owner)) { - if (in_array($contact['network'], [Protocol::OSTATUS, Protocol::DFRN])) { + if (in_array($protocol, [Protocol::OSTATUS, Protocol::DFRN])) { // create a follow slap $item = []; $item['verb'] = ACTIVITY_FOLLOW; @@ -1897,10 +2073,10 @@ class Contact extends BaseObject if (!empty($contact['notify'])) { Salmon::slapper($owner, $contact['notify'], $slap); } - } elseif ($contact['network'] == Protocol::DIASPORA) { + } elseif ($protocol == Protocol::DIASPORA) { $ret = Diaspora::sendShare($a->user, $contact); Logger::log('share returns: ' . $ret); - } elseif ($contact['network'] == Protocol::ACTIVITYPUB) { + } elseif ($protocol == Protocol::ACTIVITYPUB) { $activity_id = ActivityPub\Transmitter::activityIDFromContact($contact_id); if (empty($activity_id)) { // This really should never happen @@ -1928,7 +2104,7 @@ class Contact extends BaseObject public static function updateSslPolicy(array $contact, $new_policy) { $ssl_changed = false; - if ((intval($new_policy) == SSL_POLICY_SELFSIGN || $new_policy === 'self') && strstr($contact['url'], 'https:')) { + if ((intval($new_policy) == BaseURL::SSL_POLICY_SELFSIGN || $new_policy === 'self') && strstr($contact['url'], 'https:')) { $ssl_changed = true; $contact['url'] = str_replace('https:', 'http:', $contact['url']); $contact['request'] = str_replace('https:', 'http:', $contact['request']); @@ -1938,7 +2114,7 @@ class Contact extends BaseObject $contact['poco'] = str_replace('https:', 'http:', $contact['poco']); } - if ((intval($new_policy) == SSL_POLICY_FULL || $new_policy === 'full') && strstr($contact['url'], 'http:')) { + if ((intval($new_policy) == BaseURL::SSL_POLICY_FULL || $new_policy === 'full') && strstr($contact['url'], 'http:')) { $ssl_changed = true; $contact['url'] = str_replace('http:', 'https:', $contact['url']); $contact['request'] = str_replace('http:', 'https:', $contact['request']); @@ -1958,56 +2134,81 @@ class Contact extends BaseObject return $contact; } - public static function addRelationship($importer, $contact, $datarray, $item = '', $sharing = false) { + /** + * @param array $importer Owner (local user) data + * @param array $contact Existing owner-specific contact data we want to expand the relationship with. Optional. + * @param array $datarray An item-like array with at least the 'author-id' and 'author-url' keys for the contact. Mandatory. + * @param bool $sharing True: Contact is now sharing with Owner; False: Contact is now following Owner (default) + * @param string $note Introduction additional message + * @return bool|null True: follow request is accepted; False: relationship is rejected; Null: relationship is pending + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + * @throws \ImagickException + */ + public static function addRelationship(array $importer, array $contact, array $datarray, $sharing = false, $note = '') + { // Should always be set if (empty($datarray['author-id'])) { - return; + return false; } - $fields = ['url', 'name', 'nick', 'photo', 'network']; + $fields = ['url', 'name', 'nick', 'avatar', 'photo', 'network', 'blocked']; $pub_contact = DBA::selectFirst('contact', $fields, ['id' => $datarray['author-id']]); if (!DBA::isResult($pub_contact)) { // Should never happen - return; + return false; + } + + // Contact is blocked at node-level + if (self::isBlocked($datarray['author-id'])) { + return false; } $url = defaults($datarray, 'author-link', $pub_contact['url']); $name = $pub_contact['name']; - $photo = $pub_contact['photo']; + $photo = defaults($pub_contact, 'avatar', $pub_contact["photo"]); $nick = $pub_contact['nick']; $network = $pub_contact['network']; - if (is_array($contact)) { + if (!empty($contact)) { + // Contact is blocked at user-level + if (!empty($contact['id']) && !empty($importer['id']) && + self::isBlockedByUser($contact['id'], $importer['id'])) { + return false; + } + + // Make sure that the existing contact isn't archived + self::unmarkForArchival($contact); + if (($contact['rel'] == self::SHARING) || ($sharing && $contact['rel'] == self::FOLLOWER)) { - DBA::update('contact', ['rel' => self::FRIEND, 'writable' => true], + DBA::update('contact', ['rel' => self::FRIEND, 'writable' => true, 'pending' => false], ['id' => $contact['id'], 'uid' => $importer['uid']]); } - if ($contact['network'] == Protocol::ACTIVITYPUB) { - ActivityPub\Transmitter::sendContactAccept($contact['url'], $contact['hub-verify'], $importer['uid']); - } - - // send email notification to owner? + return true; } else { + // send email notification to owner? if (DBA::exists('contact', ['nurl' => Strings::normaliseLink($url), 'uid' => $importer['uid'], 'pending' => true])) { Logger::log('ignoring duplicated connection request from pending contact ' . $url); - return; + return null; } + // create contact record - q("INSERT INTO `contact` (`uid`, `created`, `url`, `nurl`, `name`, `nick`, `photo`, `network`, `rel`, - `blocked`, `readonly`, `pending`, `writable`) - VALUES (%d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, 0, 0, 1, 1)", - intval($importer['uid']), - DBA::escape(DateTimeFormat::utcNow()), - DBA::escape($url), - DBA::escape(Strings::normaliseLink($url)), - DBA::escape($name), - DBA::escape($nick), - DBA::escape($photo), - DBA::escape($network), - intval(self::FOLLOWER) - ); + DBA::insert('contact', [ + 'uid' => $importer['uid'], + 'created' => DateTimeFormat::utcNow(), + 'url' => $url, + 'nurl' => Strings::normaliseLink($url), + 'name' => $name, + 'nick' => $nick, + 'photo' => $photo, + 'network' => $network, + 'rel' => self::FOLLOWER, + 'blocked' => 0, + 'readonly' => 0, + 'pending' => 1, + 'writable' => 1, + ]); $contact_record = [ 'id' => DBA::lastInsertId(), @@ -2028,7 +2229,7 @@ class Contact extends BaseObject if (is_array($contact_record)) { DBA::insert('intro', ['uid' => $importer['uid'], 'contact-id' => $contact_record['id'], - 'blocked' => false, 'knowyou' => false, + 'blocked' => false, 'knowyou' => false, 'note' => $note, 'hash' => $hash, 'datetime' => DateTimeFormat::utcNow()]); } @@ -2051,19 +2252,16 @@ class Contact extends BaseObject 'verb' => ($sharing ? ACTIVITY_FRIEND : ACTIVITY_FOLLOW), 'otype' => 'intro' ]); - } } elseif (DBA::isResult($user) && in_array($user['page-flags'], [User::PAGE_FLAGS_SOAPBOX, User::PAGE_FLAGS_FREELOVE, User::PAGE_FLAGS_COMMUNITY])) { $condition = ['uid' => $importer['uid'], 'url' => $url, 'pending' => true]; DBA::update('contact', ['pending' => false], $condition); - $contact = DBA::selectFirst('contact', ['url', 'network', 'hub-verify'], ['id' => $contact_record['id']]); - - if ($contact['network'] == Protocol::ACTIVITYPUB) { - ActivityPub\Transmitter::sendContactAccept($contact['url'], $contact['hub-verify'], $importer['uid']); - } + return true; } } + + return null; } public static function removeFollower($importer, $contact, array $datarray = [], $item = "") @@ -2167,12 +2365,15 @@ class Contact extends BaseObject return $url ?: $contact_url; // Equivalent to: ($url != '') ? $url : $contact_url; } - $cid = self::getIdForURL($contact_url, 0, true); - if (empty($cid)) { + $data = self::getProbeDataFromDatabase($contact_url); + if (empty($data)) { return $url ?: $contact_url; // Equivalent to: ($url != '') ? $url : $contact_url; } - return self::magicLinkbyId($cid, $url); + // Prevents endless loop in case only a non-public contact exists for the contact URL + unset($data['uid']); + + return self::magicLinkByContact($data, $contact_url); } /** @@ -2213,10 +2414,14 @@ class Contact extends BaseObject return $url; } - if ($contact['uid'] != 0) { + if (!empty($contact['uid'])) { return self::magicLink($contact['url'], $url); } + if (empty($contact['id'])) { + return $url ?: $contact['url']; + } + $redirect = 'redir/' . $contact['id']; if ($url != '') { @@ -2257,4 +2462,18 @@ class Contact extends BaseObject // Is it a forum? return ($contact['forum'] || $contact['prv']); } + + /** + * Can the remote contact receive private messages? + * + * @param array $contact + * @return bool + */ + public static function canReceivePrivateMessages(array $contact) + { + $protocol = $contact['network'] ?? $contact['protocol'] ?? Protocol::PHANTOM; + $self = $contact['self'] ?? false; + + return in_array($protocol, [Protocol::DFRN, Protocol::DIASPORA, Protocol::ACTIVITYPUB]) && !$self; + } } diff --git a/src/Model/Event.php b/src/Model/Event.php index d8657c1e9..42742f18e 100644 --- a/src/Model/Event.php +++ b/src/Model/Event.php @@ -226,7 +226,7 @@ class Event extends BaseObject return; } - DBA::delete('event', ['id' => $event_id]); + DBA::delete('event', ['id' => $event_id], ['cascade' => false]); Logger::log("Deleted event ".$event_id, Logger::DEBUG); } diff --git a/src/Model/FileTag.php b/src/Model/FileTag.php index 2ad864c9c..f4d04634f 100644 --- a/src/Model/FileTag.php +++ b/src/Model/FileTag.php @@ -11,127 +11,149 @@ use Friendica\Database\DBA; /** * @brief This class handles FileTag related functions + * + * post categories and "save to file" use the same item.file table for storage. + * We will differentiate the different uses by wrapping categories in angle brackets + * and save to file categories in square brackets. + * To do this we need to escape these characters if they appear in our tag. */ class FileTag { - // post categories and "save to file" use the same item.file table for storage. - // We will differentiate the different uses by wrapping categories in angle brackets - // and save to file categories in square brackets. - // To do this we need to escape these characters if they appear in our tag. + /** + * @brief URL encode <, >, left and right brackets + * + * @param string $s String to be URL encoded. + * + * @return string The URL encoded string. + */ + public static function encode($s) + { + return str_replace(['<', '>', '[', ']'], ['%3c', '%3e', '%5b', '%5d'], $s); + } - /** - * @brief URL encode <, >, left and right brackets - * - * @param string $s String to be URL encoded. - * - * @return string The URL encoded string. - */ - public static function encode($s) - { - return str_replace(['<', '>', '[', ']'], ['%3c', '%3e', '%5b', '%5d'], $s); - } + /** + * @brief URL decode <, >, left and right brackets + * + * @param string $s The URL encoded string to be decoded + * + * @return string The decoded string. + */ + public static function decode($s) + { + return str_replace(['%3c', '%3e', '%5b', '%5d'], ['<', '>', '[', ']'], $s); + } - /** - * @brief URL decode <, >, left and right brackets - * - * @param string $s The URL encoded string to be decoded - * - * @return string The decoded string. - */ - public static function decode($s) - { - return str_replace(['%3c', '%3e', '%5b', '%5d'], ['<', '>', '[', ']'], $s); - } + /** + * @brief Query files for tag + * + * @param string $table The table to be queired. + * @param string $s The search term + * @param string $type Optional file type. + * + * @return string Query string. + */ + public static function fileQuery($table, $s, $type = 'file') + { + if ($type == 'file') { + $str = preg_quote('[' . str_replace('%', '%%', self::encode($s)) . ']'); + } else { + $str = preg_quote('<' . str_replace('%', '%%', self::encode($s)) . '>'); + } - /** - * @brief Query files for tag - * - * @param string $table The table to be queired. - * @param string $s The search term - * @param string $type Optional file type. - * - * @return string Query string. - */ - public static function fileQuery($table, $s, $type = 'file') - { - if ($type == 'file') { - $str = preg_quote('[' . str_replace('%', '%%', self::encode($s)) . ']'); - } else { - $str = preg_quote('<' . str_replace('%', '%%', self::encode($s)) . '>'); - } + return " AND " . (($table) ? DBA::escape($table) . '.' : '') . "file regexp '" . DBA::escape($str) . "' "; + } - return " AND " . (($table) ? DBA::escape($table) . '.' : '') . "file regexp '" . DBA::escape($str) . "' "; - } + /** + * Get file tags from array + * + * ex. given [music,video] return
  • {{/if}} + {{if $nav.logout}} + + {{/if}} {{if $nav.search}}