diff --git a/.gitignore b/.gitignore index 49d08ba71e..e70f651f53 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,6 @@ favicon.* *.out *.version* home.html - *~ robots.txt @@ -74,3 +73,6 @@ venv/ #ignore filesystem storage default path /storage + +#Ignore log folder +/log diff --git a/CHANGELOG b/CHANGELOG index 16ba3f8412..1fa98ee870 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,17 +1,20 @@ -Version 2019.06 (UNRELEASED) (2019-06-?) +Version 2019.06 (2019-06-23) Friendica Core: - Update to the tranlation (CS, DE, ET, PL, PT-BR, SV) [translation teams] - Update to the documentation [nupplaphil, realkinetix] + 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] + 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] @@ -20,35 +23,56 @@ Version 2019.06 (UNRELEASED) (2019-06-?) 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 Estinian translation [Rain Hawk] + 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] + 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] + 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: - 5011, 5047, 5850, 6303, 6319, 6478, 6319, 6720, 6815, 6864, 6879, - 6903, 6921, 6927, 6936, 6941, 6943, 6947, 6948, 6952 + 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 3b3e6ad308..c47a8a6239 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/VERSION b/VERSION index bd61e84f20..4eda9a567d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2019.06-dev +2019.06 diff --git a/boot.php b/boot.php index 9076331290..24e013fa11 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.06-dev'); +define('FRIENDICA_VERSION', '2019.06'); define('DFRN_PROTOCOL_VERSION', '2.23'); define('NEW_UPDATE_ROUTINE_VERSION', 1170); @@ -535,39 +535,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 c1366c6771..feac6c61f7 100644 --- a/composer.json +++ b/composer.json @@ -37,6 +37,7 @@ "mobiledetect/mobiledetectlib": "2.8.*", "monolog/monolog": "^1.24", "nikic/fast-route": "^1.3", + "paragonie/hidden-string": "^1.0", "pear/text_languagedetect": "1.*", "pragmarx/google2fa": "^5.0", "pragmarx/recovery": "^0.1.0", @@ -46,6 +47,7 @@ "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", @@ -95,7 +97,19 @@ }, "archive": { "exclude": [ - "log", "cache", "/photo", "/proxy" + "/.*", + "/*file", + "!/.htaccess-dist", + "/tests", + "/*.xml", + "/composer.*", + "/log", + "/cache", + "/photo", + "/proxy", + "/addon", + "!/vendor", + "!/view/asset" ] }, "require-dev": { diff --git a/composer.lock b/composer.lock index 2fe210f5df..af51b6dfe7 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": "d7302553201de079b72871c0b2922ce7", + "content-hash": "eb985236d64ed0b0fe1fc2e4ac6616e2", "packages": [ { "name": "asika/simple-console", @@ -148,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", @@ -1175,6 +1220,22 @@ "require": { "npm-asset/ev-emitter": ">=1.0.0,<2.0.0" }, + "require-dev": { + "npm-asset/chalk": ">=1.1.1,<2.0.0", + "npm-asset/cheerio": ">=0.19.0,<0.20.0", + "npm-asset/gulp": ">=3.9.0,<4.0.0", + "npm-asset/gulp-jshint": ">=1.11.2,<2.0.0", + "npm-asset/gulp-json-lint": ">=0.1.0,<0.2.0", + "npm-asset/gulp-rename": ">=1.2.2,<2.0.0", + "npm-asset/gulp-replace": ">=0.5.4,<0.6.0", + "npm-asset/gulp-requirejs-optimize": "dev-github:metafizzy/gulp-requirejs-optimize", + "npm-asset/gulp-uglify": ">=1.4.2,<2.0.0", + "npm-asset/gulp-util": ">=3.0.7,<4.0.0", + "npm-asset/highlight.js": ">=8.9.1,<9.0.0", + "npm-asset/marked": ">=0.3.5,<0.4.0", + "npm-asset/minimist": ">=1.2.0,<2.0.0", + "npm-asset/transfob": ">=1.0.0,<2.0.0" + }, "type": "npm-asset-library", "extra": { "npm-asset-bugs": { @@ -1220,6 +1281,14 @@ "reference": null, "shasum": "2736e332aaee73ccf0a14a5f0066391a0a13f4a3" }, + "require-dev": { + "npm-asset/grunt": "~0.4.2", + "npm-asset/grunt-contrib-cssmin": "~0.9.0", + "npm-asset/grunt-contrib-jshint": "~0.6.3", + "npm-asset/grunt-contrib-less": "~0.11.0", + "npm-asset/grunt-contrib-uglify": "~0.4.0", + "npm-asset/grunt-contrib-watch": "~0.6.1" + }, "type": "npm-asset-library", "extra": { "npm-asset-bugs": { @@ -1253,6 +1322,32 @@ "reference": null, "shasum": "2c89d6889b5eac522a7eea32c14521559c6cbf02" }, + "require-dev": { + "npm-asset/commitplease": "2.0.0", + "npm-asset/core-js": "0.9.17", + "npm-asset/grunt": "0.4.5", + "npm-asset/grunt-babel": "5.0.1", + "npm-asset/grunt-cli": "0.1.13", + "npm-asset/grunt-compare-size": "0.4.0", + "npm-asset/grunt-contrib-jshint": "0.11.2", + "npm-asset/grunt-contrib-uglify": "0.9.2", + "npm-asset/grunt-contrib-watch": "0.6.1", + "npm-asset/grunt-git-authors": "2.0.1", + "npm-asset/grunt-jscs": "2.1.0", + "npm-asset/grunt-jsonlint": "1.0.4", + "npm-asset/grunt-npmcopy": "0.1.0", + "npm-asset/gzip-js": "0.3.2", + "npm-asset/jsdom": "5.6.1", + "npm-asset/load-grunt-tasks": "1.0.0", + "npm-asset/qunit-assert-step": "1.0.3", + "npm-asset/qunitjs": "1.17.1", + "npm-asset/requirejs": "2.1.17", + "npm-asset/sinon": "1.10.3", + "npm-asset/sizzle": "2.2.1", + "npm-asset/strip-json-comments": "1.0.3", + "npm-asset/testswarm": "1.1.0", + "npm-asset/win-spawn": "2.0.0" + }, "type": "npm-asset-library", "extra": { "npm-asset-bugs": { @@ -1403,6 +1498,12 @@ "reference": null, "shasum": "06f0335f16e353a695e7206bf50503cb523a6ee5" }, + "require-dev": { + "npm-asset/grunt": "~0.4.1", + "npm-asset/grunt-contrib-connect": "~0.5.0", + "npm-asset/grunt-contrib-jshint": "~0.7.1", + "npm-asset/grunt-contrib-uglify": "~0.2.7" + }, "type": "npm-asset-library", "extra": { "npm-asset-bugs": { @@ -1622,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": { @@ -1681,7 +1781,56 @@ "hex2bin", "rfc4648" ], - "time": "2018-04-30T17:57:16+00:00" + "time": "2019-01-03T20:26:31+00:00" + }, + { + "name": "paragonie/hidden-string", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/paragonie/hidden-string.git", + "reference": "0bbb00be0e33b8e1d48fa79ea35cd42d3091a936" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/hidden-string/zipball/0bbb00be0e33b8e1d48fa79ea35cd42d3091a936", + "reference": "0bbb00be0e33b8e1d48fa79ea35cd42d3091a936", + "shasum": "" + }, + "require": { + "paragonie/constant_time_encoding": "^2", + "paragonie/sodium_compat": "^1.6", + "php": "^7" + }, + "require-dev": { + "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", @@ -3600,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", @@ -3702,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", @@ -3770,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" @@ -3822,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" ], @@ -3924,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" }, { diff --git a/config/settings.config.php b/config/settings.config.php index 31e8dbe5b5..bf8b62f158 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/doc/Addons.md b/doc/Addons.md index 858d64355f..47d16085a1 100644 --- a/doc/Addons.md +++ b/doc/Addons.md @@ -358,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()`. @@ -367,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()`. diff --git a/doc/FAQ.md b/doc/FAQ.md index 1407181785..83bdce3a27 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/include/api.php b/include/api.php index eccd77675e..0fab1f47c1 100644 --- a/include/api.php +++ b/include/api.php @@ -361,7 +361,7 @@ 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->getCode()} {$e->httpdesc}"); @@ -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"], @@ -1271,7 +1271,7 @@ function api_status_show($type, $item_id) function api_get_last_status($ownerId, $uid) { $condition = [ - 'owner-id' => $ownerId, + 'author-id'=> $ownerId, 'uid' => $uid, 'gravity' => [GRAVITY_PARENT, GRAVITY_COMMENT], 'private' => false diff --git a/include/items.php b/include/items.php index a28e19a05a..25c857f115 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 c4249f86c4..e4227cd7d0 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/cal.php b/mod/cal.php index 3f3cba466f..0a2a02e53c 100644 --- a/mod/cal.php +++ b/mod/cal.php @@ -147,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') { diff --git a/mod/dfrn_confirm.php b/mod/dfrn_confirm.php index 7b1171ba41..9f9684e093 100644 --- a/mod/dfrn_confirm.php +++ b/mod/dfrn_confirm.php @@ -209,7 +209,7 @@ function dfrn_confirm_post(App $a, $handsfree = null) * */ - $res = Network::post($dfrn_confirm, $params, null, $redirects, 120)->getBody(); + $res = Network::post($dfrn_confirm, $params, [], 120)->getBody(); Logger::log(' Confirm: received data: ' . $res, Logger::DATA); diff --git a/mod/dfrn_notify.php b/mod/dfrn_notify.php index 1a9f98fa33..e75d975a82 100644 --- a/mod/dfrn_notify.php +++ b/mod/dfrn_notify.php @@ -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_request.php b/mod/dfrn_request.php index 780e0afeeb..19879c21bb 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/dirfind.php b/mod/dirfind.php deleted file mode 100644 index a5b77312f3..0000000000 --- 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 fa5b2e1962..54d4792594 100644 --- a/mod/display.php +++ b/mod/display.php @@ -84,6 +84,10 @@ function display_init(App $a) displayShowFeed($item['id'], $a->argc > 3 && $a->argv[3] == 'conversation.atom'); } + if ($a->argc >= 3 && $nick == 'feed-item') { + displayShowFeed($item['id'], $a->argc > 3 && $a->argv[3] == 'conversation.atom'); + } + if (!empty($_SERVER['HTTP_ACCEPT']) && strstr($_SERVER['HTTP_ACCEPT'], 'application/atom+xml')) { Logger::log('Directly serving XML for id '.$item["id"], Logger::DEBUG); displayShowFeed($item["id"], false); @@ -186,16 +190,7 @@ 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) diff --git a/mod/events.php b/mod/events.php index 6569653a06..86cec9a7d4 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'); @@ -246,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'; diff --git a/mod/fsuggest.php b/mod/fsuggest.php index 2cede56852..2bddf48133 100644 --- a/mod/fsuggest.php +++ b/mod/fsuggest.php @@ -10,6 +10,7 @@ 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) { @@ -51,7 +52,7 @@ function fsuggest_post(App $a) 'photo' => $contact['avatar'], 'note' => $note, 'created' => DateTimeFormat::utcNow()]; DBA::insert('fsuggest', $fields); - Worker::add(PRIORITY_HIGH, 'Notifier', 'suggest', DBA::lastInsertId()); + Worker::add(PRIORITY_HIGH, 'Notifier', Delivery::SUGGESTION, DBA::lastInsertId()); info(L10n::t('Friend suggestion sent.') . EOL); } diff --git a/mod/item.php b/mod/item.php index b126c4825b..20dc9dfdae 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/network.php b/mod/network.php index cbd3071ece..87eb4308b4 100644 --- a/mod/network.php +++ b/mod/network.php @@ -78,9 +78,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) { @@ -100,7 +98,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 = [ @@ -112,12 +110,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); @@ -141,7 +139,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); @@ -155,7 +153,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', '') ); @@ -203,12 +201,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); @@ -974,7 +972,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', @@ -982,7 +980,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', @@ -992,7 +990,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', @@ -1002,7 +1000,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', @@ -1013,7 +1011,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', @@ -1023,7 +1021,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 b1eda7a2d1..0000000000 --- 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('Settings') . '

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

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

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

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

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

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

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

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

'; - $o .= ''; - $o .= '
'; - $o .= ''; - - return $o; -} diff --git a/mod/notes.php b/mod/notes.php index fdb12d6cc5..1f67e486d6 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/notifications.php b/mod/notifications.php index ff954d4189..8bc9a76c38 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 diff --git a/mod/parse_url.php b/mod/parse_url.php index 3b2522ab12..6b393932eb 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 7c0ca1b7ba..b904abe311 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) { @@ -188,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')) { @@ -315,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'); @@ -418,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)) { @@ -524,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 .= ','; } @@ -735,7 +739,7 @@ function photos_post(App $a) @unlink($src); $foo = 0; Hook::callAll('photo_post_end',$foo); - exit(); + return; } $exif = $image->orient($src); @@ -761,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) { @@ -950,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') { @@ -959,7 +963,7 @@ function photos_content(App $a) return; } - $selname = $datum ? hex2bin($datum) : ''; + $selname = Strings::isHex($datum) ? hex2bin($datum) : ''; $albumselect = ''; @@ -1026,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; @@ -1503,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 ]); @@ -1512,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'], diff --git a/mod/redir.php b/mod/redir.php index 4dbae5498b..233ec9b007 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/search.php b/mod/search.php index 1416f1d89f..4144e2608f 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 = ''; @@ -150,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'])) @@ -164,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 efb601f4d2..d744dbff17 100644 --- a/mod/settings.php +++ b/mod/settings.php @@ -28,6 +28,7 @@ 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) { @@ -390,7 +391,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'); } diff --git a/mod/tagger.php b/mod/tagger.php index 5d3d1923e4..2c15cdd28c 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) { @@ -194,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/uexport.php b/mod/uexport.php index c91309e74c..dfeb25abd7 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) { @@ -55,13 +62,25 @@ function uexport_content(App $a) { } 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; } @@ -70,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 3fb36a73e4..9e19ecf117 100644 --- a/mod/videos.php +++ b/mod/videos.php @@ -217,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/viewcontacts.php b/mod/viewcontacts.php deleted file mode 100644 index 14919820dd..0000000000 --- a/mod/viewcontacts.php +++ /dev/null @@ -1,120 +0,0 @@ -argc < 2) { - throw new \Friendica\Network\HTTPException\ForbiddenException(L10n::t('Access denied.')); - } - - Nav::setSelected('home'); - - $user = DBA::selectFirst('user', [], ['nickname' => $a->argv[1], 'blocked' => false]); - if (!DBA::isResult($user)) { - throw new \Friendica\Network\HTTPException\NotFoundException(); - } - - $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/src/App.php b/src/App.php index 7ef4e49195..f59194077c 100644 --- a/src/App.php +++ b/src/App.php @@ -993,12 +993,6 @@ class App ); } - if (strstr($this->query_string, '.well-known/host-meta') && ($this->query_string != '.well-known/host-meta')) { - Module\Special\HTTPException::rawContent( - new HTTPException\NotFoundException() - ); - } - if (!$this->getMode()->isInstall()) { // Force SSL redirection if ($this->baseURL->checkRedirectHttps()) { @@ -1105,7 +1099,7 @@ class App // Compatibility with the Android Diaspora client if ($this->module == 'stream') { - $this->internalRedirect('network?f=&order=post'); + $this->internalRedirect('network?order=post'); } if ($this->module == 'conversations') { @@ -1113,15 +1107,15 @@ class App } if ($this->module == 'commented') { - $this->internalRedirect('network?f=&order=comment'); + $this->internalRedirect('network?order=comment'); } if ($this->module == 'liked') { - $this->internalRedirect('network?f=&order=comment'); + $this->internalRedirect('network?order=comment'); } if ($this->module == 'activity') { - $this->internalRedirect('network/?f=&conv=1'); + $this->internalRedirect('network?conv=1'); } if (($this->module == 'status_messages') && ($this->cmd == 'status_messages/new')) { diff --git a/src/App/Router.php b/src/App/Router.php index 796845fbb7..6720f54437 100644 --- a/src/App/Router.php +++ b/src/App/Router.php @@ -112,6 +112,7 @@ class Router $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); @@ -122,9 +123,9 @@ class Router }); $this->routeCollector->addRoute(['GET'], '/feedtest', Module\Debug\Feed::class); $this->routeCollector->addGroup('/fetch', function (RouteCollector $collector) { - $collector->addRoute(['GET'], '/{guid}/post', Module\Diaspora\Fetch::class); - $collector->addRoute(['GET'], '/{guid}/status_message', Module\Diaspora\Fetch::class); - $collector->addRoute(['GET'], '/{guid}/reshare', Module\Diaspora\Fetch::class); + $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); @@ -160,6 +161,7 @@ class Router $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) { @@ -167,7 +169,6 @@ class Router $collector->addRoute(['GET'], '/view/{id:\d+}', Module\Notifications\Notify::class); $collector->addRoute(['GET'], '/mark/all', Module\Notifications\Notify::class); }); - $this->routeCollector->addRoute(['GET'], '/notice/{id:\d+}', Module\GnuSocial\Notice::class); $this->routeCollector->addRoute(['GET'], '/objects/{guid}', Module\Objects::class); $this->routeCollector->addGroup('/oembed', function (RouteCollector $collector) { $collector->addRoute(['GET'], '/b2h', Module\Oembed::class); @@ -186,6 +187,8 @@ class Router $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) { diff --git a/src/Console/Typo.php b/src/Console/Typo.php index 5f5fa0ba68..216d057232 100644 --- a/src/Console/Typo.php +++ b/src/Console/Typo.php @@ -43,7 +43,7 @@ HELP; throw new \Asika\SimpleConsole\CommandArgsException('Too many arguments'); } - $php_path = BaseObject::getApp()->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 af2c3725c4..98ea7aa6b9 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'], diff --git a/src/Content/Nav.php b/src/Content/Nav.php index cb4564115a..ea5c0bbc05 100644 --- a/src/Content/Nav.php +++ b/src/Content/Nav.php @@ -149,9 +149,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 +170,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 7190f1ce0e..94e95e5f51 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) { diff --git a/src/Content/Smilies.php b/src/Content/Smilies.php index 9fbfd2d629..57d14633ac 100644 --- a/src/Content/Smilies.php +++ b/src/Content/Smilies.php @@ -213,7 +213,8 @@ class Smilies return $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' => []]; @@ -230,7 +231,8 @@ class Smilies $text = preg_replace_callback('/<(3+)/', 'self::pregHeart', $text); $text = self::strOrigReplace($smilies['texts'], $smilies['icons'], $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; } @@ -242,7 +244,7 @@ class Smilies */ private static function encode($m) { - return '' . Strings::base64UrlEncode($m[1]) . ''; + return '<' . $m[1] . '>' . Strings::base64UrlEncode($m[2]) . ''; } /** @@ -253,7 +255,7 @@ class Smilies */ private static function decode($m) { - return '' . Strings::base64UrlDecode($m[1]) . ''; + return '<' . $m[1] . '>' . Strings::base64UrlDecode($m[2]) . ''; } diff --git a/src/Content/Text/BBCode.php b/src/Content/Text/BBCode.php index c4735b9e93..e08d60579d 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 b5f83a803e..dcfc1d0e3d 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 @@ -121,17 +124,28 @@ class Widget } /** - * @param string $type + * 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 - * @param string $baseUrl + * @param string $all The no filter label + * @param string $baseUrl The full page request URI * @param array $options - * @param string $selected + * @param string $selected The currently selected filter option value * @return string * @throws \Exception */ - public static function filter($type, $title, $desc, $all, $baseUrl, array $options, $selected = null) + private static function filter($type, $title, $desc, $all, $baseUrl, array $options, $selected = null) { $queryString = parse_url($baseUrl, PHP_URL_QUERY); $queryArray = []; @@ -160,6 +174,37 @@ class Widget ]); } + /** + * 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 * @@ -211,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 = '') { @@ -224,15 +269,9 @@ class Widget return; } - $matches = []; - $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[] = ['ref' => $unescaped, 'name' => $unescaped]; - } + $terms = []; + foreach (FileTag::fileToArray($saved) as $savedFolderName) { + $terms[] = ['ref' => $savedFolderName, 'name' => $savedFolderName]; } return self::filter( @@ -267,15 +306,9 @@ class Widget return; } - $matches = []; $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[] = ['ref' => $unescaped, 'name' => $unescaped]; - } + foreach (FileTag::fileToArray($saved, 'category') as $savedFolderName) { + $terms[] = ['ref' => $savedFolderName, 'name' => $savedFolderName]; } return self::filter( @@ -402,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/ContactBlock.php b/src/Content/Widget/ContactBlock.php index f4fdea2fb4..bc33b9c9c9 100644 --- a/src/Content/Widget/ContactBlock.php +++ b/src/Content/Widget/ContactBlock.php @@ -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'); diff --git a/src/Core/ACL.php b/src/Core/ACL.php index e6c82fd4bf..ec31ddb7cd 100644 --- a/src/Core/ACL.php +++ b/src/Core/ACL.php @@ -259,7 +259,7 @@ 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)) { @@ -314,7 +314,7 @@ class ACL extends BaseObject '$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/Config/Cache/ConfigCache.php b/src/Core/Config/Cache/ConfigCache.php index 3314e184f3..441cdee811 100644 --- a/src/Core/Config/Cache/ConfigCache.php +++ b/src/Core/Config/Cache/ConfigCache.php @@ -2,6 +2,8 @@ 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 @@ -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; } diff --git a/src/Core/Config/Configuration.php b/src/Core/Config/Configuration.php index 532ed982a9..18191d0429 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/Installer.php b/src/Core/Installer.php index 782a139e14..046b34ea6f 100644 --- a/src/Core/Installer.php +++ b/src/Core/Installer.php @@ -156,7 +156,7 @@ class Installer '$basepath' => $basepath, '$timezone' => $configCache->get('system', 'default_timezone'), '$language' => $configCache->get('system', 'language'), - ], false); + ]); $result = file_put_contents($basepath . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'local.config.php', $txt); diff --git a/src/Core/Search.php b/src/Core/Search.php new file mode 100644 index 0000000000..e26cc0edc3 --- /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/UserImport.php b/src/Core/UserImport.php index 0a4223fecd..71767e8cef 100644 --- a/src/Core/UserImport.php +++ b/src/Core/UserImport.php @@ -10,6 +10,7 @@ use Friendica\Database\DBStructure; use Friendica\Model\Photo; use Friendica\Object\Image; use Friendica\Util\Strings; +use Friendica\Worker\Delivery; /** * @brief UserImport class @@ -39,14 +40,21 @@ class UserImport $tableColumns = DBStructure::getColumns($table); $tcols = []; + $ttype = []; // get a plain array of column names 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; } } } @@ -271,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 0f4a527f6b..8fdd60c2a7 100644 --- a/src/Core/Worker.php +++ b/src/Core/Worker.php @@ -983,7 +983,7 @@ class Worker } $url = System::baseUrl()."/worker"; - Network::fetchUrl($url, false, $redirects, 1); + Network::fetchUrl($url, false, 1); } /** @@ -1100,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 85bdbbb628..72769dca9b 100644 --- a/src/Database/DBA.php +++ b/src/Database/DBA.php @@ -45,6 +45,7 @@ class DBA */ private static $logger; private static $server_info = ''; + /** @var PDO|mysqli */ private static $connection; private static $driver; private static $error = false; @@ -288,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) { @@ -483,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]; @@ -872,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 * @@ -889,7 +927,7 @@ class DBA 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) { @@ -938,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); @@ -1119,7 +1157,7 @@ class DBA $callstack[$key] = true; - $table = self::escape($table); + $table = self::sanitizeIdentifier($table); $commands[$key] = ['table' => $table, 'conditions' => $conditions]; @@ -1272,8 +1310,6 @@ class DBA return false; } - $table = self::escape($table); - $condition_string = self::buildCondition($condition); if (is_bool($old_fields)) { @@ -1306,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); @@ -1367,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 { @@ -1383,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); @@ -1410,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); @@ -1507,6 +1541,15 @@ 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 "; @@ -1531,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/Factory/DBFactory.php b/src/Factory/DBFactory.php index 1c01f73319..7caa63ec46 100644 --- a/src/Factory/DBFactory.php +++ b/src/Factory/DBFactory.php @@ -6,6 +6,7 @@ use Friendica\Core\Config\Cache; use Friendica\Database; use Friendica\Util\Logger\VoidLogger; use Friendica\Util\Profiler; +use ParagonIE\HiddenString\HiddenString; class DBFactory { @@ -45,7 +46,7 @@ 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']; } diff --git a/src/Factory/LoggerFactory.php b/src/Factory/LoggerFactory.php index 444a98cde5..bdd85cf3ae 100644 --- a/src/Factory/LoggerFactory.php +++ b/src/Factory/LoggerFactory.php @@ -26,6 +26,7 @@ class LoggerFactory { /** * A list of classes, which shouldn't get logged + * * @var array */ private static $ignoreClassList = [ @@ -37,8 +38,8 @@ class LoggerFactory /** * 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 @@ -55,8 +56,8 @@ class LoggerFactory } $introspection = new Introspection(self::$ignoreClassList); - $level = $config->get('system', 'loglevel'); - $loglevel = self::mapLegacyConfigDebugLevel((string)$level); + $level = $config->get('system', 'loglevel'); + $loglevel = self::mapLegacyConfigDebugLevel((string)$level); switch ($config->get('system', 'logger_config', 'stream')) { case 'monolog': @@ -71,7 +72,10 @@ class LoggerFactory $stream = $config->get('system', 'logfile'); - static::addStreamHandler($logger, $stream, $loglevel); + // 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': @@ -81,7 +85,12 @@ class LoggerFactory case 'stream': default: $stream = $config->get('system', 'logfile'); - $logger = new StreamLogger($channel, $stream, $introspection, $loglevel); + // 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; } @@ -105,8 +114,8 @@ 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 @@ -120,7 +129,8 @@ class LoggerFactory $stream = $config->get('system', 'dlogfile'); $developerIp = $config->get('system', 'dlogip'); - if (!isset($developerIp) || !$debugging) { + if ((!isset($developerIp) || !$debugging) && + (!is_file($stream) || is_writable($stream))) { $logger = new VoidLogger(); Logger::setDevLogger($logger); return $logger; @@ -149,7 +159,7 @@ class LoggerFactory break; case 'syslog': - $logger = new SyslogLogger($channel, $introspection, LogLevel::DEBUG); + $logger = new SyslogLogger($channel, $introspection, LogLevel::DEBUG); break; case 'stream': @@ -172,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 @@ -208,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/Model/APContact.php b/src/Model/APContact.php index cf1f9b7231..b027d6c478 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'); @@ -96,24 +112,28 @@ class APContact extends BaseObject if (!is_null($update)) { 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 = []; @@ -152,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']); @@ -183,11 +209,13 @@ 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; } @@ -204,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']]; @@ -228,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 diff --git a/src/Model/Contact.php b/src/Model/Contact.php index ce3aeac560..a6026d6440 100644 --- a/src/Model/Contact.php +++ b/src/Model/Contact.php @@ -124,6 +124,20 @@ class Contact extends BaseObject 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 @@ -579,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)) { @@ -595,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 @@ -615,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; @@ -647,8 +662,8 @@ class Contact extends BaseObject 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]); } } @@ -894,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]); } } @@ -1078,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']; } @@ -1449,12 +1464,14 @@ class Contact extends BaseObject return $contact_id; } - $updated = ['addr' => $data['addr'], + $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 (!empty($data['keywords'])) { $updated['keywords'] = $data['keywords']; @@ -1488,7 +1505,7 @@ class Contact extends BaseObject $updated['pubkey'] = $data['pubkey']; } - if (($data['addr'] != $contact['addr']) || (!empty($data['alias']) && ($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"])) { @@ -1709,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 { @@ -1720,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']]); } } @@ -2120,63 +2134,81 @@ class Contact extends BaseObject return $contact; } - public static function addRelationship($importer, $contact, $datarray, $item = '', $sharing = false, $note = '') { + /** + * @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); - $protocol = self::getProtocol($url, $contact['network']); - if (($contact['rel'] == self::SHARING) || ($sharing && $contact['rel'] == self::FOLLOWER)) { DBA::update('contact', ['rel' => self::FRIEND, 'writable' => true, 'pending' => false], ['id' => $contact['id'], 'uid' => $importer['uid']]); } - if ($protocol == Protocol::ACTIVITYPUB) { - ActivityPub\Transmitter::sendContactAccept($contact['url'], $contact['hub-verify'], $importer['uid']); - } - - // send email notification to owner? + return true; } else { - $protocol = self::getProtocol($url, $network); - + // 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(), @@ -2220,20 +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']]); - $protocol = self::getProtocol($contact['url'], $contact['network']); - - if ($protocol == 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 = "") @@ -2342,6 +2370,9 @@ class Contact extends BaseObject return $url ?: $contact_url; // Equivalent to: ($url != '') ? $url : $contact_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); } @@ -2431,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 d8657c1e9a..42742f18e0 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 2ad864c9c4..f4d04634fa 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