Merge branch '2024.06-rc' into stable

This commit is contained in:
Tobias Diekershoff 2024-08-17 17:22:22 +02:00
commit c8c79ddc71
427 changed files with 34011 additions and 29029 deletions

View file

@ -1,3 +1,87 @@
Version 2024.08 (unreleased)
Friendica Core
Updates to the translations AR, CS, DE, ES, FR, GD, HU, IS, IT, JA, NL, PL, RU, SV
Updates to the documentation [foss-, loma-one, mexon]
Updates to the themes (frio) [haheute]
General code cleanup [annando, haheute, mexon, MrPetovan]
Improved the redirection for contact actions [annando]
Improved the performance while fetching of replies [annando]
Improved the performance when visiting remote profiles [annando]
Improved OWA [annando]
Improved the procession of worker tasks [annando]
Improved performance in the probing process [annando]
Improved INBOX performance [annando]
Improved perfomance when expireing postings [annando]
Improved mirroring settings for RSS contacts [annando]
Improved supported image formats [annando]
Improved handling of CC for comments [annando]
Improved handling of "sensitive" flags for postings [annando]
Improved display of log levels [annando, tobiasd]
Improved handling of permissions for attachments [annando]
Improved addon handling [MrPetovan]
Improved API for channels and circles [annando]
Improved performance while displaying local postings [annando]
Improved federation with pixelfed, threads [annando]
Improved integration with Bluesky [annando]
Improved automatic cleanup of the database [annando]
Fixed access to restricted timeline via API [annando]
Fixed problem fetching from INBOXes [annando]
Fixed display of contacts from unavailable networks [annando]
Fixed profile display [annando]
Fixed a problem with local un-/follows [annando]
Fixed the uimport POST endpoint [annando]
Fixed problem with 0Auth logins [annando]
Fixed problem with @mentions in comments [annando]
Fixed XSS in profile fields [annando, apexrabbit, Devilx86, MrPetovan, ponlayookm]
Fixed bug in deleting unused cached avatar pictures [annando]
Fixed paging bug on the media tab of remote profiles [annando]
Fixed display of attached links [annando]
Fixed a bug in circle only contacts [annando]
Fixed display of moderation reports [MrPetovan, TheTomcat14]
Fixed delivery problems to group postings [annando]
Added monitoring service endpoint [annando]
Added admin option display_link_length to set the length of displayed links [annando]
Added the possibility to upload media files via API [annando]
Added console command to clear avatar cache [annando]
Added platform data to the API [annando]
Added parsing support for Nodeinfo 2.1 and 2.2 [annando]
Added node description to Nodeinfo [annando]
Added owner information of relay accounts [annando]
Added option for users about how to transmit postings with titles [annando]
Added for non HTML content of feeds [annando]
Added reshares for postings from Bluesky and tumbl [annando]
Added public forums with manual request approval [annando]
Added "next try" information for deferred worker jobs listing [annando]
Added support of FEP-e232 [annando]
Added automatic closure of registration if admin becomes inactive [annando]
Added channel only option for contacts [annando]
Friendica Addons
Updates to the translations AR, CS, DE, FR, IT, PL, SV
Blockbot
Added Relatica to good client list [hankg]
Improved agent identifier list [annando]
Bluesky
Added monitoring statistics [annando]
Added support of sensitive postings [annando]
Improved API handling [annando]
Improved fetching of user DID [annando]
Fixed conversion BS/Friendica handles [annando]
jsuploader
Improved detection of supported file types [annando]
mailstream
Improved image handling [mexon]
tumblr
Added monitoring statistics [annando]
Improved quoted postings [annando]
Closed Issues
11963, 13714, 13787, 13812, 13821, 13910, 14012, 14030, 14059,
14077, 14079, 14045, 14052, 14055, 14081, 14084, 14102, 14110,
14118, 14121, 14125, 14132, 14134, 14153, 14160, 14170, 14175,
14186, 14197, 14220, 14228, 14231, 14240, 14249, 14250, 14285,
14295, 14303, 14312, 14324, 14329, 14349, 14364
Version 2024.03 (2024-03-21) Version 2024.03 (2024-03-21)
Friendica Core Friendica Core
Updates to the translations AR, BG, CS, DE, EO, ES, FR, GD, HU, IS, IT, JA, PL, RO, RU, SV Updates to the translations AR, BG, CS, DE, EO, ES, FR, GD, HU, IS, IT, JA, PL, RO, RU, SV
@ -285,7 +369,7 @@ Version 2023.04 (2023-04-23)
twitter twitter
Improve remote-self handling [annando] Improve remote-self handling [annando]
impressum impressum
Avoide obfuscation on un-set email addresses [MrPestovan] Avoide obfuscation on un-set email addresses [MrPetovan]
notifyall notifyall
Fixed a bug selecting the email addresses [nupplaphil] Fixed a bug selecting the email addresses [nupplaphil]
tumblr tumblr
@ -1734,7 +1818,7 @@ Version 2018.05 (2018-06-01)
Friendica Addons: Friendica Addons:
Updates to the translations (DE, EN_GB, EN_US, ES, FI, FR, IS, IT, NL, PL, RU, ZH_CN) [translation teams] Updates to the translations (DE, EN_GB, EN_US, ES, FI, FR, IS, IT, NL, PL, RU, ZH_CN) [translation teams]
advancedcontentfilter: new addon with advanced filter capabilities [MrPetova] advancedcontentfilter: new addon with advanced filter capabilities [MrPetovan]
catavatar: new addon for profile pictures based on David Revoy's cat-avatar generator [annando, fabrixxm, tobiasd] catavatar: new addon for profile pictures based on David Revoy's cat-avatar generator [annando, fabrixxm, tobiasd]
languagefilter: better help text [andyhee] languagefilter: better help text [andyhee]
mathjax: fixed the config form and adopted new CDN URL [tobiasd] mathjax: fixed the config form and adopted new CDN URL [tobiasd]

View file

@ -1 +1 @@
2024.03 2024.06-rc

View file

@ -138,9 +138,9 @@ function execute_tests() {
if [ -n "${USEDOCKER}" ]; then if [ -n "${USEDOCKER}" ]; then
echo "Fire up the mysql docker" echo "Fire up the mysql docker"
DOCKER_CONTAINER_ID=$(docker run \ DOCKER_CONTAINER_ID=$(docker run \
-e MYSQL_ROOT_PASSWORD=friendica \ -e MYSQL_ROOT_PASSWORD="${DATABASE_PASSWORD}" \
-e MYSQL_USER="${DATABASE_USER}" \ -e MYSQL_USER="${DATABASE_USER}" \
-e MYSQL_PASSWORD=friendica \ -e MYSQL_PASSWORD="${DATABASE_PASSWORD}" \
-e MYSQL_DATABASE="${DATABASE_NAME}" \ -e MYSQL_DATABASE="${DATABASE_NAME}" \
-d mysql) -d mysql)
DATABASE_HOST=$(docker inspect --format="{{.NetworkSettings.IPAddress}}" "${DOCKER_CONTAINER_ID}") DATABASE_HOST=$(docker inspect --format="{{.NetworkSettings.IPAddress}}" "${DOCKER_CONTAINER_ID}")
@ -152,8 +152,8 @@ function execute_tests() {
echo "To use the docker container set the USEDOCKER environment variable" echo "To use the docker container set the USEDOCKER environment variable"
exit 3 exit 3
fi fi
mysql -u "${DATABASE_USER}" -pfriendica -e "DROP DATABASE IF EXISTS ${DATABASE_NAME}" -h ${DATABASE_HOST} || true mysql -u "${DATABASE_USER}" -p"${DATABASE_PASSWORD}" -e "DROP DATABASE IF EXISTS ${DATABASE_NAME}" -h ${DATABASE_HOST} || true
mysql -u "${DATABASE_USER}" -pfriendica -e "CREATE DATABASE ${DATABASE_NAME} DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci" -h ${DATABASE_HOST} mysql -u "${DATABASE_USER}" -p"${DATABASE_PASSWORD}" -e "CREATE DATABASE ${DATABASE_NAME} DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci" -h ${DATABASE_HOST}
else else
DATABASE_HOST=mysql DATABASE_HOST=mysql
fi fi
@ -171,9 +171,9 @@ function execute_tests() {
if [ -n "${USEDOCKER}" ]; then if [ -n "${USEDOCKER}" ]; then
echo "Fire up the mariadb docker" echo "Fire up the mariadb docker"
DOCKER_CONTAINER_ID=$(docker run \ DOCKER_CONTAINER_ID=$(docker run \
-e MYSQL_ROOT_PASSWORD=friendica \ -e MYSQL_ROOT_PASSWORD="${DATABASE_PASSWORD}" \
-e MYSQL_USER="${DATABASE_USER}" \ -e MYSQL_USER="${DATABASE_USER}" \
-e MYSQL_PASSWORD=friendica \ -e MYSQL_PASSWORD="${DATABASE_PASSWORD}" \
-e MYSQL_DATABASE="${DATABASE_NAME}" \ -e MYSQL_DATABASE="${DATABASE_NAME}" \
-d mariadb) -d mariadb)
DATABASE_HOST=$(docker inspect --format="{{.NetworkSettings.IPAddress}}" "${DOCKER_CONTAINER_ID}") DATABASE_HOST=$(docker inspect --format="{{.NetworkSettings.IPAddress}}" "${DOCKER_CONTAINER_ID}")
@ -185,8 +185,8 @@ function execute_tests() {
echo "To use the docker container set the USEDOCKER environment variable" echo "To use the docker container set the USEDOCKER environment variable"
exit 3 exit 3
fi fi
mysql -u "${DATABASE_USER}" -pfriendica -e "DROP DATABASE IF EXISTS ${DATABASE_NAME}" -h ${DATABASE_HOST} || true mysql -u "${DATABASE_USER}" -p"${DATABASE_PASSWORD}" -e "DROP DATABASE IF EXISTS ${DATABASE_NAME}" -h ${DATABASE_HOST} || true
mysql -u "${DATABASE_USER}" -pfriendica -e "CREATE DATABASE ${DATABASE_NAME} DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci" -h ${DATABASE_HOST} mysql -u "${DATABASE_USER}" -p"${DATABASE_PASSWORD}" -e "CREATE DATABASE ${DATABASE_NAME} DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci" -h ${DATABASE_HOST}
else else
DATABASE_HOST=mariadb DATABASE_HOST=mariadb
fi fi
@ -203,14 +203,14 @@ function execute_tests() {
if [ -n "${USEDOCKER}" ]; then if [ -n "${USEDOCKER}" ]; then
echo "Initialize database..." echo "Initialize database..."
docker exec ${DOCKER_CONTAINER_ID} mysql -u root -pfriendica -e "CREATE DATABASE IF NOT EXISTS ${DATABASE_NAME};" docker exec ${DOCKER_CONTAINER_ID} mysql -u root -p"${DATABASE_PASSWORD}" -e "CREATE DATABASE IF NOT EXISTS ${DATABASE_NAME};"
fi fi
export MYSQL_HOST="${DATABASE_HOST}" export MYSQL_HOST="${DATABASE_HOST}"
#call installer #call installer
echo "Installing Friendica..." echo "Installing Friendica..."
"${PHP}" ./bin/console.php autoinstall --dbuser="${DATABASE_USER}" --dbpass=friendica --dbdata="${DATABASE_NAME}" --dbhost="${DATABASE_HOST}" --url=https://friendica.local --admin=admin@friendica.local "${PHP}" ./bin/console.php autoinstall --dbuser="${DATABASE_USER}" --dbpass="${DATABASE_PASSWORD}" --dbdata="${DATABASE_NAME}" --dbhost="${DATABASE_HOST}" --url=https://friendica.local --admin=admin@friendica.local
fi fi
#test execution #test execution

View file

@ -72,7 +72,7 @@ echo "Extract strings to $OUTFILE.."
[ -f "$OUTFILE" ] && rm "$OUTFILE"; touch "$OUTFILE" [ -f "$OUTFILE" ] && rm "$OUTFILE"; touch "$OUTFILE"
# shellcheck disable=SC2086 # $FINDOPTS is meant to be split # shellcheck disable=SC2086 # $FINDOPTS is meant to be split
find_result=$(find "$FINDSTARTDIR" $FINDOPTS -name "*.php" -type f | LC_ALL=C sort --stable) find_result=$(find "$FINDSTARTDIR" $FINDOPTS -name "*.php" -type f | LC_ALL=C sort -s)
total_files=$(wc -l <<< "${find_result}") total_files=$(wc -l <<< "${find_result}")
@ -86,7 +86,7 @@ do
if [ ! -d "$file" ] if [ ! -d "$file" ]
then then
# shellcheck disable=SC2086 # $KEYWORDS is meant to be split # shellcheck disable=SC2086 # $KEYWORDS is meant to be split
xgettext $KEYWORDS -j -o "$OUTFILE" --from-code=UTF-8 "$file" || exit 1 xgettext $KEYWORDS --no-wrap -j -o "$OUTFILE" --from-code=UTF-8 "$file" || exit 1
sed -i.bkp "s/CHARSET/UTF-8/g" "$OUTFILE" sed -i.bkp "s/CHARSET/UTF-8/g" "$OUTFILE"
fi fi
(( count++ )) (( count++ ))

View file

@ -1,6 +1,6 @@
-- ------------------------------------------ -- ------------------------------------------
-- Friendica 2024.03 (Yellow Archangel) -- Friendica 2024.06-rc (Yellow Archangel)
-- DB_UPDATE_VERSION 1557 -- DB_UPDATE_VERSION 1571
-- ------------------------------------------ -- ------------------------------------------
@ -11,7 +11,7 @@ CREATE TABLE IF NOT EXISTS `gserver` (
`id` int unsigned NOT NULL auto_increment COMMENT 'sequential ID', `id` int unsigned NOT NULL auto_increment COMMENT 'sequential ID',
`url` varbinary(383) NOT NULL DEFAULT '' COMMENT '', `url` varbinary(383) NOT NULL DEFAULT '' COMMENT '',
`nurl` varbinary(383) NOT NULL DEFAULT '' COMMENT '', `nurl` varbinary(383) NOT NULL DEFAULT '' COMMENT '',
`version` varchar(255) NOT NULL DEFAULT '' COMMENT '', `version` varchar(255) NOT NULL DEFAULT '' COMMENT 'The version of this server software.',
`site_name` varchar(255) NOT NULL DEFAULT '' COMMENT '', `site_name` varchar(255) NOT NULL DEFAULT '' COMMENT '',
`info` text COMMENT '', `info` text COMMENT '',
`register_policy` tinyint NOT NULL DEFAULT 0 COMMENT '', `register_policy` tinyint NOT NULL DEFAULT 0 COMMENT '',
@ -23,10 +23,14 @@ CREATE TABLE IF NOT EXISTS `gserver` (
`local-comments` int unsigned COMMENT 'Number of local comments', `local-comments` int unsigned COMMENT 'Number of local comments',
`directory-type` tinyint DEFAULT 0 COMMENT 'Type of directory service (Poco, Mastodon)', `directory-type` tinyint DEFAULT 0 COMMENT 'Type of directory service (Poco, Mastodon)',
`poco` varbinary(383) NOT NULL DEFAULT '' COMMENT '', `poco` varbinary(383) NOT NULL DEFAULT '' COMMENT '',
`openwebauth` varbinary(383) COMMENT 'Path to the OpenWebAuth endpoint',
`authredirect` varbinary(383) COMMENT 'Path to the authRedirect endpoint',
`noscrape` varbinary(383) NOT NULL DEFAULT '' COMMENT '', `noscrape` varbinary(383) NOT NULL DEFAULT '' COMMENT '',
`network` char(4) NOT NULL DEFAULT '' COMMENT '', `network` char(4) NOT NULL DEFAULT '' COMMENT '',
`protocol` tinyint unsigned COMMENT 'The protocol of the server', `protocol` tinyint unsigned COMMENT 'The protocol of the server',
`platform` varchar(255) NOT NULL DEFAULT '' COMMENT '', `platform` varchar(255) NOT NULL DEFAULT '' COMMENT 'The canonical name of this server software.',
`repository` varbinary(383) COMMENT 'The url of the source code repository of this server software.',
`homepage` varbinary(383) COMMENT 'The url of the homepage of this server software.',
`relay-subscribe` boolean NOT NULL DEFAULT '0' COMMENT 'Has the server subscribed to the relay system', `relay-subscribe` boolean NOT NULL DEFAULT '0' COMMENT 'Has the server subscribed to the relay system',
`relay-scope` varchar(10) NOT NULL DEFAULT '' COMMENT 'The scope of messages that the server wants to get', `relay-scope` varchar(10) NOT NULL DEFAULT '' COMMENT 'The scope of messages that the server wants to get',
`detection-method` tinyint unsigned COMMENT 'Method that had been used to detect that server', `detection-method` tinyint unsigned COMMENT 'Method that had been used to detect that server',
@ -813,6 +817,7 @@ CREATE TABLE IF NOT EXISTS `inbox-entry` (
`activity-id` varbinary(383) COMMENT 'id of the incoming activity', `activity-id` varbinary(383) COMMENT 'id of the incoming activity',
`object-id` varbinary(383) COMMENT '', `object-id` varbinary(383) COMMENT '',
`in-reply-to-id` varbinary(383) COMMENT '', `in-reply-to-id` varbinary(383) COMMENT '',
`context` varbinary(383) COMMENT '',
`conversation` varbinary(383) COMMENT '', `conversation` varbinary(383) COMMENT '',
`type` varchar(64) COMMENT 'Type of the activity', `type` varchar(64) COMMENT 'Type of the activity',
`object-type` varchar(64) COMMENT 'Type of the object activity', `object-type` varchar(64) COMMENT 'Type of the object activity',
@ -823,6 +828,7 @@ CREATE TABLE IF NOT EXISTS `inbox-entry` (
`push` boolean COMMENT 'Is the entry pushed or have pulled it?', `push` boolean COMMENT 'Is the entry pushed or have pulled it?',
`trust` boolean COMMENT 'Do we trust this entry?', `trust` boolean COMMENT 'Do we trust this entry?',
`wid` int unsigned COMMENT 'Workerqueue id', `wid` int unsigned COMMENT 'Workerqueue id',
`retrial` tinyint unsigned DEFAULT 0 COMMENT 'Retrial counter',
PRIMARY KEY(`id`), PRIMARY KEY(`id`),
UNIQUE INDEX `activity-id` (`activity-id`), UNIQUE INDEX `activity-id` (`activity-id`),
INDEX `object-id` (`object-id`), INDEX `object-id` (`object-id`),
@ -1181,6 +1187,7 @@ CREATE TABLE IF NOT EXISTS `post` (
`parent-uri-id` int unsigned COMMENT 'Id of the item-uri table that contains the parent uri', `parent-uri-id` int unsigned COMMENT 'Id of the item-uri table that contains the parent uri',
`thr-parent-id` int unsigned COMMENT 'Id of the item-uri table that contains the thread parent uri', `thr-parent-id` int unsigned COMMENT 'Id of the item-uri table that contains the thread parent uri',
`external-id` int unsigned COMMENT 'Id of the item-uri table entry that contains the external uri', `external-id` int unsigned COMMENT 'Id of the item-uri table entry that contains the external uri',
`replies-id` int unsigned COMMENT 'Id of the item-uri table entry that contains the endpoint for the replies collection',
`created` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT 'Creation timestamp.', `created` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT 'Creation timestamp.',
`edited` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT 'Date of last edit (default is created)', `edited` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT 'Date of last edit (default is created)',
`received` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT 'datetime', `received` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT 'datetime',
@ -1199,6 +1206,7 @@ CREATE TABLE IF NOT EXISTS `post` (
INDEX `parent-uri-id` (`parent-uri-id`), INDEX `parent-uri-id` (`parent-uri-id`),
INDEX `thr-parent-id` (`thr-parent-id`), INDEX `thr-parent-id` (`thr-parent-id`),
INDEX `external-id` (`external-id`), INDEX `external-id` (`external-id`),
INDEX `replies-id` (`replies-id`),
INDEX `owner-id` (`owner-id`), INDEX `owner-id` (`owner-id`),
INDEX `author-id` (`author-id`), INDEX `author-id` (`author-id`),
INDEX `causer-id` (`causer-id`), INDEX `causer-id` (`causer-id`),
@ -1207,6 +1215,7 @@ CREATE TABLE IF NOT EXISTS `post` (
FOREIGN KEY (`parent-uri-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE, FOREIGN KEY (`parent-uri-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE,
FOREIGN KEY (`thr-parent-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE, FOREIGN KEY (`thr-parent-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE,
FOREIGN KEY (`external-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE, FOREIGN KEY (`external-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE,
FOREIGN KEY (`replies-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE,
FOREIGN KEY (`owner-id`) REFERENCES `contact` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT, FOREIGN KEY (`owner-id`) REFERENCES `contact` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT,
FOREIGN KEY (`author-id`) REFERENCES `contact` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT, FOREIGN KEY (`author-id`) REFERENCES `contact` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT,
FOREIGN KEY (`causer-id`) REFERENCES `contact` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT, FOREIGN KEY (`causer-id`) REFERENCES `contact` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT,
@ -1347,7 +1356,7 @@ CREATE TABLE IF NOT EXISTS `post-engagement` (
`uri-id` int unsigned NOT NULL COMMENT 'Id of the item-uri table entry that contains the item uri', `uri-id` int unsigned NOT NULL COMMENT 'Id of the item-uri table entry that contains the item uri',
`owner-id` int unsigned NOT NULL DEFAULT 0 COMMENT 'Item owner', `owner-id` int unsigned NOT NULL DEFAULT 0 COMMENT 'Item owner',
`contact-type` tinyint NOT NULL DEFAULT 0 COMMENT 'Person, organisation, news, community, relay', `contact-type` tinyint NOT NULL DEFAULT 0 COMMENT 'Person, organisation, news, community, relay',
`media-type` tinyint NOT NULL DEFAULT 0 COMMENT 'Type of media in a bit array (1 = image, 2 = video, 4 = audio', `media-type` tinyint NOT NULL DEFAULT 0 COMMENT 'Type of media in a bit array (1 = image, 2 = video, 4 = audio)',
`language` char(2) COMMENT 'Language information about this post in the ISO 639-1 format', `language` char(2) COMMENT 'Language information about this post in the ISO 639-1 format',
`searchtext` mediumtext COMMENT 'Simplified text for the full text search', `searchtext` mediumtext COMMENT 'Simplified text for the full text search',
`size` int unsigned COMMENT 'Body size', `size` int unsigned COMMENT 'Body size',
@ -1439,6 +1448,36 @@ CREATE TABLE IF NOT EXISTS `post-media` (
FOREIGN KEY (`media-uri-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE FOREIGN KEY (`media-uri-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE
) DEFAULT COLLATE utf8mb4_general_ci COMMENT='Attached media'; ) DEFAULT COLLATE utf8mb4_general_ci COMMENT='Attached media';
--
-- TABLE post-origin
--
CREATE TABLE IF NOT EXISTS `post-origin` (
`id` int unsigned NOT NULL,
`uri-id` int unsigned NOT NULL COMMENT 'Id of the item-uri table entry that contains the item uri',
`uid` mediumint unsigned NOT NULL COMMENT 'Owner id which owns this copy of the item',
`parent-uri-id` int unsigned COMMENT 'Id of the item-uri table that contains the parent uri',
`thr-parent-id` int unsigned COMMENT 'Id of the item-uri table that contains the thread parent uri',
`created` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT 'Creation timestamp.',
`received` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT 'datetime',
`gravity` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '',
`vid` smallint unsigned COMMENT 'Id of the verb table entry that contains the activity verbs',
`private` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '0=public, 1=private, 2=unlisted',
`wall` boolean NOT NULL DEFAULT '0' COMMENT 'This item was posted to the wall of uid',
PRIMARY KEY(`id`),
UNIQUE INDEX `uid_uri-id` (`uid`,`uri-id`),
INDEX `uri-id` (`uri-id`),
INDEX `parent-uri-id` (`parent-uri-id`),
INDEX `thr-parent-id` (`thr-parent-id`),
INDEX `vid` (`vid`),
INDEX `parent-uri-id_uid` (`parent-uri-id`,`uid`),
INDEX `uid_wall_received` (`uid`,`wall`,`received`),
FOREIGN KEY (`uri-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE,
FOREIGN KEY (`uid`) REFERENCES `user` (`uid`) ON UPDATE RESTRICT ON DELETE CASCADE,
FOREIGN KEY (`parent-uri-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE,
FOREIGN KEY (`thr-parent-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE,
FOREIGN KEY (`vid`) REFERENCES `verb` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT
) DEFAULT COLLATE utf8mb4_general_ci COMMENT='Posts from local users';
-- --
-- TABLE post-question -- TABLE post-question
-- --
@ -1471,7 +1510,7 @@ CREATE TABLE IF NOT EXISTS `post-question-option` (
CREATE TABLE IF NOT EXISTS `post-searchindex` ( CREATE TABLE IF NOT EXISTS `post-searchindex` (
`uri-id` int unsigned NOT NULL COMMENT 'Id of the item-uri table entry that contains the item uri', `uri-id` int unsigned NOT NULL COMMENT 'Id of the item-uri table entry that contains the item uri',
`owner-id` int unsigned NOT NULL DEFAULT 0 COMMENT 'Item owner', `owner-id` int unsigned NOT NULL DEFAULT 0 COMMENT 'Item owner',
`media-type` tinyint NOT NULL DEFAULT 0 COMMENT 'Type of media in a bit array (1 = image, 2 = video, 4 = audio', `media-type` tinyint NOT NULL DEFAULT 0 COMMENT 'Type of media in a bit array (1 = image, 2 = video, 4 = audio)',
`language` char(2) COMMENT 'Language information about this post in the ISO 639-1 format', `language` char(2) COMMENT 'Language information about this post in the ISO 639-1 format',
`searchtext` mediumtext COMMENT 'Simplified text for the full text search', `searchtext` mediumtext COMMENT 'Simplified text for the full text search',
`size` int unsigned COMMENT 'Body size', `size` int unsigned COMMENT 'Body size',
@ -1506,6 +1545,7 @@ CREATE TABLE IF NOT EXISTS `post-tag` (
-- --
CREATE TABLE IF NOT EXISTS `post-thread` ( CREATE TABLE IF NOT EXISTS `post-thread` (
`uri-id` int unsigned NOT NULL COMMENT 'Id of the item-uri table entry that contains the item uri', `uri-id` int unsigned NOT NULL COMMENT 'Id of the item-uri table entry that contains the item uri',
`context-id` int unsigned COMMENT 'Id of the item-uri table entry that contains the endpoint for the context collection',
`conversation-id` int unsigned COMMENT 'Id of the item-uri table entry that contains the conversation uri', `conversation-id` int unsigned COMMENT 'Id of the item-uri table entry that contains the conversation uri',
`owner-id` int unsigned NOT NULL DEFAULT 0 COMMENT 'Item owner', `owner-id` int unsigned NOT NULL DEFAULT 0 COMMENT 'Item owner',
`author-id` int unsigned NOT NULL DEFAULT 0 COMMENT 'Item author', `author-id` int unsigned NOT NULL DEFAULT 0 COMMENT 'Item author',
@ -1516,6 +1556,7 @@ CREATE TABLE IF NOT EXISTS `post-thread` (
`changed` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT 'Date that something in the conversation changed, indicating clients should fetch the conversation again', `changed` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT 'Date that something in the conversation changed, indicating clients should fetch the conversation again',
`commented` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT '', `commented` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT '',
PRIMARY KEY(`uri-id`), PRIMARY KEY(`uri-id`),
INDEX `context-id` (`context-id`),
INDEX `conversation-id` (`conversation-id`), INDEX `conversation-id` (`conversation-id`),
INDEX `owner-id` (`owner-id`), INDEX `owner-id` (`owner-id`),
INDEX `author-id` (`author-id`), INDEX `author-id` (`author-id`),
@ -1523,6 +1564,7 @@ CREATE TABLE IF NOT EXISTS `post-thread` (
INDEX `received` (`received`), INDEX `received` (`received`),
INDEX `commented` (`commented`), INDEX `commented` (`commented`),
FOREIGN KEY (`uri-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE, FOREIGN KEY (`uri-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE,
FOREIGN KEY (`context-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE,
FOREIGN KEY (`conversation-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE, FOREIGN KEY (`conversation-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE,
FOREIGN KEY (`owner-id`) REFERENCES `contact` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT, FOREIGN KEY (`owner-id`) REFERENCES `contact` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT,
FOREIGN KEY (`author-id`) REFERENCES `contact` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT, FOREIGN KEY (`author-id`) REFERENCES `contact` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT,
@ -1538,6 +1580,7 @@ CREATE TABLE IF NOT EXISTS `post-user` (
`parent-uri-id` int unsigned COMMENT 'Id of the item-uri table that contains the parent uri', `parent-uri-id` int unsigned COMMENT 'Id of the item-uri table that contains the parent uri',
`thr-parent-id` int unsigned COMMENT 'Id of the item-uri table that contains the thread parent uri', `thr-parent-id` int unsigned COMMENT 'Id of the item-uri table that contains the thread parent uri',
`external-id` int unsigned COMMENT 'Id of the item-uri table entry that contains the external uri', `external-id` int unsigned COMMENT 'Id of the item-uri table entry that contains the external uri',
`replies-id` int unsigned COMMENT 'Id of the item-uri table entry that contains the endpoint for the replies collection',
`created` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT 'Creation timestamp.', `created` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT 'Creation timestamp.',
`edited` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT 'Date of last edit (default is created)', `edited` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT 'Date of last edit (default is created)',
`received` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT 'datetime', `received` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT 'datetime',
@ -1550,6 +1593,7 @@ CREATE TABLE IF NOT EXISTS `post-user` (
`post-reason` tinyint unsigned NOT NULL DEFAULT 0 COMMENT 'Reason why the post arrived at the user', `post-reason` tinyint unsigned NOT NULL DEFAULT 0 COMMENT 'Reason why the post arrived at the user',
`vid` smallint unsigned COMMENT 'Id of the verb table entry that contains the activity verbs', `vid` smallint unsigned COMMENT 'Id of the verb table entry that contains the activity verbs',
`private` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '0=public, 1=private, 2=unlisted', `private` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '0=public, 1=private, 2=unlisted',
`restrictions` tinyint unsigned COMMENT 'Bit array of post restrictions (1 = Reply, 2 = Like, 4 = Announce)',
`global` boolean NOT NULL DEFAULT '0' COMMENT '', `global` boolean NOT NULL DEFAULT '0' COMMENT '',
`visible` boolean NOT NULL DEFAULT '0' COMMENT '', `visible` boolean NOT NULL DEFAULT '0' COMMENT '',
`deleted` boolean NOT NULL DEFAULT '0' COMMENT 'item has been marked for deletion', `deleted` boolean NOT NULL DEFAULT '0' COMMENT 'item has been marked for deletion',
@ -1569,6 +1613,7 @@ CREATE TABLE IF NOT EXISTS `post-user` (
INDEX `parent-uri-id` (`parent-uri-id`), INDEX `parent-uri-id` (`parent-uri-id`),
INDEX `thr-parent-id` (`thr-parent-id`), INDEX `thr-parent-id` (`thr-parent-id`),
INDEX `external-id` (`external-id`), INDEX `external-id` (`external-id`),
INDEX `replies-id` (`replies-id`),
INDEX `owner-id` (`owner-id`), INDEX `owner-id` (`owner-id`),
INDEX `author-id` (`author-id`), INDEX `author-id` (`author-id`),
INDEX `causer-id` (`causer-id`), INDEX `causer-id` (`causer-id`),
@ -1589,6 +1634,7 @@ CREATE TABLE IF NOT EXISTS `post-user` (
FOREIGN KEY (`parent-uri-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE, FOREIGN KEY (`parent-uri-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE,
FOREIGN KEY (`thr-parent-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE, FOREIGN KEY (`thr-parent-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE,
FOREIGN KEY (`external-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE, FOREIGN KEY (`external-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE,
FOREIGN KEY (`replies-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE,
FOREIGN KEY (`owner-id`) REFERENCES `contact` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT, FOREIGN KEY (`owner-id`) REFERENCES `contact` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT,
FOREIGN KEY (`author-id`) REFERENCES `contact` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT, FOREIGN KEY (`author-id`) REFERENCES `contact` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT,
FOREIGN KEY (`causer-id`) REFERENCES `contact` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT, FOREIGN KEY (`causer-id`) REFERENCES `contact` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT,
@ -1604,6 +1650,7 @@ CREATE TABLE IF NOT EXISTS `post-user` (
-- --
CREATE TABLE IF NOT EXISTS `post-thread-user` ( CREATE TABLE IF NOT EXISTS `post-thread-user` (
`uri-id` int unsigned NOT NULL COMMENT 'Id of the item-uri table entry that contains the item uri', `uri-id` int unsigned NOT NULL COMMENT 'Id of the item-uri table entry that contains the item uri',
`context-id` int unsigned COMMENT 'Id of the item-uri table entry that contains the endpoint for the context collection',
`conversation-id` int unsigned COMMENT 'Id of the item-uri table entry that contains the conversation uri', `conversation-id` int unsigned COMMENT 'Id of the item-uri table entry that contains the conversation uri',
`owner-id` int unsigned NOT NULL DEFAULT 0 COMMENT 'Item owner', `owner-id` int unsigned NOT NULL DEFAULT 0 COMMENT 'Item owner',
`author-id` int unsigned NOT NULL DEFAULT 0 COMMENT 'Item author', `author-id` int unsigned NOT NULL DEFAULT 0 COMMENT 'Item author',
@ -1629,6 +1676,7 @@ CREATE TABLE IF NOT EXISTS `post-thread-user` (
`post-user-id` int unsigned COMMENT 'Id of the post-user table', `post-user-id` int unsigned COMMENT 'Id of the post-user table',
PRIMARY KEY(`uid`,`uri-id`), PRIMARY KEY(`uid`,`uri-id`),
INDEX `uri-id` (`uri-id`), INDEX `uri-id` (`uri-id`),
INDEX `context-id` (`context-id`),
INDEX `conversation-id` (`conversation-id`), INDEX `conversation-id` (`conversation-id`),
INDEX `owner-id` (`owner-id`), INDEX `owner-id` (`owner-id`),
INDEX `author-id` (`author-id`), INDEX `author-id` (`author-id`),
@ -1651,6 +1699,7 @@ CREATE TABLE IF NOT EXISTS `post-thread-user` (
INDEX `contact-id_received` (`contact-id`,`received`), INDEX `contact-id_received` (`contact-id`,`received`),
INDEX `contact-id_created` (`contact-id`,`created`), INDEX `contact-id_created` (`contact-id`,`created`),
FOREIGN KEY (`uri-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE, FOREIGN KEY (`uri-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE,
FOREIGN KEY (`context-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE,
FOREIGN KEY (`conversation-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE, FOREIGN KEY (`conversation-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE,
FOREIGN KEY (`owner-id`) REFERENCES `contact` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT, FOREIGN KEY (`owner-id`) REFERENCES `contact` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT,
FOREIGN KEY (`author-id`) REFERENCES `contact` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT, FOREIGN KEY (`author-id`) REFERENCES `contact` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT,
@ -1947,6 +1996,7 @@ CREATE TABLE IF NOT EXISTS `user-contact` (
`ignored` boolean COMMENT 'Posts from this contact are ignored', `ignored` boolean COMMENT 'Posts from this contact are ignored',
`collapsed` boolean COMMENT 'Posts from this contact are collapsed', `collapsed` boolean COMMENT 'Posts from this contact are collapsed',
`hidden` boolean COMMENT 'This contact is hidden from the others', `hidden` boolean COMMENT 'This contact is hidden from the others',
`channel-only` boolean COMMENT 'This contact is displayed only in channels, but not in the network stream.',
`is-blocked` boolean COMMENT 'User is blocked by this contact', `is-blocked` boolean COMMENT 'User is blocked by this contact',
`channel-frequency` tinyint unsigned COMMENT 'Controls the frequency of the appearance of this contact in channels', `channel-frequency` tinyint unsigned COMMENT 'Controls the frequency of the appearance of this contact in channels',
`pending` boolean COMMENT '', `pending` boolean COMMENT '',
@ -2065,6 +2115,38 @@ CREATE VIEW `post-counts-view` AS SELECT
FROM `post-counts` FROM `post-counts`
INNER JOIN `verb` ON `verb`.`id` = `post-counts`.`vid`; INNER JOIN `verb` ON `verb`.`id` = `post-counts`.`vid`;
--
-- VIEW post-engagement-user-view
--
DROP VIEW IF EXISTS `post-engagement-user-view`;
CREATE VIEW `post-engagement-user-view` AS SELECT
`post-thread-user`.`uid` AS `uid`,
`post-engagement`.`uri-id` AS `uri-id`,
`post-engagement`.`owner-id` AS `owner-id`,
`post-engagement`.`media-type` AS `media-type`,
`post-engagement`.`language` AS `language`,
`post-engagement`.`searchtext` AS `searchtext`,
`post-engagement`.`size` AS `size`,
`post-thread-user`.`commented` AS `commented`,
`post-thread-user`.`received` AS `received`,
`post-thread-user`.`created` AS `created`,
`post-thread-user`.`network` AS `network`,
`post-engagement`.`language` AS `restricted`,
0 AS `comments`,
0 AS `activities`
FROM `post-thread-user`
INNER JOIN `post-engagement` ON `post-engagement`.`uri-id` = `post-thread-user`.`uri-id`
INNER JOIN `post-user` ON `post-user`.`id` = `post-thread-user`.`post-user-id`
STRAIGHT_JOIN `contact` ON `contact`.`id` = `post-thread-user`.`contact-id`
STRAIGHT_JOIN `contact` AS `authorcontact` ON `authorcontact`.`id` = `post-thread-user`.`author-id`
STRAIGHT_JOIN `contact` AS `ownercontact` ON `ownercontact`.`id` = `post-thread-user`.`owner-id`
WHERE `post-user`.`visible` AND NOT `post-user`.`deleted`
AND (NOT `contact`.`readonly` AND NOT `contact`.`blocked` AND NOT `contact`.`pending`)
AND (`post-thread-user`.`hidden` IS NULL OR NOT `post-thread-user`.`hidden`)
AND NOT `authorcontact`.`blocked` AND NOT `ownercontact`.`blocked`
AND NOT EXISTS(SELECT `cid` FROM `user-contact` WHERE `uid` = `post-thread-user`.`uid` AND `cid` IN (`authorcontact`.`id`, `ownercontact`.`id`) AND (`blocked` OR `ignored`))
AND NOT EXISTS(SELECT `gsid` FROM `user-gserver` WHERE `uid` = `post-thread-user`.`uid` AND `gsid` IN (`authorcontact`.`gsid`, `ownercontact`.`gsid`) AND `ignored`);
-- --
-- VIEW post-timeline-view -- VIEW post-timeline-view
-- --
@ -2110,6 +2192,52 @@ CREATE VIEW `post-timeline-view` AS SELECT
STRAIGHT_JOIN `contact` AS `owner` ON `owner`.`id` = `post-user`.`owner-id` STRAIGHT_JOIN `contact` AS `owner` ON `owner`.`id` = `post-user`.`owner-id`
LEFT JOIN `contact` AS `causer` ON `causer`.`id` = `post-user`.`causer-id`; LEFT JOIN `contact` AS `causer` ON `causer`.`id` = `post-user`.`causer-id`;
--
-- VIEW post-timeline-origin-view
--
DROP VIEW IF EXISTS `post-timeline-origin-view`;
CREATE VIEW `post-timeline-origin-view` AS SELECT
`post-origin`.`uid` AS `uid`,
`post-origin`.`uri-id` AS `uri-id`,
`post-origin`.`gravity` AS `gravity`,
`post-origin`.`created` AS `created`,
`post-user`.`edited` AS `edited`,
`post-thread-user`.`commented` AS `commented`,
`post-origin`.`received` AS `received`,
`post-thread-user`.`changed` AS `changed`,
`post-origin`.`private` AS `private`,
`post-user`.`visible` AS `visible`,
`post-user`.`deleted` AS `deleted`,
true AS `origin`,
`post-user`.`global` AS `global`,
`post-user`.`network` AS `network`,
`post-user`.`protocol` AS `protocol`,
`post-origin`.`vid` AS `vid`,
`post-user`.`contact-id` AS `contact-id`,
`contact`.`blocked` AS `contact-blocked`,
`contact`.`readonly` AS `contact-readonly`,
`contact`.`pending` AS `contact-pending`,
`contact`.`rel` AS `contact-rel`,
`contact`.`uid` AS `contact-uid`,
`contact`.`self` AS `self`,
`post-user`.`author-id` AS `author-id`,
`author`.`blocked` AS `author-blocked`,
`author`.`hidden` AS `author-hidden`,
`author`.`gsid` AS `author-gsid`,
`post-user`.`owner-id` AS `owner-id`,
`owner`.`blocked` AS `owner-blocked`,
`owner`.`gsid` AS `owner-gsid`,
`post-user`.`causer-id` AS `causer-id`,
`causer`.`blocked` AS `causer-blocked`,
`causer`.`gsid` AS `causer-gsid`
FROM `post-origin`
INNER JOIN `post-user` ON `post-user`.`id` = `post-origin`.`id`
LEFT JOIN `post-thread-user` ON `post-thread-user`.`uri-id` = `post-origin`.`parent-uri-id` AND `post-thread-user`.`uid` = `post-origin`.`uid`
STRAIGHT_JOIN `contact` ON `contact`.`id` = `post-user`.`contact-id`
STRAIGHT_JOIN `contact` AS `author` ON `author`.`id` = `post-user`.`author-id`
STRAIGHT_JOIN `contact` AS `owner` ON `owner`.`id` = `post-user`.`owner-id`
LEFT JOIN `contact` AS `causer` ON `causer`.`id` = `post-user`.`causer-id`;
-- --
-- VIEW post-searchindex-user-view -- VIEW post-searchindex-user-view
-- --
@ -2142,6 +2270,392 @@ CREATE VIEW `post-searchindex-user-view` AS SELECT
AND NOT EXISTS(SELECT `cid` FROM `user-contact` WHERE `uid` = `post-thread-user`.`uid` AND `cid` IN (`authorcontact`.`id`, `ownercontact`.`id`) AND (`blocked` OR `ignored`)) AND NOT EXISTS(SELECT `cid` FROM `user-contact` WHERE `uid` = `post-thread-user`.`uid` AND `cid` IN (`authorcontact`.`id`, `ownercontact`.`id`) AND (`blocked` OR `ignored`))
AND NOT EXISTS(SELECT `gsid` FROM `user-gserver` WHERE `uid` = `post-thread-user`.`uid` AND `gsid` IN (`authorcontact`.`gsid`, `ownercontact`.`gsid`) AND `ignored`); AND NOT EXISTS(SELECT `gsid` FROM `user-gserver` WHERE `uid` = `post-thread-user`.`uid` AND `gsid` IN (`authorcontact`.`gsid`, `ownercontact`.`gsid`) AND `ignored`);
--
-- VIEW post-origin-view
--
DROP VIEW IF EXISTS `post-origin-view`;
CREATE VIEW `post-origin-view` AS SELECT
`post-origin`.`id` AS `id`,
`post-origin`.`id` AS `post-user-id`,
`post-origin`.`uid` AS `uid`,
`post-thread-user`.`post-user-id` AS `parent`,
`item-uri`.`uri` AS `uri`,
`post-origin`.`uri-id` AS `uri-id`,
`parent-item-uri`.`uri` AS `parent-uri`,
`post-origin`.`parent-uri-id` AS `parent-uri-id`,
`thr-parent-item-uri`.`uri` AS `thr-parent`,
`post-origin`.`thr-parent-id` AS `thr-parent-id`,
`conversation-item-uri`.`uri` AS `conversation`,
`post-thread-user`.`conversation-id` AS `conversation-id`,
`context-item-uri`.`uri` AS `context`,
`post-thread-user`.`context-id` AS `context-id`,
`quote-item-uri`.`uri` AS `quote-uri`,
`post-content`.`quote-uri-id` AS `quote-uri-id`,
`item-uri`.`guid` AS `guid`,
`post-origin`.`wall` AS `wall`,
`post-origin`.`gravity` AS `gravity`,
`external-item-uri`.`uri` AS `extid`,
`post-user`.`external-id` AS `external-id`,
`replies-item-uri`.`uri` AS `replies`,
`post-user`.`replies-id` AS `replies-id`,
`post-origin`.`created` AS `created`,
`post-user`.`edited` AS `edited`,
`post-thread-user`.`commented` AS `commented`,
`post-origin`.`received` AS `received`,
`post-thread-user`.`changed` AS `changed`,
`post-user`.`post-type` AS `post-type`,
`post-user`.`post-reason` AS `post-reason`,
`post-origin`.`private` AS `private`,
`post-thread-user`.`pubmail` AS `pubmail`,
`post-user`.`visible` AS `visible`,
`post-thread-user`.`starred` AS `starred`,
`post-user`.`unseen` AS `unseen`,
`post-user`.`deleted` AS `deleted`,
true AS `origin`,
`post-thread-user`.`origin` AS `parent-origin`,
`post-thread-user`.`mention` AS `mention`,
`post-user`.`global` AS `global`,
EXISTS(SELECT `type` FROM `post-collection` WHERE `type` = 0 AND `uri-id` = `post-origin`.`uri-id`) AS `featured`,
`post-user`.`network` AS `network`,
`post-user`.`protocol` AS `protocol`,
`post-origin`.`vid` AS `vid`,
`post-user`.`psid` AS `psid`,
IF (`post-origin`.`vid` IS NULL, '', `verb`.`name`) AS `verb`,
`post-content`.`title` AS `title`,
`post-content`.`content-warning` AS `content-warning`,
`post-content`.`raw-body` AS `raw-body`,
IFNULL (`post-content`.`body`, '') AS `body`,
`post-content`.`rendered-hash` AS `rendered-hash`,
`post-content`.`rendered-html` AS `rendered-html`,
`post-content`.`language` AS `language`,
`post-content`.`plink` AS `plink`,
`post-content`.`location` AS `location`,
`post-content`.`coord` AS `coord`,
`post-content`.`sensitive` AS `sensitive`,
`post-user`.`restrictions` AS `restrictions`,
`post-content`.`app` AS `app`,
`post-content`.`object-type` AS `object-type`,
`post-content`.`object` AS `object`,
`post-content`.`target-type` AS `target-type`,
`post-content`.`target` AS `target`,
`post-content`.`resource-id` AS `resource-id`,
`post-user`.`contact-id` AS `contact-id`,
`contact`.`uri-id` AS `contact-uri-id`,
`contact`.`url` AS `contact-link`,
`contact`.`addr` AS `contact-addr`,
`contact`.`name` AS `contact-name`,
`contact`.`nick` AS `contact-nick`,
`contact`.`thumb` AS `contact-avatar`,
`contact`.`network` AS `contact-network`,
`contact`.`blocked` AS `contact-blocked`,
`contact`.`hidden` AS `contact-hidden`,
`contact`.`readonly` AS `contact-readonly`,
`contact`.`archive` AS `contact-archive`,
`contact`.`pending` AS `contact-pending`,
`contact`.`rel` AS `contact-rel`,
`contact`.`uid` AS `contact-uid`,
`contact`.`contact-type` AS `contact-contact-type`,
IF (`post-user`.`network` IN ('apub', 'dfrn', 'dspr', 'stat'), true, `contact`.`writable`) AS `writable`,
`contact`.`self` AS `self`,
`contact`.`id` AS `cid`,
`contact`.`alias` AS `alias`,
`contact`.`photo` AS `photo`,
`contact`.`name-date` AS `name-date`,
`contact`.`uri-date` AS `uri-date`,
`contact`.`avatar-date` AS `avatar-date`,
`contact`.`thumb` AS `thumb`,
`post-user`.`author-id` AS `author-id`,
`author`.`uri-id` AS `author-uri-id`,
`author`.`url` AS `author-link`,
`author`.`addr` AS `author-addr`,
IF (`contact`.`url` = `author`.`url` AND `contact`.`name` != '', `contact`.`name`, `author`.`name`) AS `author-name`,
`author`.`nick` AS `author-nick`,
`author`.`alias` AS `author-alias`,
IF (`contact`.`url` = `author`.`url` AND `contact`.`thumb` != '', `contact`.`thumb`, `author`.`thumb`) AS `author-avatar`,
`author`.`network` AS `author-network`,
`author`.`blocked` AS `author-blocked`,
`author`.`hidden` AS `author-hidden`,
`author`.`updated` AS `author-updated`,
`author`.`contact-type` AS `author-contact-type`,
`author`.`gsid` AS `author-gsid`,
`author`.`baseurl` AS `author-baseurl`,
`post-user`.`owner-id` AS `owner-id`,
`owner`.`uri-id` AS `owner-uri-id`,
`owner`.`url` AS `owner-link`,
`owner`.`addr` AS `owner-addr`,
IF (`contact`.`url` = `owner`.`url` AND `contact`.`name` != '', `contact`.`name`, `owner`.`name`) AS `owner-name`,
`owner`.`nick` AS `owner-nick`,
`owner`.`alias` AS `owner-alias`,
IF (`contact`.`url` = `owner`.`url` AND `contact`.`thumb` != '', `contact`.`thumb`, `owner`.`thumb`) AS `owner-avatar`,
`owner`.`network` AS `owner-network`,
`owner`.`blocked` AS `owner-blocked`,
`owner`.`hidden` AS `owner-hidden`,
`owner`.`updated` AS `owner-updated`,
`owner`.`gsid` AS `owner-gsid`,
`owner`.`contact-type` AS `owner-contact-type`,
`post-user`.`causer-id` AS `causer-id`,
`causer`.`uri-id` AS `causer-uri-id`,
`causer`.`url` AS `causer-link`,
`causer`.`addr` AS `causer-addr`,
`causer`.`name` AS `causer-name`,
`causer`.`nick` AS `causer-nick`,
`causer`.`alias` AS `causer-alias`,
`causer`.`thumb` AS `causer-avatar`,
`causer`.`network` AS `causer-network`,
`causer`.`blocked` AS `causer-blocked`,
`causer`.`hidden` AS `causer-hidden`,
`causer`.`gsid` AS `causer-gsid`,
`causer`.`contact-type` AS `causer-contact-type`,
`post-delivery-data`.`postopts` AS `postopts`,
`post-delivery-data`.`inform` AS `inform`,
`post-delivery-data`.`queue_count` AS `delivery_queue_count`,
`post-delivery-data`.`queue_done` AS `delivery_queue_done`,
`post-delivery-data`.`queue_failed` AS `delivery_queue_failed`,
IF (`post-user`.`psid` IS NULL, '', `permissionset`.`allow_cid`) AS `allow_cid`,
IF (`post-user`.`psid` IS NULL, '', `permissionset`.`allow_gid`) AS `allow_gid`,
IF (`post-user`.`psid` IS NULL, '', `permissionset`.`deny_cid`) AS `deny_cid`,
IF (`post-user`.`psid` IS NULL, '', `permissionset`.`deny_gid`) AS `deny_gid`,
`post-user`.`event-id` AS `event-id`,
`event`.`created` AS `event-created`,
`event`.`edited` AS `event-edited`,
`event`.`start` AS `event-start`,
`event`.`finish` AS `event-finish`,
`event`.`summary` AS `event-summary`,
`event`.`desc` AS `event-desc`,
`event`.`location` AS `event-location`,
`event`.`type` AS `event-type`,
`event`.`nofinish` AS `event-nofinish`,
`event`.`ignore` AS `event-ignore`,
`post-question`.`id` AS `question-id`,
`post-question`.`multiple` AS `question-multiple`,
`post-question`.`voters` AS `question-voters`,
`post-question`.`end-time` AS `question-end-time`,
EXISTS(SELECT `uri-id` FROM `post-category` WHERE `post-category`.`uri-id` = `post-origin`.`uri-id` AND `post-category`.`uid` = `post-origin`.`uid`) AS `has-categories`,
EXISTS(SELECT `id` FROM `post-media` WHERE `post-media`.`uri-id` = `post-origin`.`uri-id`) AS `has-media`,
`diaspora-interaction`.`interaction` AS `signed_text`,
`parent-item-uri`.`guid` AS `parent-guid`,
`post-thread-user`.`network` AS `parent-network`,
`post-thread-user`.`author-id` AS `parent-author-id`,
`parent-post-author`.`url` AS `parent-author-link`,
`parent-post-author`.`name` AS `parent-author-name`,
`parent-post-author`.`nick` AS `parent-author-nick`,
`parent-post-author`.`network` AS `parent-author-network`
FROM `post-origin`
INNER JOIN `post-user` ON `post-user`.`id` = `post-origin`.`id`
INNER JOIN `post-thread-user` ON `post-thread-user`.`uri-id` = `post-origin`.`parent-uri-id` AND `post-thread-user`.`uid` = `post-origin`.`uid`
STRAIGHT_JOIN `contact` ON `contact`.`id` = `post-user`.`contact-id`
STRAIGHT_JOIN `contact` AS `author` ON `author`.`id` = `post-user`.`author-id`
STRAIGHT_JOIN `contact` AS `owner` ON `owner`.`id` = `post-user`.`owner-id`
LEFT JOIN `contact` AS `causer` ON `causer`.`id` = `post-user`.`causer-id`
LEFT JOIN `item-uri` ON `item-uri`.`id` = `post-origin`.`uri-id`
LEFT JOIN `item-uri` AS `thr-parent-item-uri` ON `thr-parent-item-uri`.`id` = `post-origin`.`thr-parent-id`
LEFT JOIN `item-uri` AS `parent-item-uri` ON `parent-item-uri`.`id` = `post-origin`.`parent-uri-id`
LEFT JOIN `item-uri` AS `conversation-item-uri` ON `conversation-item-uri`.`id` = `post-thread-user`.`conversation-id`
LEFT JOIN `item-uri` AS `context-item-uri` ON `context-item-uri`.`id` = `post-thread-user`.`context-id`
LEFT JOIN `item-uri` AS `external-item-uri` ON `external-item-uri`.`id` = `post-user`.`external-id`
LEFT JOIN `item-uri` AS `replies-item-uri` ON `replies-item-uri`.`id` = `post-user`.`replies-id`
LEFT JOIN `verb` ON `verb`.`id` = `post-origin`.`vid`
LEFT JOIN `event` ON `event`.`id` = `post-user`.`event-id`
LEFT JOIN `diaspora-interaction` ON `diaspora-interaction`.`uri-id` = `post-origin`.`uri-id`
LEFT JOIN `post-content` ON `post-content`.`uri-id` = `post-origin`.`uri-id`
LEFT JOIN `item-uri` AS `quote-item-uri` ON `quote-item-uri`.`id` = `post-content`.`quote-uri-id`
LEFT JOIN `post-delivery-data` ON `post-delivery-data`.`uri-id` = `post-origin`.`uri-id`
LEFT JOIN `post-question` ON `post-question`.`uri-id` = `post-origin`.`uri-id`
LEFT JOIN `permissionset` ON `permissionset`.`id` = `post-user`.`psid`
LEFT JOIN `contact` AS `parent-post-author` ON `parent-post-author`.`id` = `post-thread-user`.`author-id`;
--
-- VIEW post-thread-origin-view
--
DROP VIEW IF EXISTS `post-thread-origin-view`;
CREATE VIEW `post-thread-origin-view` AS SELECT
`post-origin`.`id` AS `id`,
`post-origin`.`id` AS `post-user-id`,
`post-origin`.`uid` AS `uid`,
`post-thread-user`.`post-user-id` AS `parent`,
`item-uri`.`uri` AS `uri`,
`post-origin`.`uri-id` AS `uri-id`,
`parent-item-uri`.`uri` AS `parent-uri`,
`post-origin`.`parent-uri-id` AS `parent-uri-id`,
`thr-parent-item-uri`.`uri` AS `thr-parent`,
`post-origin`.`thr-parent-id` AS `thr-parent-id`,
`conversation-item-uri`.`uri` AS `conversation`,
`post-thread-user`.`conversation-id` AS `conversation-id`,
`context-item-uri`.`uri` AS `context`,
`post-thread-user`.`context-id` AS `context-id`,
`quote-item-uri`.`uri` AS `quote-uri`,
`post-content`.`quote-uri-id` AS `quote-uri-id`,
`item-uri`.`guid` AS `guid`,
`post-origin`.`wall` AS `wall`,
`post-origin`.`gravity` AS `gravity`,
`external-item-uri`.`uri` AS `extid`,
`post-user`.`external-id` AS `external-id`,
`replies-item-uri`.`uri` AS `replies`,
`post-user`.`replies-id` AS `replies-id`,
`post-origin`.`created` AS `created`,
`post-user`.`edited` AS `edited`,
`post-thread-user`.`commented` AS `commented`,
`post-origin`.`received` AS `received`,
`post-thread-user`.`changed` AS `changed`,
`post-user`.`post-type` AS `post-type`,
`post-user`.`post-reason` AS `post-reason`,
`post-origin`.`private` AS `private`,
`post-thread-user`.`pubmail` AS `pubmail`,
`post-thread-user`.`ignored` AS `ignored`,
`post-user`.`visible` AS `visible`,
`post-thread-user`.`starred` AS `starred`,
`post-thread-user`.`unseen` AS `unseen`,
`post-user`.`deleted` AS `deleted`,
true AS `origin`,
`post-thread-user`.`mention` AS `mention`,
`post-user`.`global` AS `global`,
EXISTS(SELECT `type` FROM `post-collection` WHERE `type` = 0 AND `uri-id` = `post-thread-user`.`uri-id`) AS `featured`,
`post-thread-user`.`network` AS `network`,
`post-origin`.`vid` AS `vid`,
`post-thread-user`.`psid` AS `psid`,
IF (`post-origin`.`vid` IS NULL, '', `verb`.`name`) AS `verb`,
`post-content`.`title` AS `title`,
`post-content`.`content-warning` AS `content-warning`,
`post-content`.`raw-body` AS `raw-body`,
`post-content`.`body` AS `body`,
`post-content`.`rendered-hash` AS `rendered-hash`,
`post-content`.`rendered-html` AS `rendered-html`,
`post-content`.`language` AS `language`,
`post-content`.`plink` AS `plink`,
`post-content`.`location` AS `location`,
`post-content`.`coord` AS `coord`,
`post-content`.`sensitive` AS `sensitive`,
`post-user`.`restrictions` AS `restrictions`,
`post-content`.`app` AS `app`,
`post-content`.`object-type` AS `object-type`,
`post-content`.`object` AS `object`,
`post-content`.`target-type` AS `target-type`,
`post-content`.`target` AS `target`,
`post-content`.`resource-id` AS `resource-id`,
`post-thread-user`.`contact-id` AS `contact-id`,
`contact`.`uri-id` AS `contact-uri-id`,
`contact`.`url` AS `contact-link`,
`contact`.`addr` AS `contact-addr`,
`contact`.`name` AS `contact-name`,
`contact`.`nick` AS `contact-nick`,
`contact`.`thumb` AS `contact-avatar`,
`contact`.`network` AS `contact-network`,
`contact`.`blocked` AS `contact-blocked`,
`contact`.`hidden` AS `contact-hidden`,
`contact`.`readonly` AS `contact-readonly`,
`contact`.`archive` AS `contact-archive`,
`contact`.`pending` AS `contact-pending`,
`contact`.`rel` AS `contact-rel`,
`contact`.`uid` AS `contact-uid`,
`contact`.`gsid` AS `contact-gsid`,
`contact`.`contact-type` AS `contact-contact-type`,
IF (`post-user`.`network` IN ('apub', 'dfrn', 'dspr', 'stat'), true, `contact`.`writable`) AS `writable`,
`contact`.`self` AS `self`,
`contact`.`id` AS `cid`,
`contact`.`alias` AS `alias`,
`contact`.`photo` AS `photo`,
`contact`.`name-date` AS `name-date`,
`contact`.`uri-date` AS `uri-date`,
`contact`.`avatar-date` AS `avatar-date`,
`contact`.`thumb` AS `thumb`,
`post-thread-user`.`author-id` AS `author-id`,
`author`.`uri-id` AS `author-uri-id`,
`author`.`url` AS `author-link`,
`author`.`addr` AS `author-addr`,
IF (`contact`.`url` = `author`.`url` AND `contact`.`name` != '', `contact`.`name`, `author`.`name`) AS `author-name`,
`author`.`nick` AS `author-nick`,
`author`.`alias` AS `author-alias`,
IF (`contact`.`url` = `author`.`url` AND `contact`.`thumb` != '', `contact`.`thumb`, `author`.`thumb`) AS `author-avatar`,
`author`.`network` AS `author-network`,
`author`.`blocked` AS `author-blocked`,
`author`.`hidden` AS `author-hidden`,
`author`.`updated` AS `author-updated`,
`author`.`contact-type` AS `author-contact-type`,
`author`.`gsid` AS `author-gsid`,
`post-thread-user`.`owner-id` AS `owner-id`,
`owner`.`uri-id` AS `owner-uri-id`,
`owner`.`url` AS `owner-link`,
`owner`.`addr` AS `owner-addr`,
IF (`contact`.`url` = `owner`.`url` AND `contact`.`name` != '', `contact`.`name`, `owner`.`name`) AS `owner-name`,
`owner`.`nick` AS `owner-nick`,
`owner`.`alias` AS `owner-alias`,
IF (`contact`.`url` = `owner`.`url` AND `contact`.`thumb` != '', `contact`.`thumb`, `owner`.`thumb`) AS `owner-avatar`,
`owner`.`network` AS `owner-network`,
`owner`.`blocked` AS `owner-blocked`,
`owner`.`hidden` AS `owner-hidden`,
`owner`.`updated` AS `owner-updated`,
`owner`.`gsid` AS `owner-gsid`,
`owner`.`contact-type` AS `owner-contact-type`,
`post-thread-user`.`causer-id` AS `causer-id`,
`causer`.`uri-id` AS `causer-uri-id`,
`causer`.`url` AS `causer-link`,
`causer`.`addr` AS `causer-addr`,
`causer`.`name` AS `causer-name`,
`causer`.`nick` AS `causer-nick`,
`causer`.`alias` AS `causer-alias`,
`causer`.`thumb` AS `causer-avatar`,
`causer`.`network` AS `causer-network`,
`causer`.`blocked` AS `causer-blocked`,
`causer`.`hidden` AS `causer-hidden`,
`causer`.`gsid` AS `causer-gsid`,
`causer`.`contact-type` AS `causer-contact-type`,
`post-delivery-data`.`postopts` AS `postopts`,
`post-delivery-data`.`inform` AS `inform`,
`post-delivery-data`.`queue_count` AS `delivery_queue_count`,
`post-delivery-data`.`queue_done` AS `delivery_queue_done`,
`post-delivery-data`.`queue_failed` AS `delivery_queue_failed`,
IF (`post-thread-user`.`psid` IS NULL, '', `permissionset`.`allow_cid`) AS `allow_cid`,
IF (`post-thread-user`.`psid` IS NULL, '', `permissionset`.`allow_gid`) AS `allow_gid`,
IF (`post-thread-user`.`psid` IS NULL, '', `permissionset`.`deny_cid`) AS `deny_cid`,
IF (`post-thread-user`.`psid` IS NULL, '', `permissionset`.`deny_gid`) AS `deny_gid`,
`post-user`.`event-id` AS `event-id`,
`event`.`created` AS `event-created`,
`event`.`edited` AS `event-edited`,
`event`.`start` AS `event-start`,
`event`.`finish` AS `event-finish`,
`event`.`summary` AS `event-summary`,
`event`.`desc` AS `event-desc`,
`event`.`location` AS `event-location`,
`event`.`type` AS `event-type`,
`event`.`nofinish` AS `event-nofinish`,
`event`.`ignore` AS `event-ignore`,
`post-question`.`id` AS `question-id`,
`post-question`.`multiple` AS `question-multiple`,
`post-question`.`voters` AS `question-voters`,
`post-question`.`end-time` AS `question-end-time`,
EXISTS(SELECT `uri-id` FROM `post-category` WHERE `post-category`.`uri-id` = `post-thread-user`.`uri-id` AND `post-category`.`uid` = `post-thread-user`.`uid`) AS `has-categories`,
EXISTS(SELECT `id` FROM `post-media` WHERE `post-media`.`uri-id` = `post-thread-user`.`uri-id`) AS `has-media`,
`diaspora-interaction`.`interaction` AS `signed_text`,
`parent-item-uri`.`guid` AS `parent-guid`,
`post-thread-user`.`network` AS `parent-network`,
`post-thread-user`.`author-id` AS `parent-author-id`,
`author`.`url` AS `parent-author-link`,
`author`.`name` AS `parent-author-name`,
`author`.`nick` AS `parent-author-nick`,
`author`.`network` AS `parent-author-network`
FROM `post-origin`
INNER JOIN `post-thread-user` ON `post-thread-user`.`uri-id` = `post-origin`.`uri-id` AND `post-thread-user`.`uid` = `post-origin`.`uid`
INNER JOIN `post-user` ON `post-user`.`id` = `post-origin`.`id`
STRAIGHT_JOIN `contact` ON `contact`.`id` = `post-thread-user`.`contact-id`
STRAIGHT_JOIN `contact` AS `author` ON `author`.`id` = `post-thread-user`.`author-id`
STRAIGHT_JOIN `contact` AS `owner` ON `owner`.`id` = `post-thread-user`.`owner-id`
LEFT JOIN `contact` AS `causer` ON `causer`.`id` = `post-thread-user`.`causer-id`
LEFT JOIN `item-uri` ON `item-uri`.`id` = `post-origin`.`uri-id`
LEFT JOIN `item-uri` AS `thr-parent-item-uri` ON `thr-parent-item-uri`.`id` = `post-origin`.`thr-parent-id`
LEFT JOIN `item-uri` AS `parent-item-uri` ON `parent-item-uri`.`id` = `post-origin`.`parent-uri-id`
LEFT JOIN `item-uri` AS `conversation-item-uri` ON `conversation-item-uri`.`id` = `post-thread-user`.`conversation-id`
LEFT JOIN `item-uri` AS `context-item-uri` ON `context-item-uri`.`id` = `post-thread-user`.`context-id`
LEFT JOIN `item-uri` AS `external-item-uri` ON `external-item-uri`.`id` = `post-user`.`external-id`
LEFT JOIN `item-uri` AS `replies-item-uri` ON `replies-item-uri`.`id` = `post-user`.`replies-id`
LEFT JOIN `verb` ON `verb`.`id` = `post-origin`.`vid`
LEFT JOIN `event` ON `event`.`id` = `post-user`.`event-id`
LEFT JOIN `diaspora-interaction` ON `diaspora-interaction`.`uri-id` = `post-origin`.`uri-id`
LEFT JOIN `post-content` ON `post-content`.`uri-id` = `post-origin`.`uri-id`
LEFT JOIN `item-uri` AS `quote-item-uri` ON `quote-item-uri`.`id` = `post-content`.`quote-uri-id`
LEFT JOIN `post-delivery-data` ON `post-delivery-data`.`uri-id` = `post-origin`.`uri-id`
LEFT JOIN `post-question` ON `post-question`.`uri-id` = `post-origin`.`uri-id`
LEFT JOIN `permissionset` ON `permissionset`.`id` = `post-thread-user`.`psid`;
-- --
-- VIEW post-user-view -- VIEW post-user-view
-- --
@ -2159,6 +2673,8 @@ CREATE VIEW `post-user-view` AS SELECT
`post-user`.`thr-parent-id` AS `thr-parent-id`, `post-user`.`thr-parent-id` AS `thr-parent-id`,
`conversation-item-uri`.`uri` AS `conversation`, `conversation-item-uri`.`uri` AS `conversation`,
`post-thread-user`.`conversation-id` AS `conversation-id`, `post-thread-user`.`conversation-id` AS `conversation-id`,
`context-item-uri`.`uri` AS `context`,
`post-thread-user`.`context-id` AS `context-id`,
`quote-item-uri`.`uri` AS `quote-uri`, `quote-item-uri`.`uri` AS `quote-uri`,
`post-content`.`quote-uri-id` AS `quote-uri-id`, `post-content`.`quote-uri-id` AS `quote-uri-id`,
`item-uri`.`guid` AS `guid`, `item-uri`.`guid` AS `guid`,
@ -2166,6 +2682,8 @@ CREATE VIEW `post-user-view` AS SELECT
`post-user`.`gravity` AS `gravity`, `post-user`.`gravity` AS `gravity`,
`external-item-uri`.`uri` AS `extid`, `external-item-uri`.`uri` AS `extid`,
`post-user`.`external-id` AS `external-id`, `post-user`.`external-id` AS `external-id`,
`replies-item-uri`.`uri` AS `replies`,
`post-user`.`replies-id` AS `replies-id`,
`post-user`.`created` AS `created`, `post-user`.`created` AS `created`,
`post-user`.`edited` AS `edited`, `post-user`.`edited` AS `edited`,
`post-thread-user`.`commented` AS `commented`, `post-thread-user`.`commented` AS `commented`,
@ -2200,6 +2718,7 @@ CREATE VIEW `post-user-view` AS SELECT
`post-content`.`location` AS `location`, `post-content`.`location` AS `location`,
`post-content`.`coord` AS `coord`, `post-content`.`coord` AS `coord`,
`post-content`.`sensitive` AS `sensitive`, `post-content`.`sensitive` AS `sensitive`,
`post-user`.`restrictions` AS `restrictions`,
`post-content`.`app` AS `app`, `post-content`.`app` AS `app`,
`post-content`.`object-type` AS `object-type`, `post-content`.`object-type` AS `object-type`,
`post-content`.`object` AS `object`, `post-content`.`object` AS `object`,
@ -2317,7 +2836,9 @@ CREATE VIEW `post-user-view` AS SELECT
LEFT JOIN `item-uri` AS `thr-parent-item-uri` ON `thr-parent-item-uri`.`id` = `post-user`.`thr-parent-id` LEFT JOIN `item-uri` AS `thr-parent-item-uri` ON `thr-parent-item-uri`.`id` = `post-user`.`thr-parent-id`
LEFT JOIN `item-uri` AS `parent-item-uri` ON `parent-item-uri`.`id` = `post-user`.`parent-uri-id` LEFT JOIN `item-uri` AS `parent-item-uri` ON `parent-item-uri`.`id` = `post-user`.`parent-uri-id`
LEFT JOIN `item-uri` AS `conversation-item-uri` ON `conversation-item-uri`.`id` = `post-thread-user`.`conversation-id` LEFT JOIN `item-uri` AS `conversation-item-uri` ON `conversation-item-uri`.`id` = `post-thread-user`.`conversation-id`
LEFT JOIN `item-uri` AS `context-item-uri` ON `context-item-uri`.`id` = `post-thread-user`.`context-id`
LEFT JOIN `item-uri` AS `external-item-uri` ON `external-item-uri`.`id` = `post-user`.`external-id` LEFT JOIN `item-uri` AS `external-item-uri` ON `external-item-uri`.`id` = `post-user`.`external-id`
LEFT JOIN `item-uri` AS `replies-item-uri` ON `replies-item-uri`.`id` = `post-user`.`replies-id`
LEFT JOIN `verb` ON `verb`.`id` = `post-user`.`vid` LEFT JOIN `verb` ON `verb`.`id` = `post-user`.`vid`
LEFT JOIN `event` ON `event`.`id` = `post-user`.`event-id` LEFT JOIN `event` ON `event`.`id` = `post-user`.`event-id`
LEFT JOIN `diaspora-interaction` ON `diaspora-interaction`.`uri-id` = `post-user`.`uri-id` LEFT JOIN `diaspora-interaction` ON `diaspora-interaction`.`uri-id` = `post-user`.`uri-id`
@ -2345,6 +2866,8 @@ CREATE VIEW `post-thread-user-view` AS SELECT
`post-user`.`thr-parent-id` AS `thr-parent-id`, `post-user`.`thr-parent-id` AS `thr-parent-id`,
`conversation-item-uri`.`uri` AS `conversation`, `conversation-item-uri`.`uri` AS `conversation`,
`post-thread-user`.`conversation-id` AS `conversation-id`, `post-thread-user`.`conversation-id` AS `conversation-id`,
`context-item-uri`.`uri` AS `context`,
`post-thread-user`.`context-id` AS `context-id`,
`quote-item-uri`.`uri` AS `quote-uri`, `quote-item-uri`.`uri` AS `quote-uri`,
`post-content`.`quote-uri-id` AS `quote-uri-id`, `post-content`.`quote-uri-id` AS `quote-uri-id`,
`item-uri`.`guid` AS `guid`, `item-uri`.`guid` AS `guid`,
@ -2352,6 +2875,8 @@ CREATE VIEW `post-thread-user-view` AS SELECT
`post-user`.`gravity` AS `gravity`, `post-user`.`gravity` AS `gravity`,
`external-item-uri`.`uri` AS `extid`, `external-item-uri`.`uri` AS `extid`,
`post-user`.`external-id` AS `external-id`, `post-user`.`external-id` AS `external-id`,
`replies-item-uri`.`uri` AS `replies`,
`post-user`.`replies-id` AS `replies-id`,
`post-thread-user`.`created` AS `created`, `post-thread-user`.`created` AS `created`,
`post-user`.`edited` AS `edited`, `post-user`.`edited` AS `edited`,
`post-thread-user`.`commented` AS `commented`, `post-thread-user`.`commented` AS `commented`,
@ -2385,6 +2910,7 @@ CREATE VIEW `post-thread-user-view` AS SELECT
`post-content`.`location` AS `location`, `post-content`.`location` AS `location`,
`post-content`.`coord` AS `coord`, `post-content`.`coord` AS `coord`,
`post-content`.`sensitive` AS `sensitive`, `post-content`.`sensitive` AS `sensitive`,
`post-user`.`restrictions` AS `restrictions`,
`post-content`.`app` AS `app`, `post-content`.`app` AS `app`,
`post-content`.`object-type` AS `object-type`, `post-content`.`object-type` AS `object-type`,
`post-content`.`object` AS `object`, `post-content`.`object` AS `object`,
@ -2502,7 +3028,9 @@ CREATE VIEW `post-thread-user-view` AS SELECT
LEFT JOIN `item-uri` AS `thr-parent-item-uri` ON `thr-parent-item-uri`.`id` = `post-user`.`thr-parent-id` LEFT JOIN `item-uri` AS `thr-parent-item-uri` ON `thr-parent-item-uri`.`id` = `post-user`.`thr-parent-id`
LEFT JOIN `item-uri` AS `parent-item-uri` ON `parent-item-uri`.`id` = `post-user`.`parent-uri-id` LEFT JOIN `item-uri` AS `parent-item-uri` ON `parent-item-uri`.`id` = `post-user`.`parent-uri-id`
LEFT JOIN `item-uri` AS `conversation-item-uri` ON `conversation-item-uri`.`id` = `post-thread-user`.`conversation-id` LEFT JOIN `item-uri` AS `conversation-item-uri` ON `conversation-item-uri`.`id` = `post-thread-user`.`conversation-id`
LEFT JOIN `item-uri` AS `context-item-uri` ON `context-item-uri`.`id` = `post-thread-user`.`context-id`
LEFT JOIN `item-uri` AS `external-item-uri` ON `external-item-uri`.`id` = `post-user`.`external-id` LEFT JOIN `item-uri` AS `external-item-uri` ON `external-item-uri`.`id` = `post-user`.`external-id`
LEFT JOIN `item-uri` AS `replies-item-uri` ON `replies-item-uri`.`id` = `post-user`.`replies-id`
LEFT JOIN `verb` ON `verb`.`id` = `post-user`.`vid` LEFT JOIN `verb` ON `verb`.`id` = `post-user`.`vid`
LEFT JOIN `event` ON `event`.`id` = `post-user`.`event-id` LEFT JOIN `event` ON `event`.`id` = `post-user`.`event-id`
LEFT JOIN `diaspora-interaction` ON `diaspora-interaction`.`uri-id` = `post-thread-user`.`uri-id` LEFT JOIN `diaspora-interaction` ON `diaspora-interaction`.`uri-id` = `post-thread-user`.`uri-id`
@ -2525,12 +3053,16 @@ CREATE VIEW `post-view` AS SELECT
`post`.`thr-parent-id` AS `thr-parent-id`, `post`.`thr-parent-id` AS `thr-parent-id`,
`conversation-item-uri`.`uri` AS `conversation`, `conversation-item-uri`.`uri` AS `conversation`,
`post-thread`.`conversation-id` AS `conversation-id`, `post-thread`.`conversation-id` AS `conversation-id`,
`context-item-uri`.`uri` AS `context`,
`post-thread`.`context-id` AS `context-id`,
`quote-item-uri`.`uri` AS `quote-uri`, `quote-item-uri`.`uri` AS `quote-uri`,
`post-content`.`quote-uri-id` AS `quote-uri-id`, `post-content`.`quote-uri-id` AS `quote-uri-id`,
`item-uri`.`guid` AS `guid`, `item-uri`.`guid` AS `guid`,
`post`.`gravity` AS `gravity`, `post`.`gravity` AS `gravity`,
`external-item-uri`.`uri` AS `extid`, `external-item-uri`.`uri` AS `extid`,
`post`.`external-id` AS `external-id`, `post`.`external-id` AS `external-id`,
`replies-item-uri`.`uri` AS `replies`,
`post`.`replies-id` AS `replies-id`,
`post`.`created` AS `created`, `post`.`created` AS `created`,
`post`.`edited` AS `edited`, `post`.`edited` AS `edited`,
`post-thread`.`commented` AS `commented`, `post-thread`.`commented` AS `commented`,
@ -2651,7 +3183,9 @@ CREATE VIEW `post-view` AS SELECT
LEFT JOIN `item-uri` AS `thr-parent-item-uri` ON `thr-parent-item-uri`.`id` = `post`.`thr-parent-id` LEFT JOIN `item-uri` AS `thr-parent-item-uri` ON `thr-parent-item-uri`.`id` = `post`.`thr-parent-id`
LEFT JOIN `item-uri` AS `parent-item-uri` ON `parent-item-uri`.`id` = `post`.`parent-uri-id` LEFT JOIN `item-uri` AS `parent-item-uri` ON `parent-item-uri`.`id` = `post`.`parent-uri-id`
LEFT JOIN `item-uri` AS `conversation-item-uri` ON `conversation-item-uri`.`id` = `post-thread`.`conversation-id` LEFT JOIN `item-uri` AS `conversation-item-uri` ON `conversation-item-uri`.`id` = `post-thread`.`conversation-id`
LEFT JOIN `item-uri` AS `context-item-uri` ON `context-item-uri`.`id` = `post-thread`.`context-id`
LEFT JOIN `item-uri` AS `external-item-uri` ON `external-item-uri`.`id` = `post`.`external-id` LEFT JOIN `item-uri` AS `external-item-uri` ON `external-item-uri`.`id` = `post`.`external-id`
LEFT JOIN `item-uri` AS `replies-item-uri` ON `replies-item-uri`.`id` = `post`.`replies-id`
LEFT JOIN `verb` ON `verb`.`id` = `post`.`vid` LEFT JOIN `verb` ON `verb`.`id` = `post`.`vid`
LEFT JOIN `diaspora-interaction` ON `diaspora-interaction`.`uri-id` = `post`.`uri-id` LEFT JOIN `diaspora-interaction` ON `diaspora-interaction`.`uri-id` = `post`.`uri-id`
LEFT JOIN `post-content` ON `post-content`.`uri-id` = `post`.`uri-id` LEFT JOIN `post-content` ON `post-content`.`uri-id` = `post`.`uri-id`
@ -2672,12 +3206,16 @@ CREATE VIEW `post-thread-view` AS SELECT
`post`.`thr-parent-id` AS `thr-parent-id`, `post`.`thr-parent-id` AS `thr-parent-id`,
`conversation-item-uri`.`uri` AS `conversation`, `conversation-item-uri`.`uri` AS `conversation`,
`post-thread`.`conversation-id` AS `conversation-id`, `post-thread`.`conversation-id` AS `conversation-id`,
`context-item-uri`.`uri` AS `context`,
`post-thread`.`context-id` AS `context-id`,
`quote-item-uri`.`uri` AS `quote-uri`, `quote-item-uri`.`uri` AS `quote-uri`,
`post-content`.`quote-uri-id` AS `quote-uri-id`, `post-content`.`quote-uri-id` AS `quote-uri-id`,
`item-uri`.`guid` AS `guid`, `item-uri`.`guid` AS `guid`,
`post`.`gravity` AS `gravity`, `post`.`gravity` AS `gravity`,
`external-item-uri`.`uri` AS `extid`, `external-item-uri`.`uri` AS `extid`,
`post`.`external-id` AS `external-id`, `post`.`external-id` AS `external-id`,
`replies-item-uri`.`uri` AS `replies`,
`post`.`replies-id` AS `replies-id`,
`post-thread`.`created` AS `created`, `post-thread`.`created` AS `created`,
`post`.`edited` AS `edited`, `post`.`edited` AS `edited`,
`post-thread`.`commented` AS `commented`, `post-thread`.`commented` AS `commented`,
@ -2800,7 +3338,9 @@ CREATE VIEW `post-thread-view` AS SELECT
LEFT JOIN `item-uri` AS `thr-parent-item-uri` ON `thr-parent-item-uri`.`id` = `post`.`thr-parent-id` LEFT JOIN `item-uri` AS `thr-parent-item-uri` ON `thr-parent-item-uri`.`id` = `post`.`thr-parent-id`
LEFT JOIN `item-uri` AS `parent-item-uri` ON `parent-item-uri`.`id` = `post`.`parent-uri-id` LEFT JOIN `item-uri` AS `parent-item-uri` ON `parent-item-uri`.`id` = `post`.`parent-uri-id`
LEFT JOIN `item-uri` AS `conversation-item-uri` ON `conversation-item-uri`.`id` = `post-thread`.`conversation-id` LEFT JOIN `item-uri` AS `conversation-item-uri` ON `conversation-item-uri`.`id` = `post-thread`.`conversation-id`
LEFT JOIN `item-uri` AS `context-item-uri` ON `context-item-uri`.`id` = `post-thread`.`context-id`
LEFT JOIN `item-uri` AS `external-item-uri` ON `external-item-uri`.`id` = `post`.`external-id` LEFT JOIN `item-uri` AS `external-item-uri` ON `external-item-uri`.`id` = `post`.`external-id`
LEFT JOIN `item-uri` AS `replies-item-uri` ON `replies-item-uri`.`id` = `post`.`replies-id`
LEFT JOIN `verb` ON `verb`.`id` = `post`.`vid` LEFT JOIN `verb` ON `verb`.`id` = `post`.`vid`
LEFT JOIN `diaspora-interaction` ON `diaspora-interaction`.`uri-id` = `post-thread`.`uri-id` LEFT JOIN `diaspora-interaction` ON `diaspora-interaction`.`uri-id` = `post-thread`.`uri-id`
LEFT JOIN `post-content` ON `post-content`.`uri-id` = `post-thread`.`uri-id` LEFT JOIN `post-content` ON `post-content`.`uri-id` = `post-thread`.`uri-id`
@ -2876,36 +3416,6 @@ CREATE VIEW `tag-view` AS SELECT
LEFT JOIN `tag` ON `post-tag`.`tid` = `tag`.`id` LEFT JOIN `tag` ON `post-tag`.`tid` = `tag`.`id`
LEFT JOIN `contact` ON `post-tag`.`cid` = `contact`.`id`; LEFT JOIN `contact` ON `post-tag`.`cid` = `contact`.`id`;
--
-- VIEW network-item-view
--
DROP VIEW IF EXISTS `network-item-view`;
CREATE VIEW `network-item-view` AS SELECT
`post-user`.`uri-id` AS `uri-id`,
`post-thread-user`.`post-user-id` AS `parent`,
`post-user`.`received` AS `received`,
`post-thread-user`.`commented` AS `commented`,
`post-user`.`created` AS `created`,
`post-user`.`uid` AS `uid`,
`post-thread-user`.`starred` AS `starred`,
`post-thread-user`.`mention` AS `mention`,
`post-user`.`network` AS `network`,
`post-user`.`unseen` AS `unseen`,
`post-user`.`gravity` AS `gravity`,
`post-user`.`contact-id` AS `contact-id`,
`ownercontact`.`contact-type` AS `contact-type`
FROM `post-user`
INNER JOIN `post-thread-user` ON `post-thread-user`.`uri-id` = `post-user`.`parent-uri-id` AND `post-thread-user`.`uid` = `post-user`.`uid`
STRAIGHT_JOIN `contact` ON `contact`.`id` = `post-thread-user`.`contact-id`
STRAIGHT_JOIN `contact` AS `authorcontact` ON `authorcontact`.`id` = `post-thread-user`.`author-id`
STRAIGHT_JOIN `contact` AS `ownercontact` ON `ownercontact`.`id` = `post-thread-user`.`owner-id`
WHERE `post-user`.`visible` AND NOT `post-user`.`deleted`
AND (NOT `contact`.`readonly` AND NOT `contact`.`blocked` AND NOT `contact`.`pending`)
AND (`post-user`.`hidden` IS NULL OR NOT `post-user`.`hidden`)
AND NOT `authorcontact`.`blocked` AND NOT `ownercontact`.`blocked`
AND NOT EXISTS(SELECT `cid` FROM `user-contact` WHERE `uid` = `post-thread-user`.`uid` AND `cid` IN (`authorcontact`.`id`, `ownercontact`.`id`) AND (`blocked` OR `ignored`))
AND NOT EXISTS(SELECT `gsid` FROM `user-gserver` WHERE `uid` = `post-thread-user`.`uid` AND `gsid` IN (`authorcontact`.`gsid`, `ownercontact`.`gsid`) AND `ignored`);
-- --
-- VIEW network-thread-view -- VIEW network-thread-view
-- --
@ -2931,7 +3441,35 @@ CREATE VIEW `network-thread-view` AS SELECT
AND (NOT `contact`.`readonly` AND NOT `contact`.`blocked` AND NOT `contact`.`pending`) AND (NOT `contact`.`readonly` AND NOT `contact`.`blocked` AND NOT `contact`.`pending`)
AND (`post-thread-user`.`hidden` IS NULL OR NOT `post-thread-user`.`hidden`) AND (`post-thread-user`.`hidden` IS NULL OR NOT `post-thread-user`.`hidden`)
AND NOT `authorcontact`.`blocked` AND NOT `ownercontact`.`blocked` AND NOT `authorcontact`.`blocked` AND NOT `ownercontact`.`blocked`
AND NOT EXISTS(SELECT `cid` FROM `user-contact` WHERE `uid` = `post-thread-user`.`uid` AND `cid` IN (`authorcontact`.`id`, `ownercontact`.`id`) AND (`blocked` OR `ignored`)) AND NOT EXISTS(SELECT `cid` FROM `user-contact` WHERE `uid` = `post-thread-user`.`uid` AND `cid` IN (`post-thread-user`.`author-id`, `post-thread-user`.`owner-id`, `post-thread-user`.`causer-id`) AND (`blocked` OR `ignored` OR `channel-only`))
AND NOT EXISTS(SELECT `gsid` FROM `user-gserver` WHERE `uid` = `post-thread-user`.`uid` AND `gsid` IN (`authorcontact`.`gsid`, `ownercontact`.`gsid`) AND `ignored`);
--
-- VIEW network-thread-circle-view
--
DROP VIEW IF EXISTS `network-thread-circle-view`;
CREATE VIEW `network-thread-circle-view` AS SELECT
`post-thread-user`.`uri-id` AS `uri-id`,
`post-thread-user`.`post-user-id` AS `parent`,
`post-thread-user`.`received` AS `received`,
`post-thread-user`.`commented` AS `commented`,
`post-thread-user`.`created` AS `created`,
`post-thread-user`.`uid` AS `uid`,
`post-thread-user`.`starred` AS `starred`,
`post-thread-user`.`mention` AS `mention`,
`post-thread-user`.`network` AS `network`,
`post-thread-user`.`contact-id` AS `contact-id`,
`ownercontact`.`contact-type` AS `contact-type`
FROM `post-thread-user`
INNER JOIN `post-user` ON `post-user`.`id` = `post-thread-user`.`post-user-id`
STRAIGHT_JOIN `contact` ON `contact`.`id` = `post-thread-user`.`contact-id`
STRAIGHT_JOIN `contact` AS `authorcontact` ON `authorcontact`.`id` = `post-thread-user`.`author-id`
STRAIGHT_JOIN `contact` AS `ownercontact` ON `ownercontact`.`id` = `post-thread-user`.`owner-id`
WHERE `post-user`.`visible` AND NOT `post-user`.`deleted`
AND (NOT `contact`.`readonly` AND NOT `contact`.`blocked` AND NOT `contact`.`pending`)
AND (`post-thread-user`.`hidden` IS NULL OR NOT `post-thread-user`.`hidden`)
AND NOT `authorcontact`.`blocked` AND NOT `ownercontact`.`blocked`
AND NOT EXISTS(SELECT `cid` FROM `user-contact` WHERE `uid` = `post-thread-user`.`uid` AND `cid` IN (`post-thread-user`.`author-id`, `post-thread-user`.`owner-id`, `post-thread-user`.`causer-id`) AND (`blocked` OR `ignored`))
AND NOT EXISTS(SELECT `gsid` FROM `user-gserver` WHERE `uid` = `post-thread-user`.`uid` AND `gsid` IN (`authorcontact`.`gsid`, `ownercontact`.`gsid`) AND `ignored`); AND NOT EXISTS(SELECT `gsid` FROM `user-gserver` WHERE `uid` = `post-thread-user`.`uid` AND `gsid` IN (`authorcontact`.`gsid`, `ownercontact`.`gsid`) AND `ignored`);
-- --

View file

@ -850,10 +850,6 @@ Here is a complete list of all hook callbacks with file locations (as of 24-Sep-
Hook::callAll('register_account', $uid); Hook::callAll('register_account', $uid);
Hook::callAll('remove_user', $user); Hook::callAll('remove_user', $user);
### src/Module/Notifications/Ping.php
Hook::callAll('network_ping', $arr);
### src/Module/PermissionTooltip.php ### src/Module/PermissionTooltip.php
Hook::callAll('lockview_content', $item); Hook::callAll('lockview_content', $item);

View file

@ -23,7 +23,7 @@ Predefined Channels
* For you: Posts from contacts you interact with and who interact with you. In detail, it consists of: * For you: Posts from contacts you interact with and who interact with you. In detail, it consists of:
* Posts from people you interact with on a more than average level. * Posts from people you interact with on a more than average level.
* Posts from the accounts that you follow with a more than average number of interactions- * Posts from the accounts that you follow with a more than average number of interactions.
* Posts from accounts where you activated "notify on new posts" or where you have set the channel frequency accordingly. * Posts from accounts where you activated "notify on new posts" or where you have set the channel frequency accordingly.
* Discover: Posts from contacts you don't follow, but that might be of interest for you to follow. In detail, it consists of: * Discover: Posts from contacts you don't follow, but that might be of interest for you to follow. In detail, it consists of:
* Posts from people you don't follow but you interact with on a more than average level. * Posts from people you don't follow but you interact with on a more than average level.
@ -50,9 +50,9 @@ Each channel is defined by these values:
* Access Key: When you want to access this channel via an access key, you can define it here. Pay attention to not use an already used one. * Access Key: When you want to access this channel via an access key, you can define it here. Pay attention to not use an already used one.
* Circle: This defines the data source for this channel. By default it is set to the public timeline. There are some predefined values, like the accounts that you follow or the accounts that follow you. Also all of your circles can be selected. * Circle: This defines the data source for this channel. By default it is set to the public timeline. There are some predefined values, like the accounts that you follow or the accounts that follow you. Also all of your circles can be selected.
* Include Tags: Comma separated list of tags. A post will be used when it contains any of the listed tags. * Include Tags: Comma separated list of tags. A post will be used when it contains any of the listed tags.
* Exclude Tags: Comma separated list of tags. If a post contain any of these tags, then it will not be part of nthis channel. * Exclude Tags: Comma separated list of tags. If a post contain any of these tags, then it will not be part of this channel.
* Full Text Search: This can be used to include or exclude content, based on the content and some additional keywords. It uses the "boolean mode" operators from MariaDB: https://mariadb.com/kb/en/full-text-index-overview/#in-boolean-mode * Full Text Search: This can be used to include or exclude content, based on the content and some additional keywords. It uses the "boolean mode" operators from MariaDB: https://mariadb.com/kb/en/full-text-index-overview/#in-boolean-mode
* Images, Videos, Audio: When selected, you will see content with the selected media type. This can be combined. If none of these fields are checked, you will see any content, with or without attacked media. * Images, Videos, Audio: When selected, you will see content with the selected media type. This can be combined. If none of these fields are checked, you will see any content, with or without attached media.
Additional keywords for the full text search Additional keywords for the full text search
--- ---
@ -61,8 +61,8 @@ Additionally to the search for content, there are keywords that can be used in t
Alternatives are presented with "|". Alternatives are presented with "|".
* from - Use "from:nickname" or "from:nickname@domain.tld" to search for posts from a specific author. * from - Use "from:nickname" or "from:nickname@domain.tld" to search for posts from a specific author.
* to - Use "from:nickname" or "from:nickname@domain.tld" to search for posts with the given contact as receiver. * to - Use "to:nickname" or "to:nickname@domain.tld" to search for posts with the given contact as receiver.
* group - Use "group:nickname" or "group:nickname@domain.tld" to search for group post of the given group. * group - Use "group:nickname" or "group:nickname@domain.tld" to search for posts of the given group.
* application | relay - Use "application:nickname" or "application:nickname@domain.tld" to search for posts that had been reshared by the given relay application. * application | relay - Use "application:nickname" or "application:nickname@domain.tld" to search for posts that had been reshared by the given relay application.
* server - Use "server:hostname" to search for posts from a specific server. In the case of group postings, the search text contains both the hostname of the group server and the author's hostname. * server - Use "server:hostname" to search for posts from a specific server. In the case of group postings, the search text contains both the hostname of the group server and the author's hostname.
* source - The ActivityPub type of the post source. Use this for example to include or exclude group posts or posts from services (aka bots). * source - The ActivityPub type of the post source. Use this for example to include or exclude group posts or posts from services (aka bots).
@ -95,5 +95,5 @@ Alternatives are presented with "|".
* visibility:private * visibility:private
* language | lang - Use "language:code" to search for posts with the given language in the [ISO 639-1](https://en.wikipedia.org/wiki/ISO_639-1) format. * language | lang - Use "language:code" to search for posts with the given language in the [ISO 639-1](https://en.wikipedia.org/wiki/ISO_639-1) format.
Remember that you can combine these kerywords. Remember that you can combine these keywords.
So for example you can create a channel with all posts that talk about the Fediverse - that aren't posted in the Fediverse with the search terms: "fediverse -network:apub -network:dfrn" So for example you can create a channel with all posts that talk about the Fediverse - that aren't posted in the Fediverse with the search terms: "fediverse -network:apub -network:dfrn"

View file

@ -1,5 +1,5 @@
Friendica Documentation and Resources Help
===================================== ====
**User Manual** **User Manual**
@ -64,18 +64,18 @@ Friendica Documentation and Resources
* [Database schema documentation](help/database) * [Database schema documentation](help/database)
* [Class Autoloading](help/autoloader) * [Class Autoloading](help/autoloader)
**External Resources** **Links**
* Website: [https://friendi.ca](https://friendi.ca)
* Help Group: [@helpers@forum.friendi.ca](https://forum.friendi.ca/~helpers)
* XMPP: [support@forum.friendi.ca](xmpp:support@forum.friendi.ca?join)
* IRC: [https://web.libera.chat/?channels=#friendica](https://web.libera.chat/?channels=#friendica)
* Matrix: [https://matrix.to/#/#friendi.ca:matrix.org](https://matrix.to/#/#friendi.ca:matrix.org)
* Mailing List: [https://mailman.friendi.ca/mailman/listinfo/support-friendi.ca](http://mailman.friendi.ca/mailman/listinfo/support-friendi.ca)
* [Main Website](https://friendi.ca)
* Ways to get Support
* Friendica Support Group: [@helpers@forum.friendi.ca](https://forum.friendi.ca/~helpers)
* [Mailing List Archive](http://mailman.friendi.ca/mailman/listinfo/support-friendi.ca) you can subscribe to the list by sending an email to ``support-request(at)friendi.ca?subject=subscribe``
* Community chat rooms (the IRC, Matrix and XMPP rooms are bridged) these public chats are logged [from IRC](https://gnusociarg.nsupdate.info/2021/%23friendica/) and [Matrix](https://view.matrix.org/alias/%23friendi.ca:matrix.org/)
* XMPP/Jabber MUC: support(at)forum.friendi.ca
* IRC: #friendica at [libera.chat](https://web.libera.chat/?channels=#friendica)
* Matrix: [#friendi.ca](https://matrix.to/#/#friendi.ca:matrix.org) or [#friendica-en](https://matrix.to/#/#friendica-en:matrix.org) at matrix.org
**About** **About**
* [Site/Version Info](friendica) * [Server Information](friendica)
* [Friendica Credits](credits) * [Terms of Service](tos)
* [Credits](credits)

View file

@ -30,7 +30,7 @@ Due to the large variety of operating systems and PHP platforms in existence we
* Apache with mod-rewrite enabled and "Options All" so you can use a local `.htaccess` file * Apache with mod-rewrite enabled and "Options All" so you can use a local `.htaccess` file
* PHP 7.4+ * PHP 7.4+
* PHP *command line* access with register_argc_argv set to true in the php.ini file * PHP *command line* access with register_argc_argv set to true in the php.ini file
* Curl, GD, GMP, PDO, mbstrings, MySQLi, hash, xml, zip, IntlChar and OpenSSL extensions * Curl, GD, GMP, PDO, mbstrings, MySQLi, hash, xml, zip, IntlChar, IDN and OpenSSL extensions
* The POSIX module of PHP needs to be activated (e.g. [RHEL, CentOS](http://www.bigsoft.co.uk/blog/index.php/2014/12/08/posix-php-commands-not-working-under-centos-7) have disabled it) * The POSIX module of PHP needs to be activated (e.g. [RHEL, CentOS](http://www.bigsoft.co.uk/blog/index.php/2014/12/08/posix-php-commands-not-working-under-centos-7) have disabled it)
* Some form of email server or email gateway such that PHP mail() works. * Some form of email server or email gateway such that PHP mail() works.
If you cannot set up your own email server, you can use the [phpmailer](https://github.com/friendica/friendica-addons/tree/develop/phpmailer) addon and use a remote SMTP server. If you cannot set up your own email server, you can use the [phpmailer](https://github.com/friendica/friendica-addons/tree/develop/phpmailer) addon and use a remote SMTP server.

View file

@ -107,5 +107,7 @@ They think they are a friend.
You can also "block" a person. You can also "block" a person.
This completely blocks communications with that person. This completely blocks communications with that person.
They may still be able to see your public posts, as can anybody in the world, but they cannot communicate with you directly. They may still be able to see your public posts, as can anybody in the world, but they cannot communicate with you directly.
Friendica will notify their server that you have blocked them, although normally that server should not notify them individually.
However, there are various simple ways they can deduce that they have been blocked if they investigate.
You can also delete a friend no matter what the friendship status - which completely removes everything relating to that person from your website. You can also delete a friend no matter what the friendship status - which completely removes everything relating to that person from your website.

View file

@ -68,6 +68,7 @@ Database Tables
| [post-history](help/database/db_post-history) | Post history | | [post-history](help/database/db_post-history) | Post history |
| [post-link](help/database/db_post-link) | Post related external links | | [post-link](help/database/db_post-link) | Post related external links |
| [post-media](help/database/db_post-media) | Attached media | | [post-media](help/database/db_post-media) | Attached media |
| [post-origin](help/database/db_post-origin) | Posts from local users |
| [post-question](help/database/db_post-question) | Question | | [post-question](help/database/db_post-question) | Question |
| [post-question-option](help/database/db_post-question-option) | Question option | | [post-question-option](help/database/db_post-question-option) | Question option |
| [post-searchindex](help/database/db_post-searchindex) | Content for all posts | | [post-searchindex](help/database/db_post-searchindex) | Content for all posts |

View file

@ -7,11 +7,11 @@ Fields
------ ------
| Field | Description | Type | Null | Key | Default | Extra | | Field | Description | Type | Null | Key | Default | Extra |
| --------------------- | -------------------------------------------------- | ---------------- | ---- | --- | ------------------- | -------------- | | --------------------- | -------------------------------------------------------------- | ---------------- | ---- | --- | ------------------- | -------------- |
| id | sequential ID | int unsigned | NO | PRI | NULL | auto_increment | | id | sequential ID | int unsigned | NO | PRI | NULL | auto_increment |
| url | | varbinary(383) | NO | | | | | url | | varbinary(383) | NO | | | |
| nurl | | varbinary(383) | NO | | | | | nurl | | varbinary(383) | NO | | | |
| version | | varchar(255) | NO | | | | | version | The version of this server software. | varchar(255) | NO | | | |
| site_name | | varchar(255) | NO | | | | | site_name | | varchar(255) | NO | | | |
| info | | text | YES | | NULL | | | info | | text | YES | | NULL | |
| register_policy | | tinyint | NO | | 0 | | | register_policy | | tinyint | NO | | 0 | |
@ -23,10 +23,14 @@ Fields
| local-comments | Number of local comments | int unsigned | YES | | NULL | | | local-comments | Number of local comments | int unsigned | YES | | NULL | |
| directory-type | Type of directory service (Poco, Mastodon) | tinyint | YES | | 0 | | | directory-type | Type of directory service (Poco, Mastodon) | tinyint | YES | | 0 | |
| poco | | varbinary(383) | NO | | | | | poco | | varbinary(383) | NO | | | |
| openwebauth | Path to the OpenWebAuth endpoint | varbinary(383) | YES | | NULL | |
| authredirect | Path to the authRedirect endpoint | varbinary(383) | YES | | NULL | |
| noscrape | | varbinary(383) | NO | | | | | noscrape | | varbinary(383) | NO | | | |
| network | | char(4) | NO | | | | | network | | char(4) | NO | | | |
| protocol | The protocol of the server | tinyint unsigned | YES | | NULL | | | protocol | The protocol of the server | tinyint unsigned | YES | | NULL | |
| platform | | varchar(255) | NO | | | | | platform | The canonical name of this server software. | varchar(255) | NO | | | |
| repository | The url of the source code repository of this server software. | varbinary(383) | YES | | NULL | |
| homepage | The url of the homepage of this server software. | varbinary(383) | YES | | NULL | |
| relay-subscribe | Has the server subscribed to the relay system | boolean | NO | | 0 | | | relay-subscribe | Has the server subscribed to the relay system | boolean | NO | | 0 | |
| relay-scope | The scope of messages that the server wants to get | varchar(10) | NO | | | | | relay-scope | The scope of messages that the server wants to get | varchar(10) | NO | | | |
| detection-method | Method that had been used to detect that server | tinyint unsigned | YES | | NULL | | | detection-method | Method that had been used to detect that server | tinyint unsigned | YES | | NULL | |

View file

@ -7,11 +7,12 @@ Fields
------ ------
| Field | Description | Type | Null | Key | Default | Extra | | Field | Description | Type | Null | Key | Default | Extra |
| ------------------ | -------------------------------------- | -------------- | ---- | --- | ------- | -------------- | | ------------------ | -------------------------------------- | ---------------- | ---- | --- | ------- | -------------- |
| id | sequential ID | int unsigned | NO | PRI | NULL | auto_increment | | id | sequential ID | int unsigned | NO | PRI | NULL | auto_increment |
| activity-id | id of the incoming activity | varbinary(383) | YES | | NULL | | | activity-id | id of the incoming activity | varbinary(383) | YES | | NULL | |
| object-id | | varbinary(383) | YES | | NULL | | | object-id | | varbinary(383) | YES | | NULL | |
| in-reply-to-id | | varbinary(383) | YES | | NULL | | | in-reply-to-id | | varbinary(383) | YES | | NULL | |
| context | | varbinary(383) | YES | | NULL | |
| conversation | | varbinary(383) | YES | | NULL | | | conversation | | varbinary(383) | YES | | NULL | |
| type | Type of the activity | varchar(64) | YES | | NULL | | | type | Type of the activity | varchar(64) | YES | | NULL | |
| object-type | Type of the object activity | varchar(64) | YES | | NULL | | | object-type | Type of the object activity | varchar(64) | YES | | NULL | |
@ -22,6 +23,7 @@ Fields
| push | Is the entry pushed or have pulled it? | boolean | YES | | NULL | | | push | Is the entry pushed or have pulled it? | boolean | YES | | NULL | |
| trust | Do we trust this entry? | boolean | YES | | NULL | | | trust | Do we trust this entry? | boolean | YES | | NULL | |
| wid | Workerqueue id | int unsigned | YES | | NULL | | | wid | Workerqueue id | int unsigned | YES | | NULL | |
| retrial | Retrial counter | tinyint unsigned | YES | | 0 | |
Indexes Indexes
------------ ------------

View file

@ -11,7 +11,7 @@ Fields
| uri-id | Id of the item-uri table entry that contains the item uri | int unsigned | NO | PRI | NULL | | | uri-id | Id of the item-uri table entry that contains the item uri | int unsigned | NO | PRI | NULL | |
| owner-id | Item owner | int unsigned | NO | | 0 | | | owner-id | Item owner | int unsigned | NO | | 0 | |
| contact-type | Person, organisation, news, community, relay | tinyint | NO | | 0 | | | contact-type | Person, organisation, news, community, relay | tinyint | NO | | 0 | |
| media-type | Type of media in a bit array (1 = image, 2 = video, 4 = audio | tinyint | NO | | 0 | | | media-type | Type of media in a bit array (1 = image, 2 = video, 4 = audio) | tinyint | NO | | 0 | |
| language | Language information about this post in the ISO 639-1 format | char(2) | YES | | NULL | | | language | Language information about this post in the ISO 639-1 format | char(2) | YES | | NULL | |
| searchtext | Simplified text for the full text search | mediumtext | YES | | NULL | | | searchtext | Simplified text for the full text search | mediumtext | YES | | NULL | |
| size | Body size | int unsigned | YES | | NULL | | | size | Body size | int unsigned | YES | | NULL | |

View file

@ -0,0 +1,48 @@
Table post-origin
===========
Posts from local users
Fields
------
| Field | Description | Type | Null | Key | Default | Extra |
| ------------- | ------------------------------------------------------------ | ------------------ | ---- | --- | ------------------- | ----- |
| id | | int unsigned | NO | PRI | NULL | |
| uri-id | Id of the item-uri table entry that contains the item uri | int unsigned | NO | | NULL | |
| uid | Owner id which owns this copy of the item | mediumint unsigned | NO | | NULL | |
| parent-uri-id | Id of the item-uri table that contains the parent uri | int unsigned | YES | | NULL | |
| thr-parent-id | Id of the item-uri table that contains the thread parent uri | int unsigned | YES | | NULL | |
| created | Creation timestamp. | datetime | NO | | 0001-01-01 00:00:00 | |
| received | datetime | datetime | NO | | 0001-01-01 00:00:00 | |
| gravity | | tinyint unsigned | NO | | 0 | |
| vid | Id of the verb table entry that contains the activity verbs | smallint unsigned | YES | | NULL | |
| private | 0=public, 1=private, 2=unlisted | tinyint unsigned | NO | | 0 | |
| wall | This item was posted to the wall of uid | boolean | NO | | 0 | |
Indexes
------------
| Name | Fields |
| ----------------- | ------------------- |
| PRIMARY | id |
| uid_uri-id | UNIQUE, uid, uri-id |
| uri-id | uri-id |
| parent-uri-id | parent-uri-id |
| thr-parent-id | thr-parent-id |
| vid | vid |
| parent-uri-id_uid | parent-uri-id, uid |
| uid_wall_received | uid, wall, received |
Foreign Keys
------------
| Field | Target Table | Target Field |
|-------|--------------|--------------|
| uri-id | [item-uri](help/database/db_item-uri) | id |
| uid | [user](help/database/db_user) | uid |
| parent-uri-id | [item-uri](help/database/db_item-uri) | id |
| thr-parent-id | [item-uri](help/database/db_item-uri) | id |
| vid | [verb](help/database/db_verb) | id |
Return to [database documentation](help/database)

View file

@ -10,7 +10,7 @@ Fields
| ---------- | --------------------------------------------------------------------- | ------------ | ---- | --- | ------- | ----- | | ---------- | --------------------------------------------------------------------- | ------------ | ---- | --- | ------- | ----- |
| uri-id | Id of the item-uri table entry that contains the item uri | int unsigned | NO | PRI | NULL | | | uri-id | Id of the item-uri table entry that contains the item uri | int unsigned | NO | PRI | NULL | |
| owner-id | Item owner | int unsigned | NO | | 0 | | | owner-id | Item owner | int unsigned | NO | | 0 | |
| media-type | Type of media in a bit array (1 = image, 2 = video, 4 = audio | tinyint | NO | | 0 | | | media-type | Type of media in a bit array (1 = image, 2 = video, 4 = audio) | tinyint | NO | | 0 | |
| language | Language information about this post in the ISO 639-1 format | char(2) | YES | | NULL | | | language | Language information about this post in the ISO 639-1 format | char(2) | YES | | NULL | |
| searchtext | Simplified text for the full text search | mediumtext | YES | | NULL | | | searchtext | Simplified text for the full text search | mediumtext | YES | | NULL | |
| size | Body size | int unsigned | YES | | NULL | | | size | Body size | int unsigned | YES | | NULL | |

View file

@ -9,6 +9,7 @@ Fields
| Field | Description | Type | Null | Key | Default | Extra | | Field | Description | Type | Null | Key | Default | Extra |
| --------------- | ------------------------------------------------------------------------------------------------------- | ------------------ | ---- | --- | ------------------- | ----- | | --------------- | ------------------------------------------------------------------------------------------------------- | ------------------ | ---- | --- | ------------------- | ----- |
| uri-id | Id of the item-uri table entry that contains the item uri | int unsigned | NO | PRI | NULL | | | uri-id | Id of the item-uri table entry that contains the item uri | int unsigned | NO | PRI | NULL | |
| context-id | Id of the item-uri table entry that contains the endpoint for the context collection | int unsigned | YES | | NULL | |
| conversation-id | Id of the item-uri table entry that contains the conversation uri | int unsigned | YES | | NULL | | | conversation-id | Id of the item-uri table entry that contains the conversation uri | int unsigned | YES | | NULL | |
| owner-id | Item owner | int unsigned | NO | | 0 | | | owner-id | Item owner | int unsigned | NO | | 0 | |
| author-id | Item author | int unsigned | NO | | 0 | | | author-id | Item author | int unsigned | NO | | 0 | |
@ -40,6 +41,7 @@ Indexes
| -------------------- | --------------------- | | -------------------- | --------------------- |
| PRIMARY | uid, uri-id | | PRIMARY | uid, uri-id |
| uri-id | uri-id | | uri-id | uri-id |
| context-id | context-id |
| conversation-id | conversation-id | | conversation-id | conversation-id |
| owner-id | owner-id | | owner-id | owner-id |
| author-id | author-id | | author-id | author-id |
@ -68,6 +70,7 @@ Foreign Keys
| Field | Target Table | Target Field | | Field | Target Table | Target Field |
|-------|--------------|--------------| |-------|--------------|--------------|
| uri-id | [item-uri](help/database/db_item-uri) | id | | uri-id | [item-uri](help/database/db_item-uri) | id |
| context-id | [item-uri](help/database/db_item-uri) | id |
| conversation-id | [item-uri](help/database/db_item-uri) | id | | conversation-id | [item-uri](help/database/db_item-uri) | id |
| owner-id | [contact](help/database/db_contact) | id | | owner-id | [contact](help/database/db_contact) | id |
| author-id | [contact](help/database/db_contact) | id | | author-id | [contact](help/database/db_contact) | id |

View file

@ -9,6 +9,7 @@ Fields
| Field | Description | Type | Null | Key | Default | Extra | | Field | Description | Type | Null | Key | Default | Extra |
| --------------- | ------------------------------------------------------------------------------------------------------- | ------------ | ---- | --- | ------------------- | ----- | | --------------- | ------------------------------------------------------------------------------------------------------- | ------------ | ---- | --- | ------------------- | ----- |
| uri-id | Id of the item-uri table entry that contains the item uri | int unsigned | NO | PRI | NULL | | | uri-id | Id of the item-uri table entry that contains the item uri | int unsigned | NO | PRI | NULL | |
| context-id | Id of the item-uri table entry that contains the endpoint for the context collection | int unsigned | YES | | NULL | |
| conversation-id | Id of the item-uri table entry that contains the conversation uri | int unsigned | YES | | NULL | | | conversation-id | Id of the item-uri table entry that contains the conversation uri | int unsigned | YES | | NULL | |
| owner-id | Item owner | int unsigned | NO | | 0 | | | owner-id | Item owner | int unsigned | NO | | 0 | |
| author-id | Item author | int unsigned | NO | | 0 | | | author-id | Item author | int unsigned | NO | | 0 | |
@ -25,6 +26,7 @@ Indexes
| Name | Fields | | Name | Fields |
| --------------- | --------------- | | --------------- | --------------- |
| PRIMARY | uri-id | | PRIMARY | uri-id |
| context-id | context-id |
| conversation-id | conversation-id | | conversation-id | conversation-id |
| owner-id | owner-id | | owner-id | owner-id |
| author-id | author-id | | author-id | author-id |
@ -38,6 +40,7 @@ Foreign Keys
| Field | Target Table | Target Field | | Field | Target Table | Target Field |
|-------|--------------|--------------| |-------|--------------|--------------|
| uri-id | [item-uri](help/database/db_item-uri) | id | | uri-id | [item-uri](help/database/db_item-uri) | id |
| context-id | [item-uri](help/database/db_item-uri) | id |
| conversation-id | [item-uri](help/database/db_item-uri) | id | | conversation-id | [item-uri](help/database/db_item-uri) | id |
| owner-id | [contact](help/database/db_contact) | id | | owner-id | [contact](help/database/db_contact) | id |
| author-id | [contact](help/database/db_contact) | id | | author-id | [contact](help/database/db_contact) | id |

View file

@ -7,12 +7,13 @@ Fields
------ ------
| Field | Description | Type | Null | Key | Default | Extra | | Field | Description | Type | Null | Key | Default | Extra |
| ----------------- | --------------------------------------------------------------------------------- | ------------------ | ---- | --- | ------------------- | -------------- | | ----------------- | ------------------------------------------------------------------------------------ | ------------------ | ---- | --- | ------------------- | -------------- |
| id | | int unsigned | NO | PRI | NULL | auto_increment | | id | | int unsigned | NO | PRI | NULL | auto_increment |
| uri-id | Id of the item-uri table entry that contains the item uri | int unsigned | NO | | NULL | | | uri-id | Id of the item-uri table entry that contains the item uri | int unsigned | NO | | NULL | |
| parent-uri-id | Id of the item-uri table that contains the parent uri | int unsigned | YES | | NULL | | | parent-uri-id | Id of the item-uri table that contains the parent uri | int unsigned | YES | | NULL | |
| thr-parent-id | Id of the item-uri table that contains the thread parent uri | int unsigned | YES | | NULL | | | thr-parent-id | Id of the item-uri table that contains the thread parent uri | int unsigned | YES | | NULL | |
| external-id | Id of the item-uri table entry that contains the external uri | int unsigned | YES | | NULL | | | external-id | Id of the item-uri table entry that contains the external uri | int unsigned | YES | | NULL | |
| replies-id | Id of the item-uri table entry that contains the endpoint for the replies collection | int unsigned | YES | | NULL | |
| created | Creation timestamp. | datetime | NO | | 0001-01-01 00:00:00 | | | created | Creation timestamp. | datetime | NO | | 0001-01-01 00:00:00 | |
| edited | Date of last edit (default is created) | datetime | NO | | 0001-01-01 00:00:00 | | | edited | Date of last edit (default is created) | datetime | NO | | 0001-01-01 00:00:00 | |
| received | datetime | datetime | NO | | 0001-01-01 00:00:00 | | | received | datetime | datetime | NO | | 0001-01-01 00:00:00 | |
@ -25,6 +26,7 @@ Fields
| post-reason | Reason why the post arrived at the user | tinyint unsigned | NO | | 0 | | | post-reason | Reason why the post arrived at the user | tinyint unsigned | NO | | 0 | |
| vid | Id of the verb table entry that contains the activity verbs | smallint unsigned | YES | | NULL | | | vid | Id of the verb table entry that contains the activity verbs | smallint unsigned | YES | | NULL | |
| private | 0=public, 1=private, 2=unlisted | tinyint unsigned | NO | | 0 | | | private | 0=public, 1=private, 2=unlisted | tinyint unsigned | NO | | 0 | |
| restrictions | Bit array of post restrictions (1 = Reply, 2 = Like, 4 = Announce) | tinyint unsigned | YES | | NULL | |
| global | | boolean | NO | | 0 | | | global | | boolean | NO | | 0 | |
| visible | | boolean | NO | | 0 | | | visible | | boolean | NO | | 0 | |
| deleted | item has been marked for deletion | boolean | NO | | 0 | | | deleted | item has been marked for deletion | boolean | NO | | 0 | |
@ -50,6 +52,7 @@ Indexes
| parent-uri-id | parent-uri-id | | parent-uri-id | parent-uri-id |
| thr-parent-id | thr-parent-id | | thr-parent-id | thr-parent-id |
| external-id | external-id | | external-id | external-id |
| replies-id | replies-id |
| owner-id | owner-id | | owner-id | owner-id |
| author-id | author-id | | author-id | author-id |
| causer-id | causer-id | | causer-id | causer-id |
@ -76,6 +79,7 @@ Foreign Keys
| parent-uri-id | [item-uri](help/database/db_item-uri) | id | | parent-uri-id | [item-uri](help/database/db_item-uri) | id |
| thr-parent-id | [item-uri](help/database/db_item-uri) | id | | thr-parent-id | [item-uri](help/database/db_item-uri) | id |
| external-id | [item-uri](help/database/db_item-uri) | id | | external-id | [item-uri](help/database/db_item-uri) | id |
| replies-id | [item-uri](help/database/db_item-uri) | id |
| owner-id | [contact](help/database/db_contact) | id | | owner-id | [contact](help/database/db_contact) | id |
| author-id | [contact](help/database/db_contact) | id | | author-id | [contact](help/database/db_contact) | id |
| causer-id | [contact](help/database/db_contact) | id | | causer-id | [contact](help/database/db_contact) | id |

View file

@ -7,11 +7,12 @@ Fields
------ ------
| Field | Description | Type | Null | Key | Default | Extra | | Field | Description | Type | Null | Key | Default | Extra |
| ------------- | --------------------------------------------------------------------------------- | ----------------- | ---- | --- | ------------------- | ----- | | ------------- | ------------------------------------------------------------------------------------ | ----------------- | ---- | --- | ------------------- | ----- |
| uri-id | Id of the item-uri table entry that contains the item uri | int unsigned | NO | PRI | NULL | | | uri-id | Id of the item-uri table entry that contains the item uri | int unsigned | NO | PRI | NULL | |
| parent-uri-id | Id of the item-uri table that contains the parent uri | int unsigned | YES | | NULL | | | parent-uri-id | Id of the item-uri table that contains the parent uri | int unsigned | YES | | NULL | |
| thr-parent-id | Id of the item-uri table that contains the thread parent uri | int unsigned | YES | | NULL | | | thr-parent-id | Id of the item-uri table that contains the thread parent uri | int unsigned | YES | | NULL | |
| external-id | Id of the item-uri table entry that contains the external uri | int unsigned | YES | | NULL | | | external-id | Id of the item-uri table entry that contains the external uri | int unsigned | YES | | NULL | |
| replies-id | Id of the item-uri table entry that contains the endpoint for the replies collection | int unsigned | YES | | NULL | |
| created | Creation timestamp. | datetime | NO | | 0001-01-01 00:00:00 | | | created | Creation timestamp. | datetime | NO | | 0001-01-01 00:00:00 | |
| edited | Date of last edit (default is created) | datetime | NO | | 0001-01-01 00:00:00 | | | edited | Date of last edit (default is created) | datetime | NO | | 0001-01-01 00:00:00 | |
| received | datetime | datetime | NO | | 0001-01-01 00:00:00 | | | received | datetime | datetime | NO | | 0001-01-01 00:00:00 | |
@ -36,6 +37,7 @@ Indexes
| parent-uri-id | parent-uri-id | | parent-uri-id | parent-uri-id |
| thr-parent-id | thr-parent-id | | thr-parent-id | thr-parent-id |
| external-id | external-id | | external-id | external-id |
| replies-id | replies-id |
| owner-id | owner-id | | owner-id | owner-id |
| author-id | author-id | | author-id | author-id |
| causer-id | causer-id | | causer-id | causer-id |
@ -50,6 +52,7 @@ Foreign Keys
| parent-uri-id | [item-uri](help/database/db_item-uri) | id | | parent-uri-id | [item-uri](help/database/db_item-uri) | id |
| thr-parent-id | [item-uri](help/database/db_item-uri) | id | | thr-parent-id | [item-uri](help/database/db_item-uri) | id |
| external-id | [item-uri](help/database/db_item-uri) | id | | external-id | [item-uri](help/database/db_item-uri) | id |
| replies-id | [item-uri](help/database/db_item-uri) | id |
| owner-id | [contact](help/database/db_contact) | id | | owner-id | [contact](help/database/db_contact) | id |
| author-id | [contact](help/database/db_contact) | id | | author-id | [contact](help/database/db_contact) | id |
| causer-id | [contact](help/database/db_contact) | id | | causer-id | [contact](help/database/db_contact) | id |

View file

@ -7,7 +7,7 @@ Fields
------ ------
| Field | Description | Type | Null | Key | Default | Extra | | Field | Description | Type | Null | Key | Default | Extra |
| ------------------------- | ----------------------------------------------------------------------- | ------------------ | ---- | --- | ------- | ----- | | ------------------------- | -------------------------------------------------------------------------- | ------------------ | ---- | --- | ------- | ----- |
| cid | Contact id of the linked public contact | int unsigned | NO | PRI | 0 | | | cid | Contact id of the linked public contact | int unsigned | NO | PRI | 0 | |
| uid | User id | mediumint unsigned | NO | PRI | 0 | | | uid | User id | mediumint unsigned | NO | PRI | 0 | |
| uri-id | Id of the item-uri table entry that contains the contact url | int unsigned | YES | | NULL | | | uri-id | Id of the item-uri table entry that contains the contact url | int unsigned | YES | | NULL | |
@ -15,6 +15,7 @@ Fields
| ignored | Posts from this contact are ignored | boolean | YES | | NULL | | | ignored | Posts from this contact are ignored | boolean | YES | | NULL | |
| collapsed | Posts from this contact are collapsed | boolean | YES | | NULL | | | collapsed | Posts from this contact are collapsed | boolean | YES | | NULL | |
| hidden | This contact is hidden from the others | boolean | YES | | NULL | | | hidden | This contact is hidden from the others | boolean | YES | | NULL | |
| channel-only | This contact is displayed only in channels, but not in the network stream. | boolean | YES | | NULL | |
| is-blocked | User is blocked by this contact | boolean | YES | | NULL | | | is-blocked | User is blocked by this contact | boolean | YES | | NULL | |
| channel-frequency | Controls the frequency of the appearance of this contact in channels | tinyint unsigned | YES | | NULL | | | channel-frequency | Controls the frequency of the appearance of this contact in channels | tinyint unsigned | YES | | NULL | |
| pending | | boolean | YES | | NULL | | | pending | | boolean | YES | | NULL | |

View file

@ -418,10 +418,6 @@ Eine komplette Liste aller Hook-Callbacks mit den zugehörigen Dateien (am 01-Ap
Hook::callAll('storage_instance', $data); Hook::callAll('storage_instance', $data);
Hook::callAll('storage_config', $data); Hook::callAll('storage_config', $data);
### src/Module/Notifications/Ping.php
Hook::callAll('network_ping', $arr);
### src/Module/PermissionTooltip.php ### src/Module/PermissionTooltip.php
Hook::callAll('lockview_content', $item); Hook::callAll('lockview_content', $item);

99
doc/de/Channels.md Normal file
View file

@ -0,0 +1,99 @@
Kanäle (Channels)
=====
* [Home](help)
Kanäle sind eine Möglichkeit neue Inhalte zu finden, oder Inhalte anzuzeigen, die du sonst möglicherweise verpasst hättest.
Es gibt mehrere vordefinierte Kanäle und zusätzlich kannst du deine eigenen, basierend auf ein paar Regeln, erstellen.
Kanäle zeigen nur Beiträge aus den letzten 24 Stunden an. (Dieser Wert kann vom Administrator geändert werden.)
In den Anzeige-Einstellungen, im Bereich "Timelines", kannst du definieren, welche Kanäle und andere Timelines du im "Kanäle"-Widget auf der "Network"-Seite sehen möchtest und welche Kanäle in der Menüleiste oben auf der Seite erscheinen sollen.
Ebenfalls in den Anzeige-Einstellungen, im Bereich "Kanäle", kannst du alle die Sprachen einstellen, die du in deinen Kanälen sehen möchtest. Hier kannst du mehr als eine Sprache auswählen.
Auf der Profilseite kannst du die Kanal-Frequenz für jeden Kontakt definieren. Die Optionen sind:
* Standardhäufigkeit: Beiträge dieses Kontakts werden im "Für Dich"-Kanal angezeigt, wenn du häufig mit diesem Kontakt interagiert hast oder wenn ein Beitrag ein gewisses Maß an Interaktion erreicht hat.
* Alle Beiträge dieses Kontakts anzeigen: Alle Beiträge dieses Kontakts werden auf dem Kanal "Für Dich" erscheinen
* Zeige nur einige Beiträge an: Wenn ein Kontakt viele Beiträge in einem kurzen Zeitraum erstellt, reduziert diese Einstellung die Anzahl der angezeigten Beiträge in jedem Kanal.
* Zeige keine Beiträge an: Beiträge von diesem Kontakt werden in keinem Kanal angezeigt.
Voreingestellte Kanäle
---
* Für Dich: Beiträge von Kontakten mit denen du interagierst und die mit dir interagieren. Im Detail bestehend aus:
* Beiträge von Leuten, mit denen du überdurchschnittlich viel interagierst.
* Beiträge von Kontakten, denen du folgst und mit denen du überdurchschnittlich viel interagierst.
* Beiträge von Kontakten, bei denen du "Benachrichtigung bei neuen Beiträgen" aktiviert hast oder wo du die Kanalfrequenz entsprechend eingestellt hast.
* Entdecken: Beiträge von Kontakten denen du nicht folgst, aber denen zu folgen für dich interessant sein könnte. Im Detail bestehend aus:
* Beiträge von Leuten denen du nicht folgst, aber mit denen du überdurchschnittlich viel interagierst.
* Beiträge von Leuten denen du nicht folgst, aber die mit dir überdurchschnittlich viel interagieren.
* Beliebte Beiträge von Leuten denen du nicht folgst, aber mit denen du interagiert hast oder die mit dir interagiert haben.
* Angesagt: Beiträge mit überdurchschnittlich hoher Anzahl von Interaktionen.
* Sprache: Beiträge in deiner Sprache.
* Folgende: Beiträge von Leuten die dir folgen, aber denen du nicht folgst.
* Geteilt von teilenden: Beiträge von Kontakten denen die Leute folgen, denen du folgst.
* Ruhige teilende: Beiträge von Konten denen du folgst, aber die nicht sehr oft posten.
* Bilder: Beiträge mit Bildern.
* Audio: Beiträge mit Audio.
* Videos: Beiträge mit Videos.
Vom Benutzer eingestellte Kanäle
---
In den Einstellungen, unter "Kanäle", kannst du deine eigenen Kanäle erstellen.
Jeder Kanal wird durch diese Werte definiert:
* Bezeichnung: Dieses Feld ist notwendig und wird für die Kanalbezeichnung verwendet.
* Beschreibung: Eine kurze Beschreibung des Inhalts. Dies kann helfen den Überblick zu behalten, wenn du viele Kanäle hast.
* Zugriffsschlüssel: Wenn du auf diesen Kanal über einen Zugriffsschlüssel zugreifen willst, kannst du ihn hier festlegen. Achte darauf, dass du nicht einen bereits verwendeten Schlüssel benutzt.
* Circle/Kanal: Dies definiert die Datenquelle für diesen Kanal. Voreingestellt ist die Globale Gemeinschaft. Es gibt ein paar vorgegebene Werte, wie die Konten denen du folgst, oder die Kontakte, die dir folgen. Außerdem können alle deine Circles ausgewählt werden.
* Tags einschließen: Durch Kommata getrennte Liste von Tags. Ein Beitrag wird verwendet, wenn er eines der aufgeführten Tags enthält.
* Tags ausschließen: Durch Kommata getrennte Liste von Tags. Wenn ein Beitrag eines dieser Tags enthält, wird er nicht Teil dieses Kanals sein.
* Volltextsuche: Dies kann genutzt werden um Inhalte, basierend auf dem Inhalt und ein paar zusätzlichen Schlüsselwörtern, ein- oder auszuschließen. Es nutzt die "boolean mode"-Operatoren von MariaDB: https://mariadb.com/kb/en/full-text-index-overview/#in-boolean-mode
* Bilder, Videos, Audio: Wenn ausgewählt, wirst du Inhalte mit dem gewählten Medientyp sehen. Diese Optionen können kombiniert werden. Wenn keines dieser Felder ausgewählt wurde, wirst du alle Inhalte, mit oder ohne angefügten Medien, sehen.
Zusätzliche Schlüsselwörter für die Volltextsuche
---
Zusätzlich zu der Suche nach Inhalten, gibt es Schlüsselwörter, die in der Volltextsuche genutzt werden können.
Alternativen werden durch "|" dargestellt.
* from - Verwende "from:nickname" oder "from:nickname@domain.tld" um nach Beiträgen von einem bestimmten Autor zu suchen.
* to - Verwende "to:nickname" oder "to:nickname@domain.tld" um nach Beiträgen mit dem gegebenen Empfänger zu suchen.
* group - Verwende "group:nickname" oder "group:nickname@domain.tld" um nach Beiträgen aus der gegebenen Gruppe zu suchen.
* application | relay - Nutze "application:nickname" oder "application:nickname@domain.tld" um Beiträge zu finden, die von der gegebenen relay application geteilt wurden.
* server - Verwende "server:hostname" um Beiträge von einem bestimmten Server zu suchen. Im Falle eine Gruppen-Postings enthält der Suchtext beides, den Hostname des Gruppen-Servers und den Hostname des Autors.
* source - Der ActivityPub-Typ der Beitragsquelle. Nutze dies um beispielsweise Gruppenpostings oder Beiträge von Services (aka Bots) ein- oder auszuschließen.
* source:person - Der Beitrag wurde von einem regulären Nutzerkonto erstellt.
* source:organization - Der Beitrag wurde von einer Organisation erstellt.
* source:group - Dieser Beitrag wurde über eine Gruppe erstellt oder verteilt.
* source:service | source:news - Dieser Beitrag stammt aus einem 'service' Account. Dieser Quellen(source)-Typ wird oft genutzt um Bot Accounts zu markieren.
* source:application | source:relay - Dieser Beitrag wurde von einer Anwendung (application) erstellt. Dies wird im Fediverse höchstwahrscheinlich für die Beitragserstellung nicht genutzt.
* tag - Nutze "tag:tagname" um nach einem bestimmten tag (Schlagwort) zu suchen.
* media - Mit diesem Schlüsselwort kannst du nach angefügten Medien suchen.
* media:image | media:photo | media:picture - Dieser Beitrag enthält ein Bild
* media:video - Dieser Beitrag enthält ein Video
* media:audio - Dieser Beitrag enthält Audio
* media:card - Dieser Beitrag enthält eine Linkvorschau-'card'
* media:post - Dieser Beitrag verweist auf einen anderen Beitrag, was bedeutet, es ist ein zitierter Beitrag
* network | net - Verwende dies um Netzwerke in deinen Kanal einzuschließen oder von ihm auszuschließen.
* network:apub | network:activitypub - ActivityPub (verwendet von den Systemen im Fediverse)
* network:dfrn | network:friendica - altes Friendica-Protokoll. Heutzutage nutzt Friendica meist ActivityPub.
* network:dspr | network:diaspora - Das Diaspora-Protokoll wird hauptsächlich von Diaspora selbst genutzt. Ein paar andere Systeme unterstützen dieses Protokoll ebenfalls, wie Hubzilla, Socialhome or Ganggo.
* network:feed - RSS/Atom feeds
* network:mail - Mails die via IMAP importiert worden sind.
* network:stat | network:ostatus - Das OStatus-Protokoll wird hauptsächlich von alten GNU Social-Installationen genutzt.
* network:dscs | network:discourse - Beiträge, die über den Discourse connector empfangen werden.
* network:tmbl | network:tumblr - Beiträge, die über den Tumblr connector empfangen werden.
* network:bsky | network:bluesky - Beiträge, die über den Bluesky connector empfangen werden.
* platform - Benutze dies, um Plattformen in deinen Kanal einzuschließen, oder von ihm auszuschließen, d.h. "+platform:friendica". Im Falle eines Gruppen-Postings enthält der Suchtext beides, die Plattform des Gruppen-Servers und die Plattform des Autors.
* visibility - Du hast die Wahl zwischen verschiedenen Sichtbarkeiten. Du kannst nur die ungelisteten oder privaten Beiträge sehen, zu denen du Zugang hast.
* visibility:public - (öffentlich)
* visibility:unlisted - (ungelistet)
* visibility:private - (privat)
* language | lang - Verwende "language:code" um nach Beiträgen in der gewünschten Sprache (im [ISO 639-1](https://en.wikipedia.org/wiki/ISO_639-1) format) zu suchen.
Denke daran, dass du diese Schlüsselwörter kombinieren kannst.
So kannst du zum Beispiel einen Kanal erstellen, mit allen Beiträgen, die über das Fediverse sprechen, aber nicht im Fediverse veröffentlich wurden, mit diesen Suchbegriffen: "fediverse -network:apub -network:dfrn".

View file

@ -1,7 +1,7 @@
Friendica - Dokumentation und Ressourcen Hilfe
===================================== =====
**Inhalte** **Dokumentation für Benutzer**
* Allgemeine Funktionen - Erste Schritte * Allgemeine Funktionen - Erste Schritte
* [Account - Basics](help/Account-Basics) * [Account - Basics](help/Account-Basics)
@ -17,7 +17,7 @@ Friendica - Dokumentation und Ressourcen
* [Circles und Privatsphäre](help/Circles-and-Privacy) * [Circles und Privatsphäre](help/Circles-and-Privacy)
* [Tags und Erwähnungen](help/Tags-and-Mentions) * [Tags und Erwähnungen](help/Tags-and-Mentions)
* [Community-Gruppen](help/Groups) * [Community-Gruppen](help/Groups)
* [Channels](help/Channels) * [Kanäle (Channels)](help/Channels)
* [Chats](help/Chats) * [Chats](help/Chats)
* Weiterführende Informationen * Weiterführende Informationen
* [Account umziehen](help/Move-Account) * [Account umziehen](help/Move-Account)
@ -60,18 +60,17 @@ Friendica - Dokumentation und Ressourcen
* [Translation of Friendica](help/translations) (EN) * [Translation of Friendica](help/translations) (EN)
* [Run tests](help/Tests) (EN) * [Run tests](help/Tests) (EN)
**Externe Ressourcen** **Links**
* [Haupt-Webseite](https://friendi.ca) * Website: [https://friendi.ca](https://friendi.ca)
* Support Kanäle * Help Group: [@helpers@forum.friendi.ca](https://forum.friendi.ca/~helpers)
* Friendica Support Gruppe: [@helpers@forum.friendi.ca](https://forum.friendi.ca/~helpers) * XMPP: [support@forum.friendi.ca](xmpp:support@forum.friendi.ca?join)
* [Mailing Listen Archiv](http://mailman.friendi.ca/mailman/listinfo/support-friendi.ca) zum Abonnieren der Liste eine E-Mail an ``support-request(at)friendi.ca?subject=subscribe`` senden * IRC: [https://web.libera.chat/?channels=#friendica](https://web.libera.chat/?channels=#friendica)
* Chats der Friendica Community (die IRC, Matrix und XMPP Räume sind mit einer Brücke verbunden) Logs dieser öffentlichen Chaträume können [hier aus dem IRC](https://gnusociarg.nsupdate.info/2021/%23frie) und [hier aus der Matrix](https://view.matrix.org/alias/%23friendi.ca:matrix.org/) gefunden werden. * Matrix: [https://matrix.to/#/#friendi.ca:matrix.org](https://matrix.to/#/#friendi.ca:matrix.org)
* XMPP/Jabber MUC: support(at)forum.friendi.ca * Mailing List: [https://mailman.friendi.ca/mailman/listinfo/support-friendi.ca](http://mailman.friendi.ca/mailman/listinfo/support-friendi.ca)
* IRC: #friendica auf [libera.chat](https://web.libera.chat/?channels=#friendica)
* Matrix: [#friendi.ca](https://matrix.to/#/#friendi.ca:matrix.org) oder [#friendica-en](https://matrix.to/#/#friendica-en:matrix.org) auf matrix.org
**Über diese Seite** **Über**
* [Seite/Friendica-Version](friendica) * [Server Information](friendica)
* [Mitwirkenden bei Friendica](credits) * [Nutzungsbedingungen](tos)
* [Mitwirkende](credits)

View file

@ -27,7 +27,7 @@ Requirements
* Apache mit einer aktiverten mod-rewrite-Funktion und dem Eintrag "Options All", so dass du die lokale .htaccess-Datei nutzen kannst * Apache mit einer aktiverten mod-rewrite-Funktion und dem Eintrag "Options All", so dass du die lokale .htaccess-Datei nutzen kannst
* PHP 7.4+ * PHP 7.4+
* PHP *Kommandozeilen*-Zugang mit register_argc_argv auf "true" gesetzt in der php.ini-Datei * PHP *Kommandozeilen*-Zugang mit register_argc_argv auf "true" gesetzt in der php.ini-Datei
* Curl, GD, GMP, PDO, mbstrings, MySQLi, hash, xml, zip, IntlChar and OpenSSL-Erweiterung * Curl, GD, GMP, PDO, mbstrings, MySQLi, hash, xml, zip, IntlChar, IDN und OpenSSL-Erweiterung
* Das POSIX Modul muss aktiviert sein ([CentOS, RHEL](http://www.bigsoft.co.uk/blog/index.php/2014/12/08/posix-php-commands-not-working-under-centos-7http://www.bigsoft.co.uk/blog/index.php/2014/12/08/posix-php-commands-not-working-under-centos-7) haben dies z.B. deaktiviert) * Das POSIX Modul muss aktiviert sein ([CentOS, RHEL](http://www.bigsoft.co.uk/blog/index.php/2014/12/08/posix-php-commands-not-working-under-centos-7http://www.bigsoft.co.uk/blog/index.php/2014/12/08/posix-php-commands-not-working-under-centos-7) haben dies z.B. deaktiviert)
* Einen E-Mail Server, so dass PHP `mail()` funktioniert. * Einen E-Mail Server, so dass PHP `mail()` funktioniert.
Wenn kein eigener E-Mail Server zur Verfügung steht, kann alternativ das [phpmailer](https://github.com/friendica/friendica-addons/tree/develop/phpmailer) Addon mit einem externen SMTP Account verwendet werden. Wenn kein eigener E-Mail Server zur Verfügung steht, kann alternativ das [phpmailer](https://github.com/friendica/friendica-addons/tree/develop/phpmailer) Addon mit einem externen SMTP Account verwendet werden.

View file

@ -78,3 +78,9 @@ The following will compress */var/log/friendica* (assuming this is the location
daily daily
rotate 2 rotate 2
} }
### Zabbix
To monitor the health status of your Friendica installation, you can use for example a tool like Zabbix. Please define 'stats_key' in your local.config.php in the 'system' section to be able to access the statistics page at /stats?key=your-defined-stats_key
The statistics contain data about the worker performance, the last cron call, number of reports, inbound and outbound packets, posts and comments.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 KiB

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 400 KiB

After

Width:  |  Height:  |  Size: 371 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1,011 KiB

After

Width:  |  Height:  |  Size: 707 KiB

View file

@ -45,7 +45,8 @@ use Friendica\Model\Post;
use Friendica\Network\HTTPException; use Friendica\Network\HTTPException;
use Friendica\Util\DateTimeFormat; use Friendica\Util\DateTimeFormat;
function item_post(App $a) { function item_post()
{
$uid = DI::userSession()->getLocalUserId(); $uid = DI::userSession()->getLocalUserId();
if (!$uid) { if (!$uid) {
@ -237,6 +238,8 @@ function item_process(array $post, array $request, bool $preview, string $return
$post['api_source'] = false; $post['api_source'] = false;
$post['attach'] = ''; $post['attach'] = '';
$post['title'] = trim($request['title'] ?? ''); $post['title'] = trim($request['title'] ?? '');
$post['content-warning'] = trim($request['summary'] ?? '');
$post['sensitive'] = !empty($request['sensitive'] ?? false);
$post['body'] = $request['body'] ?? ''; $post['body'] = $request['body'] ?? '';
$post['location'] = trim($request['location'] ?? ''); $post['location'] = trim($request['location'] ?? '');
$post['coord'] = trim($request['coord'] ?? ''); $post['coord'] = trim($request['coord'] ?? '');
@ -248,7 +251,7 @@ function item_process(array $post, array $request, bool $preview, string $return
$post['body'] .= DI::contentItem()->storeAttachmentFromRequest($request); $post['body'] .= DI::contentItem()->storeAttachmentFromRequest($request);
} }
$post = DI::contentItem()->finalizePost($post); $post = DI::contentItem()->finalizePost($post, $preview);
if (!strlen($post['body'])) { if (!strlen($post['body'])) {
if ($preview) { if ($preview) {
@ -279,6 +282,7 @@ function item_process(array $post, array $request, bool $preview, string $return
$post['body'] = BBCode::removeSharedData(Item::setHashtags($post['body'])); $post['body'] = BBCode::removeSharedData(Item::setHashtags($post['body']));
$post['writable'] = true; $post['writable'] = true;
$post['sensitive'] = false; $post['sensitive'] = false;
$post['post-reason'] = Item::PR_LOCAL;
$o = DI::conversation()->render([$post], Conversation::MODE_SEARCH, false, true); $o = DI::conversation()->render([$post], Conversation::MODE_SEARCH, false, true);

View file

@ -19,7 +19,6 @@
* *
*/ */
use Friendica\App;
use Friendica\Content\Nav; use Friendica\Content\Nav;
use Friendica\Content\Pager; use Friendica\Content\Pager;
use Friendica\Content\Text\BBCode; use Friendica\Content\Text\BBCode;
@ -34,7 +33,7 @@ use Friendica\Util\DateTimeFormat;
use Friendica\Util\Strings; use Friendica\Util\Strings;
use Friendica\Util\Temporal; use Friendica\Util\Temporal;
function message_init(App $a) function message_init()
{ {
$tabs = ''; $tabs = '';
@ -61,7 +60,7 @@ function message_init(App $a)
]); ]);
} }
function message_post(App $a) function message_post()
{ {
if (!DI::userSession()->getLocalUserId()) { if (!DI::userSession()->getLocalUserId()) {
DI::sysmsg()->addNotice(DI::l10n()->t('Permission denied.')); DI::sysmsg()->addNotice(DI::l10n()->t('Permission denied.'));
@ -104,7 +103,7 @@ function message_post(App $a)
} }
} }
function message_content(App $a) function message_content()
{ {
$o = ''; $o = '';
Nav::setSelected('messages'); Nav::setSelected('messages');
@ -114,7 +113,7 @@ function message_content(App $a)
return Login::form(); return Login::form();
} }
$myprofile = DI::baseUrl() . '/profile/' . $a->getLoggedInUserNickname(); $myprofile = DI::baseUrl() . '/profile/' . DI::userSession()->getLocalUserNickname();
$tpl = Renderer::getMarkupTemplate('mail_head.tpl'); $tpl = Renderer::getMarkupTemplate('mail_head.tpl');
if (DI::args()->getArgc() > 1 && DI::args()->getArgv()[1] == 'new') { if (DI::args()->getArgc() > 1 && DI::args()->getArgv()[1] == 'new') {
@ -177,7 +176,7 @@ function message_content(App $a)
$tpl = Renderer::getMarkupTemplate('msg-header.tpl'); $tpl = Renderer::getMarkupTemplate('msg-header.tpl');
DI::page()['htmlhead'] .= Renderer::replaceMacros($tpl, [ DI::page()['htmlhead'] .= Renderer::replaceMacros($tpl, [
'$nickname' => $a->getLoggedInUserNickname(), '$nickname' => DI::userSession()->getLocalUserNickname(),
'$linkurl' => DI::l10n()->t('Please enter a link URL:') '$linkurl' => DI::l10n()->t('Please enter a link URL:')
]); ]);
@ -282,7 +281,7 @@ function message_content(App $a)
$tpl = Renderer::getMarkupTemplate('msg-header.tpl'); $tpl = Renderer::getMarkupTemplate('msg-header.tpl');
DI::page()['htmlhead'] .= Renderer::replaceMacros($tpl, [ DI::page()['htmlhead'] .= Renderer::replaceMacros($tpl, [
'$nickname' => $a->getLoggedInUserNickname(), '$nickname' => DI::userSession()->getLocalUserNickname(),
'$linkurl' => DI::l10n()->t('Please enter a link URL:') '$linkurl' => DI::l10n()->t('Please enter a link URL:')
]); ]);
@ -415,12 +414,10 @@ function get_messages(int $uid, int $start, int $limit): array
function render_messages(array $msg, string $t): string function render_messages(array $msg, string $t): string
{ {
$a = DI::app();
$tpl = Renderer::getMarkupTemplate($t); $tpl = Renderer::getMarkupTemplate($t);
$rslt = ''; $rslt = '';
$myprofile = DI::baseUrl() . '/profile/' . $a->getLoggedInUserNickname(); $myprofile = DI::baseUrl() . '/profile/' . DI::userSession()->getLocalUserNickname();
foreach ($msg as $rr) { foreach ($msg as $rr) {
if ($rr['unknown']) { if ($rr['unknown']) {

View file

@ -46,7 +46,7 @@ function notes_content(App $a, bool $update = false)
return; return;
} }
$o = BaseProfile::getTabsHTML('notes', true, $a->getLoggedInUserNickname(), false); $o = BaseProfile::getTabsHTML('notes', true, DI::userSession()->getLocalUserNickname(), false);
if (!$update) { if (!$update) {
$o .= '<h3>' . DI::l10n()->t('Personal Notes') . '</h3>'; $o .= '<h3>' . DI::l10n()->t('Personal Notes') . '</h3>';

View file

@ -19,11 +19,9 @@
* *
*/ */
use Friendica\App;
use Friendica\Content\Nav; use Friendica\Content\Nav;
use Friendica\Content\Pager; use Friendica\Content\Pager;
use Friendica\Content\Text\BBCode; use Friendica\Content\Text\BBCode;
use Friendica\Content\Widget;
use Friendica\Core\ACL; use Friendica\Core\ACL;
use Friendica\Core\Addon; use Friendica\Core\Addon;
use Friendica\Core\Hook; use Friendica\Core\Hook;
@ -44,6 +42,7 @@ use Friendica\Module\BaseProfile;
use Friendica\Network\HTTPException; use Friendica\Network\HTTPException;
use Friendica\Network\Probe; use Friendica\Network\Probe;
use Friendica\Protocol\Activity; use Friendica\Protocol\Activity;
use Friendica\Protocol\ActivityNamespace;
use Friendica\Security\Security; use Friendica\Security\Security;
use Friendica\Util\Crypto; use Friendica\Util\Crypto;
use Friendica\Util\DateTimeFormat; use Friendica\Util\DateTimeFormat;
@ -53,7 +52,7 @@ use Friendica\Util\Strings;
use Friendica\Util\Temporal; use Friendica\Util\Temporal;
use Friendica\Util\XML; use Friendica\Util\XML;
function photos_init(App $a) function photos_init()
{ {
if (DI::config()->get('system', 'block_public') && !DI::userSession()->isAuthenticated()) { if (DI::config()->get('system', 'block_public') && !DI::userSession()->isAuthenticated()) {
return; return;
@ -67,8 +66,6 @@ function photos_init(App $a)
throw new HTTPException\NotFoundException(DI::l10n()->t('User not found.')); throw new HTTPException\NotFoundException(DI::l10n()->t('User not found.'));
} }
$is_owner = (DI::userSession()->getLocalUserId() && (DI::userSession()->getLocalUserId() == $owner['uid']));
$albums = Photo::getAlbums($owner['uid']); $albums = Photo::getAlbums($owner['uid']);
$albums_visible = ((intval($owner['hidewall']) && !DI::userSession()->isAuthenticated()) ? false : true); $albums_visible = ((intval($owner['hidewall']) && !DI::userSession()->isAuthenticated()) ? false : true);
@ -125,7 +122,7 @@ function photos_init(App $a)
return; return;
} }
function photos_post(App $a) function photos_post()
{ {
$user = User::getByNickname(DI::args()->getArgv()[1]); $user = User::getByNickname(DI::args()->getArgv()[1]);
if (!DBA::isResult($user)) { if (!DBA::isResult($user)) {
@ -136,7 +133,7 @@ function photos_post(App $a)
$visitor = 0; $visitor = 0;
$page_owner_uid = intval($user['uid']); $page_owner_uid = intval($user['uid']);
$community_page = $user['page-flags'] == User::PAGE_FLAGS_COMMUNITY; $community_page = in_array($user['page-flags'], [User::PAGE_FLAGS_COMMUNITY, User::PAGE_FLAGS_COMM_MAN]);
if (DI::userSession()->getLocalUserId() && (DI::userSession()->getLocalUserId() == $page_owner_uid)) { if (DI::userSession()->getLocalUserId() && (DI::userSession()->getLocalUserId() == $page_owner_uid)) {
$can_post = true; $can_post = true;
@ -200,7 +197,7 @@ function photos_post(App $a)
// Update the photo albums cache // Update the photo albums cache
Photo::clearAlbumCache($page_owner_uid); Photo::clearAlbumCache($page_owner_uid);
DI::baseUrl()->redirect('photos/' . $a->getLoggedInUserNickname() . '/album/' . bin2hex($newalbum)); DI::baseUrl()->redirect('photos/' . DI::userSession()->getLocalUserNickname() . '/album/' . bin2hex($newalbum));
return; // NOTREACHED return; // NOTREACHED
} }
@ -412,7 +409,7 @@ function photos_post(App $a)
if (count($links)) { if (count($links)) {
foreach ($links as $link) { foreach ($links as $link) {
if ($link['@attributes']['rel'] === 'http://webfinger.net/rel/profile-page') { if ($link['@attributes']['rel'] === ActivityNamespace::WEBFINGERPROFILE) {
$profile = $link['@attributes']['href']; $profile = $link['@attributes']['href'];
} }
@ -559,7 +556,7 @@ function photos_post(App $a)
} }
} }
function photos_content(App $a) function photos_content()
{ {
// URLs: // URLs:
// photos/name/upload // photos/name/upload
@ -618,7 +615,7 @@ function photos_content(App $a)
$owner_uid = $user['uid']; $owner_uid = $user['uid'];
$community_page = (($user['page-flags'] == User::PAGE_FLAGS_COMMUNITY) ? true : false); $community_page = in_array($user['page-flags'], [User::PAGE_FLAGS_COMMUNITY, User::PAGE_FLAGS_COMM_MAN]);
if (DI::userSession()->getLocalUserId() && (DI::userSession()->getLocalUserId() == $owner_uid)) { if (DI::userSession()->getLocalUserId() && (DI::userSession()->getLocalUserId() == $owner_uid)) {
$can_post = true; $can_post = true;
@ -672,18 +669,14 @@ function photos_content(App $a)
$selname = (!is_null($datum) && Strings::isHex($datum)) ? hex2bin($datum) : ''; $selname = (!is_null($datum) && Strings::isHex($datum)) ? hex2bin($datum) : '';
$albumselect = ''; $albumselect = ['' => '<current year>'];
$albumselect .= '<option value="" ' . (!$selname ? ' selected="selected" ' : '') . '>&lt;current year&gt;</option>'; foreach (Photo::getAlbums($owner_uid) as $album) {
$albums = Photo::getAlbums($owner_uid);
if (!empty($albums)) {
foreach ($albums as $album) {
if ($album['album'] === '') { if ($album['album'] === '') {
continue; continue;
} }
$selected = (($selname === $album['album']) ? ' selected="selected" ' : '');
$albumselect .= '<option value="' . $album['album'] . '"' . $selected . '>' . $album['album'] . '</option>'; $albumselect[$album['album']] = $album['album'];
}
} }
$uploader = ''; $uploader = '';
@ -718,7 +711,7 @@ function photos_content(App $a)
$tpl = Renderer::getMarkupTemplate('photos_upload.tpl'); $tpl = Renderer::getMarkupTemplate('photos_upload.tpl');
$aclselect_e = ($visitor ? '' : ACL::getFullSelectorHTML(DI::page(), $a->getLoggedInUserId())); $aclselect_e = ($visitor ? '' : ACL::getFullSelectorHTML(DI::page(), DI::userSession()->getLocalUserId()));
$o .= Renderer::replaceMacros($tpl, [ $o .= Renderer::replaceMacros($tpl, [
'$pagename' => DI::l10n()->t('Upload Photos'), '$pagename' => DI::l10n()->t('Upload Photos'),
@ -729,9 +722,10 @@ function photos_content(App $a)
'$existalbumtext' => DI::l10n()->t('or select existing album:'), '$existalbumtext' => DI::l10n()->t('or select existing album:'),
'$nosharetext' => DI::l10n()->t('Do not show a status post for this upload'), '$nosharetext' => DI::l10n()->t('Do not show a status post for this upload'),
'$albumselect' => $albumselect, '$albumselect' => $albumselect,
'$selname' => $selname,
'$permissions' => DI::l10n()->t('Permissions'), '$permissions' => DI::l10n()->t('Permissions'),
'$aclselect' => $aclselect_e, '$aclselect' => $aclselect_e,
'$lockstate' => ACL::getLockstateForUserId($a->getLoggedInUserId()) ? 'lock' : 'unlock', '$lockstate' => ACL::getLockstateForUserId(DI::userSession()->getLocalUserId()) ? 'lock' : 'unlock',
'$alt_uploader' => $ret['addon_text'], '$alt_uploader' => $ret['addon_text'],
'$default_upload_box' => ($ret['default_upload'] ? $default_upload_box : ''), '$default_upload_box' => ($ret['default_upload'] ? $default_upload_box : ''),
'$default_upload_submit' => ($ret['default_upload'] ? $default_upload_submit : ''), '$default_upload_submit' => ($ret['default_upload'] ? $default_upload_submit : ''),
@ -1077,7 +1071,7 @@ function photos_content(App $a)
$album_e = $ph[0]['album']; $album_e = $ph[0]['album'];
$caption_e = $ph[0]['desc']; $caption_e = $ph[0]['desc'];
$aclselect_e = ACL::getFullSelectorHTML(DI::page(), $a->getLoggedInUserId(), false, ACL::getDefaultUserPermissions($ph[0])); $aclselect_e = ACL::getFullSelectorHTML(DI::page(), DI::userSession()->getLocalUserId(), false, ACL::getDefaultUserPermissions($ph[0]));
$edit = Renderer::replaceMacros($edit_tpl, [ $edit = Renderer::replaceMacros($edit_tpl, [
'$id' => $ph[0]['id'], '$id' => $ph[0]['id'],

View file

@ -24,6 +24,7 @@ namespace Friendica;
use Exception; use Exception;
use Friendica\App\Arguments; use Friendica\App\Arguments;
use Friendica\App\BaseURL; use Friendica\App\BaseURL;
use Friendica\App\Request;
use Friendica\Capabilities\ICanCreateResponses; use Friendica\Capabilities\ICanCreateResponses;
use Friendica\Content\Nav; use Friendica\Content\Nav;
use Friendica\Core\Config\Factory\Config; use Friendica\Core\Config\Factory\Config;
@ -39,10 +40,10 @@ use Friendica\Core\L10n;
use Friendica\Core\System; use Friendica\Core\System;
use Friendica\Core\Theme; use Friendica\Core\Theme;
use Friendica\Database\Database; use Friendica\Database\Database;
use Friendica\Model\Contact;
use Friendica\Model\Profile;
use Friendica\Module\Special\HTTPException as ModuleHTTPException; use Friendica\Module\Special\HTTPException as ModuleHTTPException;
use Friendica\Network\HTTPException; use Friendica\Network\HTTPException;
use Friendica\Protocol\ATProtocol\DID;
use Friendica\Security\OpenWebAuth;
use Friendica\Util\DateTimeFormat; use Friendica\Util\DateTimeFormat;
use Friendica\Util\HTTPInputData; use Friendica\Util\HTTPInputData;
use Friendica\Util\HTTPSignature; use Friendica\Util\HTTPSignature;
@ -64,7 +65,7 @@ class App
{ {
const PLATFORM = 'Friendica'; const PLATFORM = 'Friendica';
const CODENAME = 'Yellow Archangel'; const CODENAME = 'Yellow Archangel';
const VERSION = '2024.03'; const VERSION = '2024.06-rc';
// Allow themes to control internal parameters // Allow themes to control internal parameters
// by changing App values in theme.php // by changing App values in theme.php
@ -93,6 +94,12 @@ class App
/** @var string The name of the current mobile theme */ /** @var string The name of the current mobile theme */
private $currentMobileTheme; private $currentMobileTheme;
/** @var string */
private $requestId;
/** @var Authentication */
private $auth;
/** /**
* @var IManageConfigValues The config * @var IManageConfigValues The config
*/ */
@ -133,42 +140,6 @@ class App
*/ */
private $session; private $session;
/**
* @deprecated 2022.03
* @see IHandleUserSessions::isAuthenticated()
*/
public function isLoggedIn(): bool
{
return $this->session->isAuthenticated();
}
/**
* @deprecated 2022.03
* @see IHandleUserSessions::isSiteAdmin()
*/
public function isSiteAdmin(): bool
{
return $this->session->isSiteAdmin();
}
/**
* @deprecated 2022.03
* @see IHandleUserSessions::getLocalUserId()
*/
public function getLoggedInUserId(): int
{
return $this->session->getLocalUserId();
}
/**
* @deprecated 2022.03
* @see IHandleUserSessions::getLocalUserNickname()
*/
public function getLoggedInUserNickname(): string
{
return $this->session->getLocalUserNickname();
}
/** /**
* Set the profile owner ID * Set the profile owner ID
* *
@ -314,8 +285,10 @@ class App
* @param DbaDefinition $dbaDefinition * @param DbaDefinition $dbaDefinition
* @param ViewDefinition $viewDefinition * @param ViewDefinition $viewDefinition
*/ */
public function __construct(Database $database, IManageConfigValues $config, App\Mode $mode, BaseURL $baseURL, LoggerInterface $logger, Profiler $profiler, L10n $l10n, Arguments $args, IManagePersonalConfigValues $pConfig, IHandleUserSessions $session, DbaDefinition $dbaDefinition, ViewDefinition $viewDefinition) public function __construct(Request $request, Authentication $auth, Database $database, IManageConfigValues $config, App\Mode $mode, BaseURL $baseURL, LoggerInterface $logger, Profiler $profiler, L10n $l10n, Arguments $args, IManagePersonalConfigValues $pConfig, IHandleUserSessions $session, DbaDefinition $dbaDefinition, ViewDefinition $viewDefinition)
{ {
$this->requestId = $request->getRequestId();
$this->auth = $auth;
$this->database = $database; $this->database = $database;
$this->config = $config; $this->config = $config;
$this->mode = $mode; $this->mode = $mode;
@ -565,8 +538,8 @@ class App
*/ */
public function runFrontend(App\Router $router, IManagePersonalConfigValues $pconfig, Authentication $auth, App\Page $page, Nav $nav, ModuleHTTPException $httpException, HTTPInputData $httpInput, float $start_time, array $server) public function runFrontend(App\Router $router, IManagePersonalConfigValues $pconfig, Authentication $auth, App\Page $page, Nav $nav, ModuleHTTPException $httpException, HTTPInputData $httpInput, float $start_time, array $server)
{ {
$requeststring = ($_SERVER['REQUEST_METHOD'] ?? '') . ' ' . ($_SERVER['REQUEST_URI'] ?? '') . ' ' . ($_SERVER['SERVER_PROTOCOL'] ?? ''); $requeststring = ($server['REQUEST_METHOD'] ?? '') . ' ' . ($server['REQUEST_URI'] ?? '') . ' ' . ($server['SERVER_PROTOCOL'] ?? '');
$this->logger->debug('Request received', ['address' => $_SERVER['REMOTE_ADDR'] ?? '', 'request' => $requeststring, 'referer' => $_SERVER['HTTP_REFERER'] ?? '', 'user-agent' => $_SERVER['HTTP_USER_AGENT'] ?? '']); $this->logger->debug('Request received', ['address' => $server['REMOTE_ADDR'] ?? '', 'request' => $requeststring, 'referer' => $server['HTTP_REFERER'] ?? '', 'user-agent' => $server['HTTP_USER_AGENT'] ?? '']);
$request_start = microtime(true); $request_start = microtime(true);
$this->profiler->set($start_time, 'start'); $this->profiler->set($start_time, 'start');
@ -593,10 +566,12 @@ class App
Core\Hook::callAll('init_1'); Core\Hook::callAll('init_1');
} }
DID::routeRequest($this->args->getCommand(), $server);
if ($this->mode->isNormal() && !$this->mode->isBackend()) { if ($this->mode->isNormal() && !$this->mode->isBackend()) {
$requester = HTTPSignature::getSigner('', $_SERVER); $requester = HTTPSignature::getSigner('', $server);
if (!empty($requester)) { if (!empty($requester)) {
Profile::addVisitorCookieForHandle($requester); OpenWebAuth::addVisitorCookieForHandle($requester);
} }
} }
@ -606,17 +581,8 @@ class App
// Valid profile links contain a path with "/profile/" and no query parameters // Valid profile links contain a path with "/profile/" and no query parameters
if ((parse_url($_GET['zrl'], PHP_URL_QUERY) == '') && if ((parse_url($_GET['zrl'], PHP_URL_QUERY) == '') &&
strpos(parse_url($_GET['zrl'], PHP_URL_PATH) ?? '', '/profile/') !== false) { strpos(parse_url($_GET['zrl'], PHP_URL_PATH) ?? '', '/profile/') !== false) {
if ($this->session->get('visitor_home') != $_GET['zrl']) { $this->auth->setUnauthenticatedVisitor($_GET['zrl']);
$this->session->set('my_url', $_GET['zrl']); OpenWebAuth::zrlInit();
$this->session->set('authenticated', 0);
$remote_contact = Contact::getByURL($_GET['zrl'], false, ['subscribe']);
if (!empty($remote_contact['subscribe'])) {
$_SESSION['remote_comment'] = $remote_contact['subscribe'];
}
}
Model\Profile::zrlInit($this);
} else { } else {
// Someone came with an invalid parameter, maybe as a DDoS attempt // Someone came with an invalid parameter, maybe as a DDoS attempt
// We simply stop processing here // We simply stop processing here
@ -627,14 +593,14 @@ class App
if (!empty($_GET['owt']) && $this->mode->isNormal()) { if (!empty($_GET['owt']) && $this->mode->isNormal()) {
$token = $_GET['owt']; $token = $_GET['owt'];
Model\Profile::openWebAuthInit($token); OpenWebAuth::init($token);
} }
if (!$this->mode->isBackend()) { if (!$this->mode->isBackend()) {
$auth->withSession($this); $auth->withSession($this);
} }
if (empty($_SESSION['authenticated'])) { if ($this->session->isUnauthenticated()) {
header('X-Account-Management-Status: none'); header('X-Account-Management-Status: none');
} }
@ -654,6 +620,11 @@ class App
Core\Hook::loadHooks(); Core\Hook::loadHooks();
} }
// Compatibility with Hubzilla
if ($moduleName == 'rpost') {
$this->baseURL->redirect('compose');
}
// Compatibility with the Android Diaspora client // Compatibility with the Android Diaspora client
if ($moduleName == 'stream') { if ($moduleName == 'stream') {
$this->baseURL->redirect('network?order=post'); $this->baseURL->redirect('network?order=post');
@ -713,13 +684,15 @@ class App
// Wrapping HTML responses in the theme template // Wrapping HTML responses in the theme template
if ($response->getHeaderLine(ICanCreateResponses::X_HEADER) === ICanCreateResponses::TYPE_HTML) { if ($response->getHeaderLine(ICanCreateResponses::X_HEADER) === ICanCreateResponses::TYPE_HTML) {
$response = $page->run($this, $this->baseURL, $this->args, $this->mode, $response, $this->l10n, $this->profiler, $this->config, $pconfig, $nav, $this->session->getLocalUserId()); $response = $page->run($this, $this->session, $this->baseURL, $this->args, $this->mode, $response, $this->l10n, $this->profiler, $this->config, $pconfig, $nav, $this->session->getLocalUserId());
} }
$this->logger->debug('Request processed sucessfully', ['response' => $response->getStatusCode(), 'address' => $_SERVER['REMOTE_ADDR'] ?? '', 'request' => $requeststring, 'referer' => $_SERVER['HTTP_REFERER'] ?? '', 'user-agent' => $_SERVER['HTTP_USER_AGENT'] ?? '', 'duration' => number_format(microtime(true) - $request_start, 3)]); $this->logger->debug('Request processed sucessfully', ['response' => $response->getStatusCode(), 'address' => $server['REMOTE_ADDR'] ?? '', 'request' => $requeststring, 'referer' => $server['HTTP_REFERER'] ?? '', 'user-agent' => $server['HTTP_USER_AGENT'] ?? '', 'duration' => number_format(microtime(true) - $request_start, 3)]);
$this->logSlowCalls(microtime(true) - $request_start, $response->getStatusCode(), $requeststring, $server['HTTP_USER_AGENT'] ?? '');
System::echoResponse($response); System::echoResponse($response);
} catch (HTTPException $e) { } catch (HTTPException $e) {
$this->logger->debug('Request processed with exception', ['response' => $e->getCode(), 'address' => $_SERVER['REMOTE_ADDR'] ?? '', 'request' => $requeststring, 'referer' => $_SERVER['HTTP_REFERER'] ?? '', 'user-agent' => $_SERVER['HTTP_USER_AGENT'] ?? '', 'duration' => number_format(microtime(true) - $request_start, 3)]); $this->logger->debug('Request processed with exception', ['response' => $e->getCode(), 'address' => $server['REMOTE_ADDR'] ?? '', 'request' => $requeststring, 'referer' => $server['HTTP_REFERER'] ?? '', 'user-agent' => $server['HTTP_USER_AGENT'] ?? '', 'duration' => number_format(microtime(true) - $request_start, 3)]);
$this->logSlowCalls(microtime(true) - $request_start, $e->getCode(), $requeststring, $server['HTTP_USER_AGENT'] ?? '');
$httpException->rawContent($e); $httpException->rawContent($e);
} }
$page->logRuntime($this->config, 'runFrontend'); $page->logRuntime($this->config, 'runFrontend');
@ -741,4 +714,30 @@ class App
$this->baseURL->redirect($toUrl); $this->baseURL->redirect($toUrl);
} }
} }
/**
* Log slow page executions
*
* @param float $duration
* @param integer $code
* @param string $request
* @param string $agent
* @return void
*/
private function logSlowCalls(float $duration, int $code, string $request, string $agent)
{
$logfile = $this->config->get('system', 'page_execution_logfile');
$loglimit = $this->config->get('system', 'page_execution_log_limit');
if (empty($logfile) || empty($loglimit) || ($duration < $loglimit)) {
return;
}
@file_put_contents(
$logfile,
DateTimeFormat::utcNow() . "\t" . round($duration, 3) . "\t" .
$this->requestId . "\t" . $code . "\t" .
$request . "\t" . $agent . "\n",
FILE_APPEND
);
}
} }

View file

@ -32,6 +32,7 @@ use Friendica\Core\L10n;
use Friendica\Core\Logger; use Friendica\Core\Logger;
use Friendica\Core\PConfig\Capability\IManagePersonalConfigValues; use Friendica\Core\PConfig\Capability\IManagePersonalConfigValues;
use Friendica\Core\Renderer; use Friendica\Core\Renderer;
use Friendica\Core\Session\Model\UserSession;
use Friendica\Core\System; use Friendica\Core\System;
use Friendica\Core\Theme; use Friendica\Core\Theme;
use Friendica\Module\Response; use Friendica\Module\Response;
@ -325,13 +326,13 @@ class Page implements ArrayAccess
* *
* @throws HTTPException\InternalServerErrorException * @throws HTTPException\InternalServerErrorException
*/ */
private function initFooter(App $app, Mode $mode, L10n $l10n) private function initFooter(UserSession $session, Mode $mode, L10n $l10n)
{ {
// If you're just visiting, let javascript take you home // If you're just visiting, let javascript take you home
if (!empty($_SESSION['visitor_home'])) { if (!empty($_SESSION['visitor_home'])) {
$homebase = $_SESSION['visitor_home']; $homebase = $_SESSION['visitor_home'];
} elseif (!empty($app->getLoggedInUserNickname())) { } elseif (!empty($session->getLocalUserNickname())) {
$homebase = 'profile/' . $app->getLoggedInUserNickname(); $homebase = 'profile/' . $session->getLocalUserNickname();
} }
if (isset($homebase)) { if (isset($homebase)) {
@ -420,7 +421,7 @@ class Page implements ArrayAccess
* @throws HTTPException\InternalServerErrorException * @throws HTTPException\InternalServerErrorException
* @throws HTTPException\ServiceUnavailableException * @throws HTTPException\ServiceUnavailableException
*/ */
public function run(App $app, BaseURL $baseURL, Arguments $args, Mode $mode, ResponseInterface $response, L10n $l10n, Profiler $profiler, IManageConfigValues $config, IManagePersonalConfigValues $pconfig, Nav $nav, int $localUID) public function run(App $app, UserSession $session, BaseURL $baseURL, Arguments $args, Mode $mode, ResponseInterface $response, L10n $l10n, Profiler $profiler, IManageConfigValues $config, IManagePersonalConfigValues $pconfig, Nav $nav, int $localUID)
{ {
$moduleName = $args->getModuleName(); $moduleName = $args->getModuleName();
@ -459,7 +460,7 @@ class Page implements ArrayAccess
/* Build the page ending -- this is stuff that goes right before /* Build the page ending -- this is stuff that goes right before
* the closing </body> tag * the closing </body> tag
*/ */
$this->initFooter($app, $mode, $l10n); $this->initFooter($session, $mode, $l10n);
$profiler->set(microtime(true) - $timestamp, 'aftermath'); $profiler->set(microtime(true) - $timestamp, 'aftermath');

View file

@ -224,17 +224,17 @@ abstract class BaseModule implements ICanHandleRequests
switch ($this->args->getMethod()) { switch ($this->args->getMethod()) {
case Router::DELETE: case Router::DELETE:
$this->delete($request); $this->delete($request);
break; return $this->response->generate();
case Router::PATCH: case Router::PATCH:
$this->patch($request); $this->patch($request);
break; return $this->response->generate();
case Router::POST: case Router::POST:
Core\Hook::callAll($this->args->getModuleName() . '_mod_post', $request); Core\Hook::callAll($this->args->getModuleName() . '_mod_post', $request);
$this->post($request); $this->post($request);
break; return $this->response->generate();
case Router::PUT: case Router::PUT:
$this->put($request); $this->put($request);
break; return $this->response->generate();
} }
$timestamp = microtime(true); $timestamp = microtime(true);
@ -356,7 +356,7 @@ abstract class BaseModule implements ICanHandleRequests
*/ */
public static function getFormSecurityToken(string $typename = ''): string public static function getFormSecurityToken(string $typename = ''): string
{ {
$user = User::getById(DI::app()->getLoggedInUserId(), ['guid', 'prvkey']); $user = User::getById(DI::userSession()->getLocalUserId(), ['guid', 'prvkey']);
$timestamp = time(); $timestamp = time();
$sec_hash = hash('whirlpool', ($user['guid'] ?? '') . ($user['prvkey'] ?? '') . session_id() . $timestamp . $typename); $sec_hash = hash('whirlpool', ($user['guid'] ?? '') . ($user['prvkey'] ?? '') . session_id() . $timestamp . $typename);
@ -390,7 +390,7 @@ abstract class BaseModule implements ICanHandleRequests
$max_livetime = 10800; // 3 hours $max_livetime = 10800; // 3 hours
$user = User::getById(DI::app()->getLoggedInUserId(), ['guid', 'prvkey']); $user = User::getById(DI::userSession()->getLocalUserId(), ['guid', 'prvkey']);
$x = explode('.', $hash); $x = explode('.', $hash);
if (time() > (intval($x[0]) + $max_livetime)) { if (time() > (intval($x[0]) + $max_livetime)) {
@ -410,7 +410,7 @@ abstract class BaseModule implements ICanHandleRequests
public static function checkFormSecurityTokenRedirectOnError(string $err_redirect, string $typename = '', string $formname = 'form_security_token') public static function checkFormSecurityTokenRedirectOnError(string $err_redirect, string $typename = '', string $formname = 'form_security_token')
{ {
if (!self::checkFormSecurityToken($typename, $formname)) { if (!self::checkFormSecurityToken($typename, $formname)) {
Logger::notice('checkFormSecurityToken failed: user ' . DI::app()->getLoggedInUserNickname() . ' - form element ' . $typename); Logger::notice('checkFormSecurityToken failed: user ' . DI::userSession()->getLocalUserNickname() . ' - form element ' . $typename);
Logger::debug('checkFormSecurityToken failed', ['request' => $_REQUEST]); Logger::debug('checkFormSecurityToken failed', ['request' => $_REQUEST]);
DI::sysmsg()->addNotice(self::getFormSecurityStandardErrorMessage()); DI::sysmsg()->addNotice(self::getFormSecurityStandardErrorMessage());
DI::baseUrl()->redirect($err_redirect); DI::baseUrl()->redirect($err_redirect);
@ -420,7 +420,7 @@ abstract class BaseModule implements ICanHandleRequests
public static function checkFormSecurityTokenForbiddenOnError(string $typename = '', string $formname = 'form_security_token') public static function checkFormSecurityTokenForbiddenOnError(string $typename = '', string $formname = 'form_security_token')
{ {
if (!self::checkFormSecurityToken($typename, $formname)) { if (!self::checkFormSecurityToken($typename, $formname)) {
Logger::notice('checkFormSecurityToken failed: user ' . DI::app()->getLoggedInUserNickname() . ' - form element ' . $typename); Logger::notice('checkFormSecurityToken failed: user ' . DI::userSession()->getLocalUserNickname() . ' - form element ' . $typename);
Logger::debug('checkFormSecurityToken failed', ['request' => $_REQUEST]); Logger::debug('checkFormSecurityToken failed', ['request' => $_REQUEST]);
throw new \Friendica\Network\HTTPException\ForbiddenException(); throw new \Friendica\Network\HTTPException\ForbiddenException();

View file

@ -0,0 +1,119 @@
<?php
/**
* @copyright Copyright (C) 2010-2024, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Console;
use Friendica\App\BaseURL;
use Friendica\Contact\Avatar;
use Friendica\Core\L10n;
use Friendica\Model\Contact;
use Friendica\Core\Config\Capability\IManageConfigValues;
/**
* tool to clear the avatar file cache.
*/
class ClearAvatarCache extends \Asika\SimpleConsole\Console
{
protected $helpOptions = ['h', 'help', '?'];
/**
* @var $dba Friendica\Database\Database
*/
private $dba;
/**
* @var $baseurl Friendica\App\BaseURL
*/
private $baseUrl;
/**
* @var L10n
*/
private $l10n;
/**
* @var IManageConfigValues
*/
private $config;
protected function getHelp()
{
$help = <<<HELP
console clearavatarcache - Clear the file based avatar cache
Synopsis
bin/console clearavatarcache
Description
bin/console clearavatarcache
Clear the file based avatar cache
Options
-h|--help|-? Show help information
HELP;
return $help;
}
public function __construct(\Friendica\Database\Database $dba, BaseURL $baseUrl, L10n $l10n, IManageConfigValues $config, array $argv = null)
{
parent::__construct($argv);
$this->dba = $dba;
$this->baseUrl = $baseUrl;
$this->l10n = $l10n;
$this->config = $config;
}
protected function doExecute(): int
{
if ($this->config->get('system', 'avatar_cache')) {
$this->err($this->l10n->t('The avatar cache needs to be disabled in local.config.php to use this command.'));
return 2;
}
// Contacts (but not self contacts) with cached avatars.
$condition = ["NOT `self` AND (`photo` != ? OR `thumb` != ? OR `micro` != ?)", '', '', ''];
$total = $this->dba->count('contact', $condition);
$count = 0;
$contacts = $this->dba->select('contact', ['id', 'uri-id', 'url', 'uid', 'photo', 'thumb', 'micro'], $condition);
while ($contact = $this->dba->fetch($contacts)) {
if (Avatar::deleteCache($contact) || $this->isAvatarCache($contact)) {
Contact::update(['photo' => '', 'thumb' => '', 'micro' => ''], ['id' => $contact['id']]);
}
$this->out(++$count . '/' . $total . "\t" . $contact['id'] . "\t" . $contact['url'] . "\t" . $contact['photo']);
}
$this->dba->close($contacts);
return 0;
}
private function isAvatarCache(array $contact): bool
{
if (!empty($contact['photo']) && strpos($contact['photo'], Avatar::baseUrl()) === 0) {
return true;
}
if (!empty($contact['thumb']) && strpos($contact['thumb'], Avatar::baseUrl()) === 0) {
return true;
}
if (!empty($contact['micro']) && strpos($contact['micro'], Avatar::baseUrl()) === 0) {
return true;
}
return false;
}
}

View file

@ -25,6 +25,9 @@ use Asika\SimpleConsole\CommandArgsException;
use Friendica\Core\Storage\Repository\StorageManager; use Friendica\Core\Storage\Repository\StorageManager;
use Friendica\Core\Storage\Exception\ReferenceStorageException; use Friendica\Core\Storage\Exception\ReferenceStorageException;
use Friendica\Core\Storage\Exception\StorageException; use Friendica\Core\Storage\Exception\StorageException;
use Friendica\Database\DBA;
use Friendica\Model\Contact;
use Friendica\Model\Photo;
/** /**
* tool to manage storage backend and stored data from CLI * tool to manage storage backend and stored data from CLI
@ -57,6 +60,9 @@ Synopsis
bin/console storage list bin/console storage list
List available storage backends List available storage backends
bin/console storage clear
Remove the contact avatar cache data
bin/console storage set <name> bin/console storage set <name>
Set current storage backend Set current storage backend
name storage backend to use. see "list". name storage backend to use. see "list".
@ -87,6 +93,9 @@ HELP;
case 'list': case 'list':
return $this->doList(); return $this->doList();
break; break;
case 'clear':
return $this->clear();
break;
case 'set': case 'set':
return $this->doSet(); return $this->doSet();
break; break;
@ -126,6 +135,22 @@ HELP;
return 0; return 0;
} }
protected function clear()
{
$fields = ['photo' => '', 'thumb' => '', 'micro' => ''];
$photos = DBA::select('photo', ['id', 'contact-id'], ['uid' => 0, 'photo-type' => [Photo::CONTACT_AVATAR, Photo::CONTACT_BANNER]]);
while ($photo = DBA::fetch($photos)) {
if (Photo::delete(['id' => $photo['id']])) {
Contact::update($fields, ['id' => $photo['contact-id']]);
$this->out('Cleared photo id ' . $photo['id'] . ' - contact id ' . $photo['contact-id']);
} else {
$this->out('Photo id ' . $photo['id'] . ' was not deleted.');
}
}
DBA::close($photos);
return 0;
}
protected function doSet() protected function doSet()
{ {
if (count($this->args) !== 2 || empty($this->args[1])) { if (count($this->args) !== 2 || empty($this->args[1])) {

View file

@ -23,14 +23,11 @@ namespace Friendica\Contact;
use Friendica\Core\Logger; use Friendica\Core\Logger;
use Friendica\DI; use Friendica\DI;
use Friendica\Model\Item;
use Friendica\Network\HTTPClient\Client\HttpClientAccept; use Friendica\Network\HTTPClient\Client\HttpClientAccept;
use Friendica\Network\HTTPClient\Client\HttpClientOptions; use Friendica\Network\HTTPClient\Client\HttpClientOptions;
use Friendica\Object\Image; use Friendica\Object\Image;
use Friendica\Util\DateTimeFormat; use Friendica\Util\DateTimeFormat;
use Friendica\Util\HTTPSignature; use Friendica\Util\HTTPSignature;
use Friendica\Util\Images;
use Friendica\Util\Network;
use Friendica\Util\Proxy; use Friendica\Util\Proxy;
/** /**
@ -57,7 +54,7 @@ class Avatar
return $fields; return $fields;
} }
if (Network::isLocalLink($avatar) || empty($avatar)) { if (DI::baseUrl()->isLocalUrl($avatar) || empty($avatar)) {
self::deleteCache($contact); self::deleteCache($contact);
return $fields; return $fields;
} }
@ -97,7 +94,7 @@ class Avatar
return $fields; return $fields;
} }
$filename = self::getFilename($contact['url'], $avatar); $filename = self::getFilename($contact['url']);
$timestamp = time(); $timestamp = time();
$fields['blurhash'] = $image->getBlurHash(); $fields['blurhash'] = $image->getBlurHash();
@ -120,12 +117,12 @@ class Avatar
return $fields; return $fields;
} }
if (Network::isLocalLink($contact['avatar']) || empty($contact['avatar'])) { if (DI::baseUrl()->isLocalUrl($contact['avatar']) || empty($contact['avatar'])) {
self::deleteCache($contact); self::deleteCache($contact);
return $fields; return $fields;
} }
$filename = self::getFilename($contact['url'], $contact['avatar']); $filename = self::getFilename($contact['url']);
$timestamp = time(); $timestamp = time();
$fields['photo'] = self::storeAvatarCache($image, $filename, Proxy::PIXEL_SMALL, $timestamp); $fields['photo'] = self::storeAvatarCache($image, $filename, Proxy::PIXEL_SMALL, $timestamp);
@ -135,12 +132,10 @@ class Avatar
return $fields; return $fields;
} }
private static function getFilename(string $url, string $host): string private static function getFilename(string $url): string
{ {
$guid = Item::guidFromUri($url, $host); $guid = hash('ripemd128', $url);
return substr($guid, 0, 3) . '/' . substr($guid, 4) . '-';
return substr($guid, 0, 2) . '/' . substr($guid, 3, 2) . '/' . substr($guid, 5, 3) . '/' .
substr($guid, 9, 2) .'/' . substr($guid, 11, 2) . '/' . substr($guid, 13, 4). '/' . substr($guid, 18) . '-';
} }
private static function storeAvatarCache(Image $image, string $filename, int $size, int $timestamp): string private static function storeAvatarCache(Image $image, string $filename, int $size, int $timestamp): string
@ -279,7 +274,7 @@ class Avatar
$localFile = self::getCacheFile($avatar); $localFile = self::getCacheFile($avatar);
if (!empty($localFile)) { if (!empty($localFile)) {
@unlink($localFile); @unlink($localFile);
Logger::debug('Unlink avatar', ['avatar' => $avatar]); Logger::debug('Unlink avatar', ['avatar' => $avatar, 'local' => $localFile]);
} }
} }
@ -316,7 +311,7 @@ class Avatar
* *
* @return string * @return string
*/ */
private static function baseUrl(): string public static function baseUrl(): string
{ {
$baseurl = DI::config()->get('system', 'avatar_cache_url'); $baseurl = DI::config()->get('system', 'avatar_cache_url');
if (!empty($baseurl)) { if (!empty($baseurl)) {

View file

@ -23,8 +23,8 @@ namespace Friendica\Content;
use Friendica\Core\L10n; use Friendica\Core\L10n;
use Friendica\Core\Renderer; use Friendica\Core\Renderer;
use Friendica\Util\Network;
use Friendica\Util\Strings; use Friendica\Util\Strings;
use GuzzleHttp\Psr7\Uri;
/** /**
* This pager should be used by lists using the min_id†/max_id† parameters * This pager should be used by lists using the min_id†/max_id† parameters
@ -67,7 +67,7 @@ class BoundariesPager extends Pager
$parsed['query'] = http_build_query($queryParameters); $parsed['query'] = http_build_query($queryParameters);
$url = Network::unparseURL($parsed); $url = (string)Uri::fromParts((array)$parsed);
$this->setQueryString($url); $this->setQueryString($url);
} }

View file

@ -25,8 +25,8 @@ use Friendica\Core\Hook;
use Friendica\Core\Protocol; use Friendica\Core\Protocol;
use Friendica\Database\DBA; use Friendica\Database\DBA;
use Friendica\DI; use Friendica\DI;
use Friendica\Util\Network;
use Friendica\Util\Strings; use Friendica\Util\Strings;
use GuzzleHttp\Psr7\Uri;
/** /**
* ContactSelector class * ContactSelector class
@ -102,7 +102,7 @@ class ContactSelector
// Create the server url out of the profile url // Create the server url out of the profile url
$parts = parse_url($profile); $parts = parse_url($profile);
unset($parts['path']); unset($parts['path']);
$server_url = Strings::normaliseLink(Network::unparseURL($parts)); $server_url = Strings::normaliseLink((string)Uri::fromParts((array)$parts));
} }
self::$server_url[$profile] = $server_url; self::$server_url[$profile] = $server_url;

View file

@ -308,7 +308,7 @@ class Conversation
public function statusEditor(array $x = [], int $notes_cid = 0, bool $popup = false): string public function statusEditor(array $x = [], int $notes_cid = 0, bool $popup = false): string
{ {
$user = User::getById($this->app->getLoggedInUserId(), ['uid', 'nickname', 'allow_location', 'default-location']); $user = User::getById($this->session->getLocalUserId(), ['uid', 'nickname', 'allow_location', 'default-location']);
if (empty($user['uid'])) { if (empty($user['uid'])) {
return ''; return '';
} }
@ -332,7 +332,6 @@ class Conversation
$tpl = Renderer::getMarkupTemplate('jot-header.tpl'); $tpl = Renderer::getMarkupTemplate('jot-header.tpl');
$this->page['htmlhead'] .= Renderer::replaceMacros($tpl, [ $this->page['htmlhead'] .= Renderer::replaceMacros($tpl, [
'$newpost' => 'true', '$newpost' => 'true',
'$baseurl' => $this->baseURL,
'$geotag' => $geotag, '$geotag' => $geotag,
'$nickname' => $x['nickname'], '$nickname' => $x['nickname'],
'$ispublic' => $this->l10n->t('Visible to <strong>everybody</strong>'), '$ispublic' => $this->l10n->t('Visible to <strong>everybody</strong>'),
@ -389,7 +388,7 @@ class Conversation
'$title' => $x['title'] ?? '', '$title' => $x['title'] ?? '',
'$placeholdertitle' => $this->l10n->t('Set title'), '$placeholdertitle' => $this->l10n->t('Set title'),
'$category' => $x['category'] ?? '', '$category' => $x['category'] ?? '',
'$placeholdercategory' => Feature::isEnabled($this->session->getLocalUserId(), 'categories') ? $this->l10n->t("Categories \x28comma-separated list\x29") : '', '$placeholdercategory' => Feature::isEnabled($this->session->getLocalUserId(), Feature::CATEGORIES) ? $this->l10n->t("Categories \x28comma-separated list\x29") : '',
'$scheduled_at' => Temporal::getDateTimeField( '$scheduled_at' => Temporal::getDateTimeField(
new \DateTime(), new \DateTime(),
new \DateTime('now + 6 months'), new \DateTime('now + 6 months'),
@ -405,7 +404,6 @@ class Conversation
'$posttype' => $notes_cid ? ItemModel::PT_PERSONAL_NOTE : ItemModel::PT_ARTICLE, '$posttype' => $notes_cid ? ItemModel::PT_PERSONAL_NOTE : ItemModel::PT_ARTICLE,
'$content' => $x['content'] ?? '', '$content' => $x['content'] ?? '',
'$post_id' => $x['post_id'] ?? '', '$post_id' => $x['post_id'] ?? '',
'$baseurl' => $this->baseURL,
'$defloc' => $x['default_location'], '$defloc' => $x['default_location'],
'$visitor' => $x['visitor'], '$visitor' => $x['visitor'],
'$pvisit' => $notes_cid ? 'none' : $x['visitor'], '$pvisit' => $notes_cid ? 'none' : $x['visitor'],
@ -591,7 +589,6 @@ class Conversation
} }
$o = Renderer::replaceMacros($page_template, [ $o = Renderer::replaceMacros($page_template, [
'$baseurl' => $this->baseURL,
'$return_path' => $this->args->getQueryString(), '$return_path' => $this->args->getQueryString(),
'$live_update' => $live_update_div, '$live_update' => $live_update_div,
'$remove' => $this->l10n->t('remove'), '$remove' => $this->l10n->t('remove'),
@ -1517,14 +1514,6 @@ class Conversation
[$categories, $folders] = $this->item->determineCategoriesTerms($item, $this->session->getLocalUserId()); [$categories, $folders] = $this->item->determineCategoriesTerms($item, $this->session->getLocalUserId());
if (!empty($item['title'])) {
$title = $item['title'];
} elseif (!empty($item['content-warning']) && $this->pConfig->get($this->session->getLocalUserId(), 'system', 'disable_cw', false)) {
$title = ucfirst($item['content-warning']);
} else {
$title = '';
}
if (!empty($item['featured'])) { if (!empty($item['featured'])) {
$pinned = $this->l10n->t('Pinned item'); $pinned = $this->l10n->t('Pinned item');
} else { } else {
@ -1550,7 +1539,8 @@ class Conversation
'sparkle' => $sparkle, 'sparkle' => $sparkle,
'lock' => false, 'lock' => false,
'thumb' => $this->baseURL->remove($this->item->getAuthorAvatar($item)), 'thumb' => $this->baseURL->remove($this->item->getAuthorAvatar($item)),
'title' => $title, 'title' => $item['title'],
'summary' => $item['content-warning'],
'body_html' => $body_html, 'body_html' => $body_html,
'tags' => $tags['tags'], 'tags' => $tags['tags'],
'hashtags' => $tags['hashtags'], 'hashtags' => $tags['hashtags'],

View file

@ -26,6 +26,25 @@ use Friendica\DI;
class Feature class Feature
{ {
const ACCOUNTS = 'accounts';
const ADD_ABSTRACT = 'add_abstract';
const ARCHIVE = 'archive';
const CATEGORIES = 'categories';
const CHANNELS = 'channels';
const CIRCLES = 'circles';
const COMMUNITY = 'community';
const EXPLICIT_MENTIONS = 'explicit_mentions';
const FOLDERS = 'folders';
const GROUPS = 'forumlist_profile';
const MEMBER_SINCE = 'profile_membersince';
const NETWORKS = 'networks';
const NOSHARER = 'nosharer';
const PHOTO_LOCATION = 'photo_location';
const PUBLIC_CALENDAR = 'public_calendar';
const SEARCHES = 'searches';
const TAGCLOUD = 'tagadelic';
const TRENDING_TAGS = 'trending_tags';
/** /**
* check if feature is enabled * check if feature is enabled
* *
@ -34,25 +53,18 @@ class Feature
* @return boolean * @return boolean
* @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/ */
public static function isEnabled(int $uid, $feature) public static function isEnabled(int $uid, $feature): bool
{ {
$x = DI::config()->get('feature_lock', $feature, false); if (!DI::config()->get('feature_lock', $feature, false)) {
$enabled = DI::config()->get('feature', $feature) ?? self::getDefault($feature);
if ($x === false) { $enabled = DI::pConfig()->get($uid, 'feature', $feature) ?? $enabled;
$x = DI::pConfig()->get($uid, 'feature', $feature, false); } else {
$enabled = true;
} }
if ($x === false) { $arr = ['uid' => $uid, 'feature' => $feature, 'enabled' => $enabled];
$x = DI::config()->get('feature', $feature, false);
}
if ($x === false) {
$x = self::getDefault($feature);
}
$arr = ['uid' => $uid, 'feature' => $feature, 'enabled' => $x];
Hook::callAll('isEnabled', $arr); Hook::callAll('isEnabled', $arr);
return($arr['enabled']); return (bool)$arr['enabled'];
} }
/** /**
@ -64,8 +76,7 @@ class Feature
*/ */
private static function getDefault($feature) private static function getDefault($feature)
{ {
$f = self::get(); foreach (self::get() as $cat) {
foreach ($f as $cat) {
foreach ($cat as $feat) { foreach ($cat as $feat) {
if (is_array($feat) && $feat[0] === $feature) { if (is_array($feat) && $feat[0] === $feature) {
return $feat[3]; return $feat[3];
@ -95,36 +106,49 @@ class Feature
'general' => [ 'general' => [
DI::l10n()->t('General Features'), DI::l10n()->t('General Features'),
//array('expire', DI::l10n()->t('Content Expiration'), DI::l10n()->t('Remove old posts/comments after a period of time')), //array('expire', DI::l10n()->t('Content Expiration'), DI::l10n()->t('Remove old posts/comments after a period of time')),
['photo_location', DI::l10n()->t('Photo Location'), DI::l10n()->t("Photo metadata is normally stripped. This extracts the location \x28if present\x29 prior to stripping metadata and links it to a map."), false, DI::config()->get('feature_lock', 'photo_location', false)], [self::PHOTO_LOCATION, DI::l10n()->t('Photo Location'), DI::l10n()->t("Photo metadata is normally stripped. This extracts the location \x28if present\x29 prior to stripping metadata and links it to a map."), false, DI::config()->get('feature_lock', self::PHOTO_LOCATION, false)],
['trending_tags', DI::l10n()->t('Trending Tags'), DI::l10n()->t('Show a community page widget with a list of the most popular tags in recent public posts.'), false, DI::config()->get('feature_lock', 'trending_tags', false)], [self::COMMUNITY, DI::l10n()->t('Display the community in the navigation'), DI::l10n()->t('If enabled, the community can be accessed via the navigation menu. Independent from this setting, the community timelines can always be accessed via the channels.'), true, DI::config()->get('feature_lock', self::COMMUNITY, false)],
], ],
// Post composition // Post composition
'composition' => [ 'composition' => [
DI::l10n()->t('Post Composition Features'), DI::l10n()->t('Post Composition Features'),
['aclautomention', DI::l10n()->t('Auto-mention Groups'), DI::l10n()->t('Add/remove mention when a group page is selected/deselected in ACL window.'), false, DI::config()->get('feature_lock', 'aclautomention', false)], [self::EXPLICIT_MENTIONS, DI::l10n()->t('Explicit Mentions'), DI::l10n()->t('Add explicit mentions to comment box for manual control over who gets mentioned in replies.'), false, DI::config()->get('feature_lock', Feature::EXPLICIT_MENTIONS, false)],
['explicit_mentions', DI::l10n()->t('Explicit Mentions'), DI::l10n()->t('Add explicit mentions to comment box for manual control over who gets mentioned in replies.'), false, DI::config()->get('feature_lock', 'explicit_mentions', false)], [self::ADD_ABSTRACT, DI::l10n()->t('Add an abstract from ActivityPub content warnings'), DI::l10n()->t('Add an abstract when commenting on ActivityPub posts with a content warning. Abstracts are displayed as content warning on systems like Mastodon or Pleroma.'), false, DI::config()->get('feature_lock', self::ADD_ABSTRACT, false)],
['add_abstract', DI::l10n()->t('Add an abstract from ActivityPub content warnings'), DI::l10n()->t('Add an abstract when commenting on ActivityPub posts with a content warning. Abstracts are displayed as content warning on systems like Mastodon or Pleroma.'), false, DI::config()->get('feature_lock', 'add_abstract', false)],
], ],
// Item tools // Item tools
'tools' => [ 'tools' => [
DI::l10n()->t('Post/Comment Tools'), DI::l10n()->t('Post/Comment Tools'),
['categories', DI::l10n()->t('Post Categories'), DI::l10n()->t('Add categories to your posts'), false, DI::config()->get('feature_lock', 'categories', false)], [self::CATEGORIES, DI::l10n()->t('Post Categories'), DI::l10n()->t('Add categories to your posts'), false, DI::config()->get('feature_lock', self::CATEGORIES, false)],
],
// Widget visibility on the network stream
'network' => [
DI::l10n()->t('Network Widgets'),
[self::CIRCLES, DI::l10n()->t('Circles'), DI::l10n()->t('Display posts that have been created by accounts of the selected circle.'), true, DI::config()->get('feature_lock', self::CIRCLES, false)],
[self::GROUPS, DI::l10n()->t('Groups'), DI::l10n()->t('Display posts that have been distributed by the selected group.'), true, DI::config()->get('feature_lock', self::GROUPS, false)],
[self::ARCHIVE, DI::l10n()->t('Archives'), DI::l10n()->t('Display an archive where posts can be selected by month and year.'), true, DI::config()->get('feature_lock', self::ARCHIVE, false)],
[self::NETWORKS, DI::l10n()->t('Protocols'), DI::l10n()->t('Display posts with the selected protocols.'), true, DI::config()->get('feature_lock', self::NETWORKS, false)],
[self::ACCOUNTS, DI::l10n()->t('Account Types'), DI::l10n()->t('Display posts done by accounts with the selected account type.'), true, DI::config()->get('feature_lock', self::ACCOUNTS, false)],
[self::CHANNELS, DI::l10n()->t('Channels'), DI::l10n()->t('Display posts in the system channels and user defined channels.'), true, DI::config()->get('feature_lock', self::CHANNELS, false)],
[self::SEARCHES, DI::l10n()->t('Saved Searches'), DI::l10n()->t('Display posts that contain subscribed hashtags.'), true, DI::config()->get('feature_lock', self::SEARCHES, false)],
[self::FOLDERS, DI::l10n()->t('Saved Folders'), DI::l10n()->t('Display a list of folders in which posts are stored.'), true, DI::config()->get('feature_lock', self::FOLDERS, false)],
[self::NOSHARER, DI::l10n()->t('Own Contacts'), DI::l10n()->t('Include or exclude posts from subscribed accounts. This widget is not visible on all channels.'), true, DI::config()->get('feature_lock', self::NOSHARER, false)],
[self::TRENDING_TAGS, DI::l10n()->t('Trending Tags'), DI::l10n()->t('Display a list of the most popular tags in recent public posts.'), false, DI::config()->get('feature_lock', self::TRENDING_TAGS, false)],
], ],
// Advanced Profile Settings // Advanced Profile Settings
'advanced_profile' => [ 'advanced_profile' => [
DI::l10n()->t('Advanced Profile Settings'), DI::l10n()->t('Advanced Profile Settings'),
['forumlist_profile', DI::l10n()->t('List Groups'), DI::l10n()->t('Show visitors public groups at the Advanced Profile Page'), false, DI::config()->get('feature_lock', 'forumlist_profile', false)], [self::TAGCLOUD, DI::l10n()->t('Tag Cloud'), DI::l10n()->t('Provide a personal tag cloud on your profile page'), false, DI::config()->get('feature_lock', self::TAGCLOUD, false)],
['tagadelic', DI::l10n()->t('Tag Cloud'), DI::l10n()->t('Provide a personal tag cloud on your profile page'), false, DI::config()->get('feature_lock', 'tagadelic', false)], [self::MEMBER_SINCE, DI::l10n()->t('Display Membership Date'), DI::l10n()->t('Display membership date in profile'), false, DI::config()->get('feature_lock', self::MEMBER_SINCE, false)],
['profile_membersince', DI::l10n()->t('Display Membership Date'), DI::l10n()->t('Display membership date in profile'), false, DI::config()->get('feature_lock', 'profile_membersince', false)],
], ],
//Advanced Calendar Settings //Advanced Calendar Settings
'advanced_calendar' => [ 'advanced_calendar' => [
DI::l10n()->t('Advanced Calendar Settings'), DI::l10n()->t('Advanced Calendar Settings'),
['public_calendar', DI::l10n()->t('Allow anonymous access to your calendar'), DI::l10n()->t('Allows anonymous visitors to consult your calendar and your public events. Contact birthday events are private to you.'), false, DI::config()->get('feature_lock', 'public_calendar', false)], [self::PUBLIC_CALENDAR, DI::l10n()->t('Allow anonymous access to your calendar'), DI::l10n()->t('Allows anonymous visitors to consult your calendar and your public events. Contact birthday events are private to you.'), false, DI::config()->get('feature_lock', self::PUBLIC_CALENDAR, false)],
] ]
]; ];

View file

@ -66,7 +66,7 @@ class GroupManager
'archive' => false, 'archive' => false,
]; ];
$condition = DBA::mergeConditions($condition, ["`platform` != ?", 'peertube']); $condition = DBA::mergeConditions($condition, ["`platform` NOT IN (?, ?)", 'peertube', 'wordpress']);
if (!$showprivate) { if (!$showprivate) {
$condition = DBA::mergeConditions($condition, ['manually-approve' => false]); $condition = DBA::mergeConditions($condition, ['manually-approve' => false]);
@ -172,8 +172,7 @@ class GroupManager
*/ */
public static function profileAdvanced($uid) public static function profileAdvanced($uid)
{ {
$profile = intval(Feature::isEnabled($uid, 'forumlist_profile')); if (!Feature::isEnabled($uid, Feature::GROUPS)) {
if (!$profile) {
return ''; return '';
} }

View file

@ -48,7 +48,6 @@ use Friendica\Model\User;
use Friendica\Network\HTTPException; use Friendica\Network\HTTPException;
use Friendica\Object\EMail\ItemCCEMail; use Friendica\Object\EMail\ItemCCEMail;
use Friendica\Protocol\Activity; use Friendica\Protocol\Activity;
use Friendica\Protocol\ActivityPub;
use Friendica\Util\ACLFormatter; use Friendica\Util\ACLFormatter;
use Friendica\Util\DateTimeFormat; use Friendica\Util\DateTimeFormat;
use Friendica\Util\Emailer; use Friendica\Util\Emailer;
@ -440,9 +439,11 @@ class Item
]; ];
if (!empty($item['language'])) { if (!empty($item['language'])) {
$menu[$this->l10n->t('Languages')] = 'javascript:alert(\'' . ItemModel::getLanguageMessage($item) . '\');'; $menu[$this->l10n->t('Languages')] = 'javascript:displayLanguage(' . $item['uri-id'] . ');';
} }
$menu[$this->l10n->t('Search Text')] = 'javascript:displaySearchText(' . $item['uri-id'] . ');';
if ((($cid == 0) || ($rel == Contact::FOLLOWER)) && if ((($cid == 0) || ($rel == Contact::FOLLOWER)) &&
in_array($item['network'], Protocol::FEDERATED) in_array($item['network'], Protocol::FEDERATED)
) { ) {
@ -546,9 +547,9 @@ class Item
$item['private'] = $private_group ? ItemModel::PRIVATE : ItemModel::UNLISTED; $item['private'] = $private_group ? ItemModel::PRIVATE : ItemModel::UNLISTED;
if ($only_to_group) { if ($only_to_group) {
$cdata = Contact::getPublicAndUserContactID($group_contact['id'], $item['uid']); $pcid = Contact::getPublicContactId($group_contact['id'], $item['uid']);
if (!empty($cdata['user'])) { if ($pcid) {
$item['owner-id'] = $cdata['user']; $item['owner-id'] = $pcid;
unset($item['owner-link']); unset($item['owner-link']);
unset($item['owner-name']); unset($item['owner-name']);
unset($item['owner-avatar']); unset($item['owner-avatar']);
@ -800,14 +801,14 @@ class Item
*/ */
public function addShareLink(string $body, int $quote_uri_id): string public function addShareLink(string $body, int $quote_uri_id): string
{ {
$post = Post::selectFirstPost(['uri', 'plink'], ['uri-id' => $quote_uri_id]); $post = Post::selectFirstPost(['uri'], ['uri-id' => $quote_uri_id]);
if (empty($post)) { if (empty($post)) {
return $body; return $body;
} }
$body = BBCode::removeSharedData($body); $body = BBCode::removeSharedData($body);
$body .= "\n" . ($post['plink'] ?: $post['uri']); $body .= "\nRE: " . $post['uri'];
return $body; return $body;
} }
@ -910,40 +911,6 @@ class Item
return $post; return $post;
} }
public function moveAttachmentsFromBodyToAttach(array $post): array
{
if (!preg_match_all('/(\[attachment\]([0-9]+)\[\/attachment\])/', $post['body'], $match)) {
return $post;
}
foreach ($match[2] as $attachment_id) {
$attachment = Attach::selectFirst(['id', 'uid', 'filename', 'filesize', 'filetype'], ['id' => $attachment_id, 'uid' => $post['uid']]);
if (empty($attachment)) {
continue;
}
if ($post['attach']) {
$post['attach'] .= ',';
}
$post['attach'] .= Post\Media::getAttachElement(
$this->baseURL . '/attach/' . $attachment['id'],
$attachment['filesize'],
$attachment['filetype'],
$attachment['filename'] ?? ''
);
$fields = [
'allow_cid' => $post['allow_cid'], 'allow_gid' => $post['allow_gid'],
'deny_cid' => $post['deny_cid'], 'deny_gid' => $post['deny_gid']
];
$condition = ['id' => $attachment_id];
Attach::update($fields, $condition);
}
$post['body'] = str_replace($match[1], '', $post['body']);
return $post;
}
private function setObjectType(array $post): array private function setObjectType(array $post): array
{ {
if (empty($post['post-type'])) { if (empty($post['post-type'])) {
@ -1023,8 +990,13 @@ class Item
return $post; return $post;
} }
public function finalizePost(array $post): array public function finalizePost(array $post, bool $preview): array
{ {
if ($preview) {
$post['body'] = Attach::addAttachmentToBody($post['body'], $post['uid']);
} else {
Attach::setPermissionFromBody($post);
}
if (preg_match("/\[attachment\](.*?)\[\/attachment\]/ism", $post['body'], $matches)) { if (preg_match("/\[attachment\](.*?)\[\/attachment\]/ism", $post['body'], $matches)) {
$post['body'] = preg_replace("/\[attachment].*?\[\/attachment\]/ism", PageInfo::getFooterFromUrl($matches[1]), $post['body']); $post['body'] = preg_replace("/\[attachment].*?\[\/attachment\]/ism", PageInfo::getFooterFromUrl($matches[1]), $post['body']);
} }
@ -1045,7 +1017,7 @@ class Item
public function postProcessPost(array $post, array $recipients = []) public function postProcessPost(array $post, array $recipients = [])
{ {
if (!\Friendica\Content\Feature::isEnabled($post['uid'], 'explicit_mentions') && ($post['gravity'] == ItemModel::GRAVITY_COMMENT)) { if (!Feature::isEnabled($post['uid'], Feature::EXPLICIT_MENTIONS) && ($post['gravity'] == ItemModel::GRAVITY_COMMENT)) {
Tag::createImplicitMentions($post['uri-id'], $post['thr-parent-id']); Tag::createImplicitMentions($post['uri-id'], $post['thr-parent-id']);
} }
@ -1060,7 +1032,7 @@ class Item
} }
$this->emailer->send(new ItemCCEMail( $this->emailer->send(new ItemCCEMail(
$this->app, $this->userSession,
$this->l10n, $this->l10n,
$this->baseURL, $this->baseURL,
$post, $post,

View file

@ -30,12 +30,12 @@ use Friendica\Core\Renderer;
use Friendica\Core\Session\Capability\IHandleUserSessions; use Friendica\Core\Session\Capability\IHandleUserSessions;
use Friendica\Database\Database; use Friendica\Database\Database;
use Friendica\Model\Contact; use Friendica\Model\Contact;
use Friendica\Model\Profile;
use Friendica\Model\User; use Friendica\Model\User;
use Friendica\Module\Conversation\Community; use Friendica\Module\Conversation\Community;
use Friendica\Module\Home; use Friendica\Module\Home;
use Friendica\Module\Security\Login; use Friendica\Module\Security\Login;
use Friendica\Network\HTTPException; use Friendica\Network\HTTPException;
use Friendica\Security\OpenWebAuth;
class Nav class Nav
{ {
@ -251,7 +251,7 @@ class Nav
$nav['home'] = [$homelink, $this->l10n->t('Home'), '', $this->l10n->t('Home Page')]; $nav['home'] = [$homelink, $this->l10n->t('Home'), '', $this->l10n->t('Home Page')];
} }
if (intval($this->config->get('config', 'register_policy')) === \Friendica\Module\Register::OPEN && !$this->session->isAuthenticated()) { if (\Friendica\Module\Register::getPolicy() === \Friendica\Module\Register::OPEN && !$this->session->isAuthenticated()) {
$nav['register'] = ['register', $this->l10n->t('Register'), '', $this->l10n->t('Create an account')]; $nav['register'] = ['register', $this->l10n->t('Register'), '', $this->l10n->t('Create an account')];
} }
@ -281,11 +281,11 @@ class Nav
$gdirpath = 'directory'; $gdirpath = 'directory';
if ($this->config->get('system', 'singleuser') && $this->config->get('system', 'directory')) { if ($this->config->get('system', 'singleuser') && $this->config->get('system', 'directory')) {
$gdirpath = Profile::zrl($this->config->get('system', 'directory'), true); $gdirpath = OpenWebAuth::getZrlUrl($this->config->get('system', 'directory'), true);
} }
if (($this->session->getLocalUserId() || $this->config->get('system', 'community_page_style') != Community::DISABLED_VISITOR) && if (Feature::isEnabled($this->session->getLocalUserId(), Feature::COMMUNITY) && (($this->session->getLocalUserId() || $this->config->get('system', 'community_page_style') != Community::DISABLED_VISITOR) &&
!($this->config->get('system', 'community_page_style') == Community::DISABLED)) { !($this->config->get('system', 'community_page_style') == Community::DISABLED))) {
$nav['community'] = ['community', $this->l10n->t('Community'), '', $this->l10n->t('Conversations on this and other servers')]; $nav['community'] = ['community', $this->l10n->t('Community'), '', $this->l10n->t('Conversations on this and other servers')];
} }

View file

@ -32,6 +32,7 @@ use Friendica\Database\Database;
use Friendica\Database\DBA; use Friendica\Database\DBA;
use Friendica\DI; use Friendica\DI;
use Friendica\Network\HTTPClient\Client\HttpClientAccept; use Friendica\Network\HTTPClient\Client\HttpClientAccept;
use Friendica\Network\HTTPClient\Client\HttpClientRequest;
use Friendica\Util\DateTimeFormat; use Friendica\Util\DateTimeFormat;
use Friendica\Util\Network; use Friendica\Util\Network;
use Friendica\Util\ParseUrl; use Friendica\Util\ParseUrl;
@ -86,7 +87,7 @@ class OEmbed
if (!in_array($ext, $noexts)) { if (!in_array($ext, $noexts)) {
// try oembed autodiscovery // try oembed autodiscovery
$html_text = DI::httpClient()->fetch($embedurl, HttpClientAccept::HTML, 15); $html_text = DI::httpClient()->fetch($embedurl, HttpClientAccept::HTML, 15, '', HttpClientRequest::SITEINFO);
if (!empty($html_text)) { if (!empty($html_text)) {
$dom = new DOMDocument(); $dom = new DOMDocument();
if (@$dom->loadHTML($html_text)) { if (@$dom->loadHTML($html_text)) {
@ -100,7 +101,7 @@ class OEmbed
// but their OEmbed endpoint is only accessible by HTTPS ¯\_(ツ)_/¯ // but their OEmbed endpoint is only accessible by HTTPS ¯\_(ツ)_/¯
$href = str_replace(['http://www.youtube.com/', 'http://player.vimeo.com/'], $href = str_replace(['http://www.youtube.com/', 'http://player.vimeo.com/'],
['https://www.youtube.com/', 'https://player.vimeo.com/'], $href); ['https://www.youtube.com/', 'https://player.vimeo.com/'], $href);
$result = DI::httpClient()->fetchFull($href . '&maxwidth=' . $a->getThemeInfoValue('videowidth')); $result = DI::httpClient()->fetchFull($href . '&maxwidth=' . $a->getThemeInfoValue('videowidth'), HttpClientAccept::DEFAULT, 0, '', HttpClientRequest::SITEINFO);
if ($result->isSuccess()) { if ($result->isSuccess()) {
$json_string = $result->getBodyString(); $json_string = $result->getBodyString();
break; break;

View file

@ -40,6 +40,7 @@ use Friendica\Model\Post;
use Friendica\Model\Tag; use Friendica\Model\Tag;
use Friendica\Network\HTTPClient\Client\HttpClientAccept; use Friendica\Network\HTTPClient\Client\HttpClientAccept;
use Friendica\Network\HTTPClient\Client\HttpClientOptions; use Friendica\Network\HTTPClient\Client\HttpClientOptions;
use Friendica\Network\HTTPClient\Client\HttpClientRequest;
use Friendica\Util\Images; use Friendica\Util\Images;
use Friendica\Util\Map; use Friendica\Util\Map;
use Friendica\Util\Network; use Friendica\Util\Network;
@ -47,11 +48,12 @@ use Friendica\Util\ParseUrl;
use Friendica\Util\Proxy; use Friendica\Util\Proxy;
use Friendica\Util\Strings; use Friendica\Util\Strings;
use Friendica\Util\XML; use Friendica\Util\XML;
use GuzzleHttp\Psr7\Uri;
class BBCode class BBCode
{ {
// Update this value to the current date whenever changes are made to BBCode::convert // Update this value to the current date whenever changes are made to BBCode::convert
const VERSION = '2021-07-28'; const VERSION = '2024-04-07';
const INTERNAL = 0; const INTERNAL = 0;
const EXTERNAL = 1; const EXTERNAL = 1;
@ -146,8 +148,7 @@ class BBCode
case 'title': case 'title':
$value = self::toPlaintext(html_entity_decode($value, ENT_QUOTES, 'UTF-8')); $value = self::toPlaintext(html_entity_decode($value, ENT_QUOTES, 'UTF-8'));
$value = html_entity_decode($value, ENT_QUOTES, 'UTF-8'); $value = html_entity_decode($value, ENT_QUOTES, 'UTF-8');
$value = str_replace(['[', ']'], ['&#91;', '&#93;'], $value); $data['title'] = self::escapeContent($value);
$data['title'] = $value;
default: default:
$data[$field] = html_entity_decode($value, ENT_QUOTES, 'UTF-8'); $data[$field] = html_entity_decode($value, ENT_QUOTES, 'UTF-8');
@ -312,13 +313,14 @@ class BBCode
public static function proxyUrl(string $image, int $simplehtml = self::INTERNAL, int $uriid = 0, string $size = ''): string public static function proxyUrl(string $image, int $simplehtml = self::INTERNAL, int $uriid = 0, string $size = ''): string
{ {
$image = self::idnUrl($image);
// Only send proxied pictures to API and for internal display // Only send proxied pictures to API and for internal display
if (!in_array($simplehtml, [self::INTERNAL, self::MASTODON_API, self::TWITTER_API])) { if (!in_array($simplehtml, [self::INTERNAL, self::MASTODON_API, self::TWITTER_API])) {
return $image; return $image;
} elseif ($uriid > 0) { } elseif ($uriid > 0) {
return Post\Link::getByLink($uriid, $image, $size); return Post\Link::getByLink($uriid, $image, $size);
} else { } else {
return Proxy::proxifyUrl($image, $size); return $image;
} }
} }
@ -551,71 +553,6 @@ class BBCode
return $text . "\n" . $data['after']; return $text . "\n" . $data['after'];
} }
/**
* Converts [url] BBCodes in a format that looks fine on Mastodon. (callback function)
*
* @param array $match Array with the matching values
* @return string reformatted link including HTML codes
*/
private static function convertUrlForActivityPubCallback(array $match): string
{
$url = $match[1];
if (isset($match[2]) && ($match[1] != $match[2])) {
return $match[0];
}
$parts = parse_url($url);
if (!isset($parts['scheme'])) {
return $match[0];
}
return self::convertUrlForActivityPub($url);
}
/**
* Converts [url] BBCodes in a format that looks fine on ActivityPub systems.
*
* @param string $url URL that is about to be reformatted
* @return string reformatted link including HTML codes
*/
private static function convertUrlForActivityPub(string $url): string
{
return sprintf('<a href="%s" target="_blank" rel="noopener noreferrer">%s</a>', $url, Strings::getStyledURL($url));
}
/*
* [noparse][i]italic[/i][/noparse] turns into
* [noparse][ i ]italic[ /i ][/noparse],
* to hide them from parser.
*
* @param array $match
* @return string
*/
private static function escapeNoparseCallback(array $match): string
{
$whole_match = $match[0];
$captured = $match[1];
$spacefied = preg_replace("/\[(.*?)\]/", "[ $1 ]", $captured);
$new_str = str_replace($captured, $spacefied, $whole_match);
return $new_str;
}
/*
* The previously spacefied [noparse][ i ]italic[ /i ][/noparse],
* now turns back and the [noparse] tags are trimmed
* returning [i]italic[/i]
*
* @param array $match
* @return string
*/
private static function unescapeNoparseCallback(array $match): string
{
$captured = $match[1];
$unspacefied = preg_replace("/\[ (.*?)\ ]/", "[$1]", $captured);
return $unspacefied;
}
/** /**
* Returns the bracket character positions of a set of opening and closing BBCode tags, optionally skipping first * Returns the bracket character positions of a set of opening and closing BBCode tags, optionally skipping first
* occurrences * occurrences
@ -1026,7 +963,7 @@ class BBCode
$text = DI::cache()->get($cache_key); $text = DI::cache()->get($cache_key);
if (is_null($text)) { if (is_null($text)) {
$curlResult = DI::httpClient()->head($match[1], [HttpClientOptions::TIMEOUT => DI::config()->get('system', 'xrd_timeout')]); $curlResult = DI::httpClient()->head($match[1], [HttpClientOptions::TIMEOUT => DI::config()->get('system', 'xrd_timeout'), HttpClientOptions::REQUEST => HttpClientRequest::CONTENTTYPE]);
if ($curlResult->isSuccess()) { if ($curlResult->isSuccess()) {
$mimetype = $curlResult->getContentType() ?? ''; $mimetype = $curlResult->getContentType() ?? '';
} else { } else {
@ -1039,7 +976,7 @@ class BBCode
$text = '[url=' . $match[2] . ']' . $match[2] . '[/url]'; $text = '[url=' . $match[2] . ']' . $match[2] . '[/url]';
// if its not a picture then look if its a page that contains a picture link // if its not a picture then look if its a page that contains a picture link
$body = DI::httpClient()->fetch($match[1], HttpClientAccept::HTML, 0); $body = DI::httpClient()->fetch($match[1], HttpClientAccept::HTML, 0, '', HttpClientRequest::SITEINFO);
if (empty($body)) { if (empty($body)) {
DI::cache()->set($cache_key, $text); DI::cache()->set($cache_key, $text);
return $text; return $text;
@ -1124,7 +1061,7 @@ class BBCode
return $text; return $text;
} }
$curlResult = DI::httpClient()->head($match[1], [HttpClientOptions::TIMEOUT => DI::config()->get('system', 'xrd_timeout')]); $curlResult = DI::httpClient()->head($match[1], [HttpClientOptions::TIMEOUT => DI::config()->get('system', 'xrd_timeout'), HttpClientOptions::REQUEST => HttpClientRequest::CONTENTTYPE]);
if ($curlResult->isSuccess()) { if ($curlResult->isSuccess()) {
$mimetype = $curlResult->getContentType() ?? ''; $mimetype = $curlResult->getContentType() ?? '';
} else { } else {
@ -1142,7 +1079,7 @@ class BBCode
} }
// if its not a picture then look if its a page that contains a picture link // if its not a picture then look if its a page that contains a picture link
$body = DI::httpClient()->fetch($match[1], HttpClientAccept::HTML, 0); $body = DI::httpClient()->fetch($match[1], HttpClientAccept::HTML, 0, '', HttpClientRequest::SITEINFO);
if (empty($body)) { if (empty($body)) {
DI::cache()->set($cache_key, $text); DI::cache()->set($cache_key, $text);
return $text; return $text;
@ -1383,10 +1320,8 @@ class BBCode
Hook::callAll('bbcode', $text); Hook::callAll('bbcode', $text);
$a = DI::app(); $text = self::performWithEscapedTags($text, ['code'], function ($text) use ($try_oembed, $simple_html, $for_plaintext, $uriid) {
$text = self::performWithEscapedTags($text, ['noparse', 'nobb', 'pre'], function ($text) use ($try_oembed, $simple_html, $for_plaintext, $uriid) {
$text = self::performWithEscapedTags($text, ['code'], function ($text) use ($try_oembed, $simple_html, $for_plaintext, $a, $uriid) {
$text = self::performWithEscapedTags($text, ['noparse', 'nobb', 'pre'], function ($text) use ($try_oembed, $simple_html, $for_plaintext, $a, $uriid) {
/* /*
* preg_match_callback function to replace potential Oembed tags with Oembed content * preg_match_callback function to replace potential Oembed tags with Oembed content
* *
@ -1407,6 +1342,117 @@ class BBCode
return $return; return $return;
}; };
// Extract the private images which use data urls since preg has issues with
// large data sizes. Stash them away while we do bbcode conversion, and then put them back
// in after we've done all the regex matching. We cannot use any preg functions to do this.
$extracted = self::extractImagesFromItemBody($text);
$saved_image = $extracted['images'];
// General clean up of the content, for example unneeded blanks and new lines
$text = self::normaliseInput($extracted['body']);
// Now the structural elements are converted
$text = self::convertHeaderToHtml($text, $simple_html);
$text = self::convertStylesToHtml($text, $simple_html);
$text = self::convertListsToHtml($text);
$text = self::convertTablesToHtml($text);
$text = self::convertSpoilersToHtml($text);
$text = self::convertStructuresToHtml($text);
// We add URL without a surrounding URL at this time, since at a earlier stage it would had been too early,
// since the used regular expression won't touch URL inside of BBCode elements, but with the structural ones it should.
// At a later stage we won't be able to exclude certain parts of the code.
$text = self::performWithEscapedTags($text, ['url', 'img', 'audio', 'video', 'youtube', 'vimeo', 'share', 'attachment', 'iframe', 'bookmark', 'map', 'oembed'], function ($text) use ($simple_html, $for_plaintext) {
if (!$for_plaintext) {
$text = preg_replace(Strings::autoLinkRegEx(), '[url]$1[/url]', $text) ?? '';
}
return self::convertSmileysToHtml($text, $simple_html, $for_plaintext);
});
// Now for some more complex BBCode elements (mostly non standard ones)
$text = self::convertAttachmentsToHtml($text, $simple_html, $try_oembed, $uriid);
$text = self::convertMapsToHtml($text, $simple_html);
$text = self::convertQuotesToHtml($text);
$text = self::convertVideoPlatformsToHtml($text, $try_oembed);
$text = self::convertOEmbedToHtml($text, $uriid);
$text = self::convertEventsToHtml($text, $simple_html, $uriid);
// Some simpler non standard elements
$text = self::convertEmojisToHtml($text, $simple_html);
$text = self::convertCryptToHtml($text);
$text = self::convertIFramesToHtml($text);
$text = self::convertMailToHtml($text);
$text = self::convertAudioVideoToHtml($text, $simple_html, $try_oembed, $try_oembed_callback);
// At last, some standard elements. URL has to go last,
// since some previous conversions use URL elements.
$text = self::convertImagesToHtml($text, $simple_html, $uriid);
$text = self::convertUrlToHtml($text, $simple_html, $for_plaintext, $try_oembed, $try_oembed_callback);
// If the post only consists of an emoji, we display it larger than normal.
if (!$for_plaintext && DI::config()->get('system', 'big_emojis') && ($simple_html != self::DIASPORA) && Smilies::isEmojiPost($text)) {
$text = '<span style="font-size: xx-large; line-height: normal;">' . $text . '</span>';
}
// Sanitize the created HTML.
$text = self::cleanupHtml($text);
// This needs to be called after the cleanup, since otherwise some links are invalidated
$text = self::convertSharesToHtml($text, $simple_html, $try_oembed, $uriid);
// Insert the previously extracted embedded image again.
return self::interpolateSavedImagesIntoItemBody($uriid, $text, $saved_image);
}); // Escaped noparse, nobb, pre
// Remove escaping tags and replace new lines that remain
$text = preg_replace_callback('/\[(noparse|nobb)](.*?)\[\/\1]/ism', function ($match) {
return str_replace("\n", "<br>", $match[2]);
}, $text);
// Additionally, [pre] tags preserve spaces
$text = preg_replace_callback("/\[pre\](.*?)\[\/pre\]/ism", function ($match) {
return str_replace([' ', "\n"], ['&nbsp;', "<br>"], htmlentities($match[1], ENT_NOQUOTES, 'UTF-8'));
}, $text);
return $text;
}); // Escaped code
$text = preg_replace_callback(
"#\[code(?:=([^\]]*))?\](.*?)\[\/code\]#ism",
function ($matches) {
if (strpos($matches[2], "\n") !== false) {
$return = '<pre><code class="language-' . trim($matches[1]) . '">' . htmlentities(trim($matches[2], "\n\r"), ENT_NOQUOTES, 'UTF-8') . '</code></pre>';
} else {
$return = '<code>' . htmlentities($matches[2], ENT_NOQUOTES, 'UTF-8') . '</code>';
}
return $return;
},
$text
);
// Default iframe allowed domains/path
$allowedIframeDomains = DI::config()->get('system', 'no_oembed_rich_content') ? [] : ['www.youtube.com/embed/', 'player.vimeo.com/video/'];
$allowedIframeDomains = array_merge(
$allowedIframeDomains,
DI::config()->get('system', 'allowed_oembed') ?
explode(',', DI::config()->get('system', 'allowed_oembed'))
: []
);
if (strpos($text, '<p>') !== false || strpos($text, '</p>') !== false) {
$text = '<p>' . $text . '</p>';
}
$text = HTML::purify($text, $allowedIframeDomains);
DI::profiler()->stopRecording();
return trim($text);
}
private static function normaliseInput(string $text): string
{
// Remove the abstract element. It is a non visible element. // Remove the abstract element. It is a non visible element.
$text = self::stripAbstract($text); $text = self::stripAbstract($text);
@ -1417,20 +1463,6 @@ class BBCode
$text = preg_replace("#\[(\w*)](\n*)#ism", '$2[$1]', $text); $text = preg_replace("#\[(\w*)](\n*)#ism", '$2[$1]', $text);
$text = preg_replace("#(\n*)\[/(\w*)]#ism", '[/$2]$1', $text); $text = preg_replace("#(\n*)\[/(\w*)]#ism", '[/$2]$1', $text);
// Extract the private images which use data urls since preg has issues with
// large data sizes. Stash them away while we do bbcode conversion, and then put them back
// in after we've done all the regex matching. We cannot use any preg functions to do this.
$extracted = self::extractImagesFromItemBody($text);
$text = $extracted['body'];
$saved_image = $extracted['images'];
// If we find any event code, turn it into an event.
// After we're finished processing the bbcode we'll
// replace all of the event code with a reformatted version.
$ev = Event::fromBBCode($text);
// Replace any html brackets with HTML Entities to prevent executing HTML or script // Replace any html brackets with HTML Entities to prevent executing HTML or script
// Don't use strip_tags here because it breaks [url] search by replacing & with amp // Don't use strip_tags here because it breaks [url] search by replacing & with amp
@ -1441,11 +1473,6 @@ class BBCode
$text = preg_replace("/\s?\[share(.*?)\]\s?(.*?)\s?\[\/share\]\s?/ism", "\n[share$1]$2[/share]\n", $text); $text = preg_replace("/\s?\[share(.*?)\]\s?(.*?)\s?\[\/share\]\s?/ism", "\n[share$1]$2[/share]\n", $text);
$text = preg_replace("/\s?\[quote(.*?)\]\s?(.*?)\s?\[\/quote\]\s?/ism", "\n[quote$1]$2[/quote]\n", $text); $text = preg_replace("/\s?\[quote(.*?)\]\s?(.*?)\s?\[\/quote\]\s?/ism", "\n[quote$1]$2[/quote]\n", $text);
// when the content is meant exporting to other systems then remove the avatar picture since this doesn't really look good on these systems
if (!$try_oembed) {
$text = preg_replace("/\[share(.*?)avatar\s?=\s?'.*?'\s?(.*?)\]\s?(.*?)\s?\[\/share\]\s?/ism", "\n[share$1$2]$3[/share]", $text);
}
// Remove linefeeds inside of the table elements. See issue #6799 // Remove linefeeds inside of the table elements. See issue #6799
$search = [ $search = [
"\n[th]", "[th]\n", " [th]", "\n[/th]", "[/th]\n", "[/th] ", "\n[th]", "[th]\n", " [th]", "\n[/th]", "[/th]\n", "[/th] ",
@ -1494,6 +1521,38 @@ class BBCode
} while ($oldtext != $text); } while ($oldtext != $text);
} }
return $text;
}
private static function convertEventsToHtml(string $text, int $simple_html, int $uriid): string
{
// If we find any event code, turn it into an event.
// After we're finished processing the bbcode we'll
// replace all of the event code with a reformatted version.
$ev = Event::fromBBCode($text);
// If we found an event earlier, strip out all the event code and replace with a reformatted version.
// Replace the event-start section with the entire formatted event. The other bbcode is stripped.
// Summary (e.g. title) is required, earlier revisions only required description (in addition to
// start which is always required). Allow desc with a missing summary for compatibility.
if ((!empty($ev['desc']) || !empty($ev['summary'])) && !empty($ev['start'])) {
$sub = Event::getHTML($ev, $simple_html, $uriid);
$text = preg_replace("/\[event\-summary\](.*?)\[\/event\-summary\]/ism", '', $text);
$text = preg_replace("/\[event\-description\](.*?)\[\/event\-description\]/ism", '', $text);
$text = preg_replace("/\[event\-start\](.*?)\[\/event\-start\]/ism", $sub, $text);
$text = preg_replace("/\[event\-finish\](.*?)\[\/event\-finish\]/ism", '', $text);
$text = preg_replace("/\[event\-location\](.*?)\[\/event\-location\]/ism", '', $text);
$text = preg_replace("/\[event\-id\](.*?)\[\/event\-id\]/ism", '', $text);
}
return $text;
}
private static function convertAttachmentsToHtml(string $text, int $simple_html, bool $try_oembed, int $uriid): string
{
/// @todo Have a closer look at the different html modes /// @todo Have a closer look at the different html modes
// Handle attached links or videos // Handle attached links or videos
if ($simple_html == self::NPF) { if ($simple_html == self::NPF) {
@ -1506,16 +1565,11 @@ class BBCode
$text = self::convertAttachment($text, $simple_html, $try_oembed, [], $uriid); $text = self::convertAttachment($text, $simple_html, $try_oembed, [], $uriid);
} }
$nosmile = strpos($text, '[nosmile]') !== false; return $text;
$text = str_replace('[nosmile]', '', $text);
// Replace non graphical smilies for external posts
if (!$nosmile) {
$text = self::performWithEscapedTags($text, ['url', 'img', 'audio', 'video', 'youtube', 'vimeo', 'share', 'attachment', 'iframe', 'bookmark'], function ($text) use ($simple_html, $for_plaintext) {
return Smilies::replace($text, ($simple_html != self::INTERNAL) || $for_plaintext);
});
} }
private static function convertMapsToHtml(string $text, int $simple_html): string
{
// leave open the possibility of [map=something] // leave open the possibility of [map=something]
// this is replaced in Item::prepareBody() which has knowledge of the item location // this is replaced in Item::prepareBody() which has knowledge of the item location
if (strpos($text, '[/map]') !== false) { if (strpos($text, '[/map]') !== false) {
@ -1542,6 +1596,11 @@ class BBCode
$text = preg_replace("/\[map\]/", '<p class="map"></p>', $text); $text = preg_replace("/\[map\]/", '<p class="map"></p>', $text);
} }
return $text;
}
private static function convertHeaderToHtml(string $text, int $simple_html): string
{
// Check for headers // Check for headers
if ($simple_html == self::INTERNAL) { if ($simple_html == self::INTERNAL) {
@ -1570,20 +1629,52 @@ class BBCode
$text = preg_replace("(\[h6\](.*?)\[\/h6\])ism", '</p><h6>$1</h6><p>', $text); $text = preg_replace("(\[h6\](.*?)\[\/h6\])ism", '</p><h6>$1</h6><p>', $text);
} }
// Check for paragraph return $text;
$text = preg_replace("(\[p\](.*?)\[\/p\])ism", '<p>$1</p>', $text); }
// Check for bold text private static function convertEmojisToHtml(string $text, int $simple_html): string
$text = preg_replace("(\[b\](.*?)\[\/b\])ism", '<strong>$1</strong>', $text); {
// Mastodon Emoji (internal tag, do not document for users)
if ($simple_html == self::MASTODON_API) {
$text = preg_replace("(\[emoji=(.*?)](.*?)\[/emoji])ism", '$2', $text);
} else {
$text = preg_replace("(\[emoji=(.*?)](.*?)\[/emoji])ism", '<span class="mastodon emoji"><img src="$1" alt="$2" title="$2"/></span>', $text);
}
return $text;
}
// Check for Italics text private static function convertStylesToHtml(string $text, int $simple_html): string
$text = preg_replace("(\[i\](.*?)\[\/i\])ism", '<em>$1</em>', $text); {
// Markdown is designed to pass through HTML elements that it can't handle itself,
// so that the other system would parse the original HTML element.
// But Diaspora has chosen not to do this and doesn't parse HTML elements.
// So we need to make some changes here.
if ($simple_html == BBCode::DIASPORA) {
$elements = ['big', 'small'];
foreach ($elements as $bbcode) {
$text = preg_replace("(\[" . $bbcode . "\](.*?)\[\/" . $bbcode . "\])ism", '$1', $text);
}
// Check for Underline text $elements = [
$text = preg_replace("(\[u\](.*?)\[\/u\])ism", '<u>$1</u>', $text); 'del' => 's', 'ins' => 'em', 'kbd' => 'code', 'mark' => 'strong',
'samp' => 'code', 'u' => 'em', 'var' => 'em'
];
foreach ($elements as $bbcode => $html) {
$text = preg_replace("(\[" . $bbcode . "\](.*?)\[\/" . $bbcode . "\])ism", '<' . $html . '>$1</' . $html . '>', $text);
}
}
// Check for strike-through text // Several easy to replace HTML elements
$text = preg_replace("(\[s\](.*?)\[\/s\])ism", '<s>$1</s>', $text); // @todo add the new elements to the documentation by the end of 2024 so that most systems will support them.
$elements = [
'b', 'del', 'em', 'i', 'ins', 'kbd', 'mark',
's', 'samp', 'small', 'strong', 'sub', 'sup', 'u', 'var'
];
foreach ($elements as $element) {
$text = preg_replace("(\[" . $element . "\](.*?)\[\/" . $element . "\])ism", '<' . $element . '>$1</' . $element . '>', $text);
}
$text = preg_replace("(\[big\](.*?)\[\/big\])ism", "<span style=\"font-size: larger;\">$1</span>", $text);
// Check for over-line text // Check for over-line text
$text = preg_replace("(\[o\](.*?)\[\/o\])ism", '<span class="overline">$1</span>', $text); $text = preg_replace("(\[o\](.*?)\[\/o\])ism", '<span class="overline">$1</span>', $text);
@ -1601,7 +1692,6 @@ class BBCode
$text = preg_replace("(\[size=(.*?)\](.*?)\[\/size\])ism", "$2", $text); $text = preg_replace("(\[size=(.*?)\](.*?)\[\/size\])ism", "$2", $text);
} }
// Check for centered text // Check for centered text
$text = preg_replace("(\[center\](.*?)\[\/center\])ism", '<div style="text-align:center;">$1</div>', $text); $text = preg_replace("(\[center\](.*?)\[\/center\])ism", '<div style="text-align:center;">$1</div>', $text);
@ -1611,13 +1701,6 @@ class BBCode
// Check for inline custom CSS // Check for inline custom CSS
$text = preg_replace("(\[style=(.*?)\](.*?)\[\/style\])ism", '<span style="$1">$2</span>', $text); $text = preg_replace("(\[style=(.*?)\](.*?)\[\/style\])ism", '<span style="$1">$2</span>', $text);
// Mastodon Emoji (internal tag, do not document for users)
if ($simple_html == self::MASTODON_API) {
$text = preg_replace("(\[emoji=(.*?)](.*?)\[/emoji])ism", '$2', $text);
} else {
$text = preg_replace("(\[emoji=(.*?)](.*?)\[/emoji])ism", '<span class="mastodon emoji"><img src="$1" alt="$2" title="$2"/></span>', $text);
}
// Check for CSS classes // Check for CSS classes
// @deprecated since 2021.12, left for backward-compatibility reasons // @deprecated since 2021.12, left for backward-compatibility reasons
$text = preg_replace("(\[class=(.*?)\](.*?)\[\/class\])ism", '<span class="$1">$2</span>', $text); $text = preg_replace("(\[class=(.*?)\](.*?)\[\/class\])ism", '<span class="$1">$2</span>', $text);
@ -1625,6 +1708,27 @@ class BBCode
$text = str_replace("\n\n", '</p><p>', $text); $text = str_replace("\n\n", '</p><p>', $text);
$text = str_replace("\n", '<br>', $text); $text = str_replace("\n", '<br>', $text);
// Check for font change text
$text = preg_replace("/\[font=(.*?)\](.*?)\[\/font\]/sm", "<span style=\"font-family: $1;\">$2</span>", $text);
return $text;
}
private static function convertTablesToHtml(string $text): string
{
$text = preg_replace("/\[th\](.*?)\[\/th\]/sm", '<th>$1</th>', $text);
$text = preg_replace("/\[td\](.*?)\[\/td\]/sm", '<td>$1</td>', $text);
$text = preg_replace("/\[tr\](.*?)\[\/tr\]/sm", '<tr>$1</tr>', $text);
$text = preg_replace("/\[table\](.*?)\[\/table\]/sm", '</p><table>$1</table><p>', $text);
$text = preg_replace("/\[table border=1\](.*?)\[\/table\]/sm", '</p><table border="1" >$1</table><p>', $text);
$text = preg_replace("/\[table border=0\](.*?)\[\/table\]/sm", '</p><table border="0" >$1</table><p>', $text);
return $text;
}
private static function convertListsToHtml(string $text): string
{
// handle nested lists // handle nested lists
$endlessloop = 0; $endlessloop = 0;
@ -1648,25 +1752,11 @@ class BBCode
$text = str_replace("[*]", "<li>", $text); $text = str_replace("[*]", "<li>", $text);
$text = str_replace("[li]", "<li>", $text); $text = str_replace("[li]", "<li>", $text);
$text = preg_replace("/\[th\](.*?)\[\/th\]/sm", '<th>$1</th>', $text); return $text;
$text = preg_replace("/\[td\](.*?)\[\/td\]/sm", '<td>$1</td>', $text);
$text = preg_replace("/\[tr\](.*?)\[\/tr\]/sm", '<tr>$1</tr>', $text);
$text = preg_replace("/\[table\](.*?)\[\/table\]/sm", '</p><table>$1</table><p>', $text);
$text = preg_replace("/\[table border=1\](.*?)\[\/table\]/sm", '</p><table border="1" >$1</table><p>', $text);
$text = preg_replace("/\[table border=0\](.*?)\[\/table\]/sm", '</p><table border="0" >$1</table><p>', $text);
$text = str_replace('[hr]', '</p><hr /><p>', $text);
if (!$for_plaintext) {
$text = self::performWithEscapedTags($text, ['url', 'img', 'audio', 'video', 'youtube', 'vimeo', 'share', 'attachment', 'iframe', 'bookmark'], function ($text) {
return preg_replace(Strings::autoLinkRegEx(), '[url]$1[/url]', $text);
});
} }
// Check for font change text private static function convertSpoilersToHtml(string $text): string
$text = preg_replace("/\[font=(.*?)\](.*?)\[\/font\]/sm", "<span style=\"font-family: $1;\">$2</span>", $text); {
// Declare the format for [spoiler] layout // Declare the format for [spoiler] layout
$SpoilerLayout = '<details class="spoiler"><summary>' . DI::l10n()->t('Click to open/close') . '</summary>$1</details>'; $SpoilerLayout = '<details class="spoiler"><summary>' . DI::l10n()->t('Click to open/close') . '</summary>$1</details>';
@ -1689,6 +1779,28 @@ class BBCode
); );
} }
return $text;
}
private static function convertStructuresToHtml(string $text): string
{
$text = preg_replace("(\[p\](.*?)\[\/p\])ism", '<p>$1</p>', $text);
// Check for paragraph
return str_replace('[hr]', '</p><hr /><p>', $text);
}
private static function convertSmileysToHtml(string $text, int $simple_html, bool $for_plaintext): string
{
if (strpos($text, '[nosmile]') !== false) {
$text = str_replace('[nosmile]', '', $text);
return $text;
}
return Smilies::replace($text, ($simple_html != self::INTERNAL) || $for_plaintext);
}
private static function convertQuotesToHtml(string $text): string
{
// Declare the format for [quote] layout // Declare the format for [quote] layout
$QuoteLayout = '</p><blockquote>$1</blockquote><p>'; $QuoteLayout = '</p><blockquote>$1</blockquote><p>';
@ -1713,7 +1825,11 @@ class BBCode
); );
} }
return $text;
}
private static function convertImagesToHtml(string $text, int $simple_html, int $uriid): string
{
// [img=widthxheight]image source[/img] // [img=widthxheight]image source[/img]
$text = preg_replace_callback( $text = preg_replace_callback(
"/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", "/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism",
@ -1766,10 +1882,18 @@ class BBCode
$text = self::convertImages($text, $simple_html, $uriid); $text = self::convertImages($text, $simple_html, $uriid);
return $text;
}
private static function convertCryptToHtml(string $text): string
{
$text = preg_replace("/\[crypt\](.*?)\[\/crypt\]/ism", '<br><img src="' . DI::baseUrl() . '/images/lock_icon.gif" alt="' . DI::l10n()->t('Encrypted content') . '" title="' . DI::l10n()->t('Encrypted content') . '" /><br>', $text); $text = preg_replace("/\[crypt\](.*?)\[\/crypt\]/ism", '<br><img src="' . DI::baseUrl() . '/images/lock_icon.gif" alt="' . DI::l10n()->t('Encrypted content') . '" title="' . DI::l10n()->t('Encrypted content') . '" /><br>', $text);
$text = preg_replace("/\[crypt(.*?)\](.*?)\[\/crypt\]/ism", '<br><img src="' . DI::baseUrl() . '/images/lock_icon.gif" alt="' . DI::l10n()->t('Encrypted content') . '" title="' . '$1' . ' ' . DI::l10n()->t('Encrypted content') . '" /><br>', $text); $text = preg_replace("/\[crypt(.*?)\](.*?)\[\/crypt\]/ism", '<br><img src="' . DI::baseUrl() . '/images/lock_icon.gif" alt="' . DI::l10n()->t('Encrypted content') . '" title="' . '$1' . ' ' . DI::l10n()->t('Encrypted content') . '" /><br>', $text);
//$text = preg_replace("/\[crypt=(.*?)\](.*?)\[\/crypt\]/ism", '<br><img src="' .DI::baseUrl() . '/images/lock_icon.gif" alt="' . DI::l10n()->t('Encrypted content') . '" title="' . '$1' . ' ' . DI::l10n()->t('Encrypted content') . '" /><br>', $text); return $text;
}
private static function convertAudioVideoToHtml(string $text, int $simple_html, bool $try_oembed, \Closure $try_oembed_callback): string
{
// Simplify "video" element // Simplify "video" element
$text = preg_replace('(\[video[^\]]*?\ssrc\s?=\s?([^\s\]]+)[^\]]*?\].*?\[/video\])ism', '[video]$1[/video]', $text); $text = preg_replace('(\[video[^\]]*?\ssrc\s?=\s?([^\s\]]+)[^\]]*?\].*?\[/video\])ism', '[video]$1[/video]', $text);
@ -1798,79 +1922,58 @@ class BBCode
$text = preg_replace_callback("/\[video\](.*?)\[\/video\]/ism", $try_oembed_callback, $text); $text = preg_replace_callback("/\[video\](.*?)\[\/video\]/ism", $try_oembed_callback, $text);
$text = preg_replace_callback("/\[audio\](.*?)\[\/audio\]/ism", $try_oembed_callback, $text); $text = preg_replace_callback("/\[audio\](.*?)\[\/audio\]/ism", $try_oembed_callback, $text);
$text = preg_replace( $text = preg_replace("/\[video\](.*?)\[\/video\]/ism", '[url]$1[/url]', $text);
"/\[video\](.*?)\[\/video\]/ism",
'<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>',
$text
);
$text = preg_replace("/\[audio\](.*?)\[\/audio\]/ism", '<audio src="$1" controls><a href="$1">$1</a></audio>', $text); $text = preg_replace("/\[audio\](.*?)\[\/audio\]/ism", '<audio src="$1" controls><a href="$1">$1</a></audio>', $text);
} else { } else {
$text = preg_replace( $text = preg_replace("/\[video\](.*?)\[\/video\]/ism", '[url]$1[/url]', $text);
"/\[video\](.*?)\[\/video\]/ism", $text = preg_replace("/\[audio\](.*?)\[\/audio\]/ism", '[url]$1[/url]', $text);
'<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>', }
$text return $text;
);
$text = preg_replace(
"/\[audio\](.*?)\[\/audio\]/ism",
'<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>',
$text
);
} }
private static function convertIFramesToHtml(string $text): string
{
// Backward compatibility, [iframe] support has been removed in version 2020.12 // Backward compatibility, [iframe] support has been removed in version 2020.12
$text = preg_replace_callback("/\[(iframe)\](.*?)\[\/iframe\]/ism", [self::class, 'sanitizeLinksCallback'], $text); $text = preg_replace_callback("/\[(iframe)\](.*?)\[\/iframe\]/ism", [self::class, 'sanitizeLinksCallback'], $text);
$text = preg_replace("/\[iframe\](.*?)\[\/iframe\]/ism", '<a href="$1">$1</a>', $text); $text = preg_replace("/\[iframe\](.*?)\[\/iframe\]/ism", '[url]$1[/url]', $text);
return $text;
}
private static function convertVideoPlatformsToHtml(string $text, bool $try_oembed): string
{
$a = DI::app();
$text = self::normalizeVideoLinks($text); $text = self::normalizeVideoLinks($text);
// Youtube extensions // Youtube extensions
if ($try_oembed && OEmbed::isAllowedURL('https://www.youtube.com/embed/')) { if ($try_oembed && OEmbed::isAllowedURL('https://www.youtube.com/embed/')) {
$text = preg_replace("/\[youtube\]([A-Za-z0-9\-_=]+)(.*?)\[\/youtube\]/ism", '<iframe width="' . $a->getThemeInfoValue('videowidth') . '" height="' . $a->getThemeInfoValue('videoheight') . '" src="https://www.youtube.com/embed/$1" frameborder="0" ></iframe>', $text); $text = preg_replace("/\[youtube\]([A-Za-z0-9\-_=]+)(.*?)\[\/youtube\]/ism", '<iframe width="' . $a->getThemeInfoValue('videowidth') . '" height="' . $a->getThemeInfoValue('videoheight') . '" src="https://www.youtube.com/embed/$1" frameborder="0" ></iframe>', $text);
} else { } else {
$text = preg_replace( $text = preg_replace("/\[youtube\]([A-Za-z0-9\-_=]+)(.*?)\[\/youtube\]/ism", '[url]https://www.youtube.com/watch?v=$1[/url]', $text);
"/\[youtube\]([A-Za-z0-9\-_=]+)(.*?)\[\/youtube\]/ism",
'<a href="https://www.youtube.com/watch?v=$1" target="_blank" rel="noopener noreferrer">https://www.youtube.com/watch?v=$1</a>',
$text
);
} }
// Vimeo extensions // Vimeo extensions
if ($try_oembed && OEmbed::isAllowedURL('https://player.vimeo.com/video')) { if ($try_oembed && OEmbed::isAllowedURL('https://player.vimeo.com/video')) {
$text = preg_replace("/\[vimeo\]([0-9]+)(.*?)\[\/vimeo\]/ism", '<iframe width="' . $a->getThemeInfoValue('videowidth') . '" height="' . $a->getThemeInfoValue('videoheight') . '" src="https://player.vimeo.com/video/$1" frameborder="0" ></iframe>', $text); $text = preg_replace("/\[vimeo\]([0-9]+)(.*?)\[\/vimeo\]/ism", '<iframe width="' . $a->getThemeInfoValue('videowidth') . '" height="' . $a->getThemeInfoValue('videoheight') . '" src="https://player.vimeo.com/video/$1" frameborder="0" ></iframe>', $text);
} else { } else {
$text = preg_replace( $text = preg_replace("/\[vimeo\]([0-9]+)(.*?)\[\/vimeo\]/ism", '[url]https://vimeo.com/$1[/url]', $text);
"/\[vimeo\]([0-9]+)(.*?)\[\/vimeo\]/ism", }
'<a href="https://vimeo.com/$1" target="_blank" rel="noopener noreferrer">https://vimeo.com/$1</a>', return $text;
$text
);
} }
private static function convertOEmbedToHtml(string $text, int $uriid): string
{
// oembed tag // oembed tag
$text = OEmbed::BBCode2HTML($text, $uriid); $text = OEmbed::BBCode2HTML($text, $uriid);
// Avoid triple linefeeds through oembed // Avoid triple linefeeds through oembed
$text = str_replace("<br style='clear:left'></span><br><br>", "<br style='clear:left'></span><br>", $text); $text = str_replace("<br style='clear:left'></span><br><br>", "<br style='clear:left'></span><br>", $text);
// If we found an event earlier, strip out all the event code and replace with a reformatted version. return $text;
// Replace the event-start section with the entire formatted event. The other bbcode is stripped.
// Summary (e.g. title) is required, earlier revisions only required description (in addition to
// start which is always required). Allow desc with a missing summary for compatibility.
if ((!empty($ev['desc']) || !empty($ev['summary'])) && !empty($ev['start'])) {
$sub = Event::getHTML($ev, $simple_html, $uriid);
$text = preg_replace("/\[event\-summary\](.*?)\[\/event\-summary\]/ism", '', $text);
$text = preg_replace("/\[event\-description\](.*?)\[\/event\-description\]/ism", '', $text);
$text = preg_replace("/\[event\-start\](.*?)\[\/event\-start\]/ism", $sub, $text);
$text = preg_replace("/\[event\-finish\](.*?)\[\/event\-finish\]/ism", '', $text);
$text = preg_replace("/\[event\-location\](.*?)\[\/event\-location\]/ism", '', $text);
$text = preg_replace("/\[event\-id\](.*?)\[\/event\-id\]/ism", '', $text);
}
if (!$for_plaintext && DI::config()->get('system', 'big_emojis') && ($simple_html != self::DIASPORA) && Smilies::isEmojiPost($text)) {
$text = '<span style="font-size: xx-large; line-height: normal;">' . $text . '</span>';
} }
private static function convertUrlToHtml(string $text, int $simple_html, bool $for_plaintext, bool $try_oembed, \Closure $try_oembed_callback): string
{
$text = preg_replace_callback("/\[(url)\](.*?)\[\/url\]/ism", [self::class, 'sanitizeLinksCallback'], $text); $text = preg_replace_callback("/\[(url)\](.*?)\[\/url\]/ism", [self::class, 'sanitizeLinksCallback'], $text);
$text = preg_replace_callback("/\[(url)\=(.*?)\](.*?)\[\/url\]/ism", [self::class, 'sanitizeLinksCallback'], $text); $text = preg_replace_callback("/\[(url)\=(.*?)\](.*?)\[\/url\]/ism", [self::class, 'sanitizeLinksCallback'], $text);
@ -1914,12 +2017,7 @@ class BBCode
$text = preg_replace("/([#@!])\[url\=(.*?)\](.*?)\[\/url\]/ism", '$1$3', $text); $text = preg_replace("/([#@!])\[url\=(.*?)\](.*?)\[\/url\]/ism", '$1$3', $text);
} }
if (!$for_plaintext) { if ($for_plaintext) {
if (in_array($simple_html, [self::OSTATUS, self::MASTODON_API, self::TWITTER_API, self::ACTIVITYPUB])) {
$text = preg_replace_callback("/\[url\](.*?)\[\/url\]/ism", [self::class, 'convertUrlForActivityPubCallback'], $text);
$text = preg_replace_callback("/\[url\=(.*?)\](.*?)\[\/url\]/ism", [self::class, 'convertUrlForActivityPubCallback'], $text);
}
} else {
$text = preg_replace("(\[url\](.*?)\[\/url\])ism", " $1 ", $text); $text = preg_replace("(\[url\](.*?)\[\/url\])ism", " $1 ", $text);
$text = preg_replace_callback("&\[url=([^\[\]]*)\]\[img\](.*)\[\/img\]\[\/url\]&Usi", [self::class, 'removePictureLinksCallback'], $text); $text = preg_replace_callback("&\[url=([^\[\]]*)\]\[img\](.*)\[\/img\]\[\/url\]&Usi", [self::class, 'removePictureLinksCallback'], $text);
} }
@ -1988,29 +2086,121 @@ class BBCode
return $text; return $text;
}); });
// Red compatibility, though the link can't be authenticated on Friendica
$text = preg_replace("/\[zrl\=(.*?)\](.*?)\[\/zrl\]/ism", '[url=$1]$2[/url]', $text);
if (in_array($simple_html, [self::INTERNAL, self::EXTERNAL, self::DIASPORA, self::OSTATUS, self::MASTODON_API, self::TWITTER_API, self::ACTIVITYPUB])) {
$text = self::shortenLinkDescription($text, $simple_html);
} else {
$text = self::unifyLinks($text);
}
// We need no target="_blank" rel="noopener noreferrer" for local links // We need no target="_blank" rel="noopener noreferrer" for local links
// convert links start with DI::baseUrl() as local link without the target="_blank" rel="noopener noreferrer" attribute // convert links start with DI::baseUrl() as local link without the target="_blank" rel="noopener noreferrer" attribute
$escapedBaseUrl = preg_quote(DI::baseUrl(), '/'); $text = preg_replace("/\[url\=(" . preg_quote(DI::baseUrl(), '/') . ".*?)\](.*?)\[\/url\]/ism", '<a href="$1">$2</a>', $text);
$text = preg_replace("/\[url\](" . $escapedBaseUrl . ".*?)\[\/url\]/ism", '<a href="$1">$1</a>', $text);
$text = preg_replace("/\[url\=(" . $escapedBaseUrl . ".*?)\](.*?)\[\/url\]/ism", '<a href="$1">$2</a>', $text);
$text = preg_replace("/\[url\](.*?)\[\/url\]/ism", '<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>', $text);
$text = preg_replace("/\[url\=(.*?)\](.*?)\[\/url\]/ism", '<a href="$1" target="_blank" rel="noopener noreferrer">$2</a>', $text); $text = preg_replace("/\[url\=(.*?)\](.*?)\[\/url\]/ism", '<a href="$1" target="_blank" rel="noopener noreferrer">$2</a>', $text);
// Red compatibility, though the link can't be authenticated on Friendica
$text = preg_replace("/\[zrl\=(.*?)\](.*?)\[\/zrl\]/ism", '<a href="$1" target="_blank" rel="noopener noreferrer">$2</a>', $text);
// we may need to restrict this further if it picks up too many strays // we may need to restrict this further if it picks up too many strays
// link acct:user@host to a webfinger profile redirector // link acct:user@host to a webfinger profile redirector
return preg_replace('/acct:([^@]+)@((?!\-)(?:[a-zA-Z\d\-]{0,62}[a-zA-Z\d]\.){1,126}(?!\d+)[a-zA-Z\d]{1,63})/', '<a href="' . DI::baseUrl() . '/acctlink?addr=$1@$2" target="extlink">acct:$1@$2</a>', $text);
}
$text = preg_replace('/acct:([^@]+)@((?!\-)(?:[a-zA-Z\d\-]{0,62}[a-zA-Z\d]\.){1,126}(?!\d+)[a-zA-Z\d]{1,63})/', '<a href="' . DI::baseUrl() . '/acctlink?addr=$1@$2" target="extlink">acct:$1@$2</a>', $text); private static function escapeUrl(string $url): string
{
return self::escapeContent(self::idnUrl($url));
}
// Perform MAIL Search private static function escapeContent(string $url): string
{
return str_replace(['[', ']'], ['&#91;', '&#93;'], $url);
}
private static function idnUrl(string $url): string
{
$parts = parse_url($url);
if (empty($parts['host'])) {
return $url;
}
$parts['host'] = idn_to_ascii(urldecode($parts['host']));
try {
return (string)Uri::fromParts($parts);
} catch (\Throwable $th) {
Logger::notice('Exception on unparsing url', ['url' => $url, 'parts' => $parts, 'code' => $th->getCode(), 'message' => $th->getMessage()]);
return $url;
}
}
private static function unifyLinks(string $text): string
{
return preg_replace_callback(
"/\[url\](.*?)\[\/url\]/ism",
function ($match) {
return "[url=" . self::escapeUrl($match[1]) . "]" . $match[1] . "[/url]";
},
$text
);
}
private static function shortenLinkDescription(string $text, int $simple_html): string
{
if ($simple_html == self::INTERNAL) {
$max_length = DI::config()->get('system', 'display_link_length');
} else {
$max_length = 30;
}
$text = preg_replace_callback(
"/\[url\](.*?)\[\/url\]/ism",
function ($match) use ($max_length) {
return "[url=" . self::escapeUrl($match[1]) . "]" . Strings::getStyledURL($match[1], $max_length) . "[/url]";
},
$text
);
$text = preg_replace_callback(
"/\[url\=(.*?)\](.*?)\[\/url\]/ism",
function ($match) use ($max_length) {
if ($match[1] == $match[2]) {
return "[url=" . self::escapeUrl($match[1]) . "]" . Strings::getStyledURL($match[2], $max_length) . "[/url]";
} else {
return "[url=" . self::escapeUrl($match[1]) . "]" . $match[2] . "[/url]";
}
},
$text
);
return $text;
}
private static function convertMailToHtml(string $text): string
{
$text = preg_replace_callback("/\[(mail)\](.*?)\[\/mail\]/ism", [self::class, 'sanitizeLinksCallback'], $text); $text = preg_replace_callback("/\[(mail)\](.*?)\[\/mail\]/ism", [self::class, 'sanitizeLinksCallback'], $text);
$text = preg_replace("/\[mail\](.*?)\[\/mail\]/", '<a href="mailto:$1">$1</a>', $text); $text = preg_replace("/\[mail\](.*?)\[\/mail\]/", '<a href="mailto:$1">$1</a>', $text);
$text = preg_replace("/\[mail\=(.*?)\](.*?)\[\/mail\]/", '<a href="mailto:$1">$2</a>', $text); $text = preg_replace("/\[mail\=(.*?)\](.*?)\[\/mail\]/", '<a href="mailto:$1">$2</a>', $text);
return $text;
}
private static function convertSharesToHtml(string $text, int $simple_html, bool $try_oembed, int $uriid): string
{
// Shared content
// when the content is meant exporting to other systems then remove the avatar picture since this doesn't really look good on these systems
if (!$try_oembed) {
$text = preg_replace("/\[share(.*?)avatar\s?=\s?'.*?'\s?(.*?)\]\s?(.*?)\s?\[\/share\]\s?/ism", "\n[share$1$2]$3[/share]", $text);
}
$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);
},
$uriid
);
return $text;
}
private static function cleanupHtml(string $text): string
{
/// @todo What is the meaning of these lines? /// @todo What is the meaning of these lines?
$text = preg_replace('/\[\&amp\;([#a-z0-9]+)\;\]/', '&$1;', $text); $text = preg_replace('/\[\&amp\;([#a-z0-9]+)\;\]/', '&$1;', $text);
$text = preg_replace('/\&\#039\;/', '\'', $text); $text = preg_replace('/\&\#039\;/', '\'', $text);
@ -2051,65 +2241,7 @@ class BBCode
$regex = '#<([^>]*?)(href)="(?!' . implode('|', $allowed_link_protocols) . ')(.*?)"(.*?)>#ism'; $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="' . DI::l10n()->t('Invalid link protocol') . '">', $text); $text = preg_replace($regex, '<$1$2="javascript:void(0)"$4 data-original-href="$3" class="invalid-href" title="' . DI::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);
},
$uriid
);
$text = self::interpolateSavedImagesIntoItemBody($uriid, $text, $saved_image);
return $text; return $text;
}); // Escaped noparse, nobb, pre
// Remove escaping tags and replace new lines that remain
$text = preg_replace_callback('/\[(noparse|nobb)](.*?)\[\/\1]/ism', function ($match) {
return str_replace("\n", "<br>", $match[2]);
}, $text);
// Additionally, [pre] tags preserve spaces
$text = preg_replace_callback("/\[pre\](.*?)\[\/pre\]/ism", function ($match) {
return str_replace([' ', "\n"], ['&nbsp;', "<br>"], htmlentities($match[1], ENT_NOQUOTES, 'UTF-8'));
}, $text);
return $text;
}); // Escaped code
$text = preg_replace_callback(
"#\[code(?:=([^\]]*))?\](.*?)\[\/code\]#ism",
function ($matches) {
if (strpos($matches[2], "\n") !== false) {
$return = '<pre><code class="language-' . trim($matches[1]) . '">' . htmlentities(trim($matches[2], "\n\r"), ENT_NOQUOTES, 'UTF-8') . '</code></pre>';
} else {
$return = '<code>' . htmlentities($matches[2], ENT_NOQUOTES, 'UTF-8') . '</code>';
}
return $return;
},
$text
);
// Default iframe allowed domains/path
$allowedIframeDomains = DI::config()->get('system', 'no_oembed_rich_content') ? [] : ['www.youtube.com/embed/', 'player.vimeo.com/video/'];
$allowedIframeDomains = array_merge(
$allowedIframeDomains,
DI::config()->get('system', 'allowed_oembed') ?
explode(',', DI::config()->get('system', 'allowed_oembed'))
: []
);
if (strpos($text, '<p>') !== false || strpos($text, '</p>') !== false) {
$text = '<p>' . $text . '</p>';
}
$text = HTML::purify($text, $allowedIframeDomains);
DI::profiler()->stopRecording();
return trim($text);
} }
/** /**
@ -2520,7 +2652,7 @@ class BBCode
$bbcode = "\n" . '[audio]' . $url . '[/audio]' . "\n"; $bbcode = "\n" . '[audio]' . $url . '[/audio]' . "\n";
break; break;
default: default:
$bbcode = "\n" . '[img]' . $url . '[/img]' . "\n"; $bbcode = "\n" . '[img=' . $url . '][/img]' . "\n";
break; break;
} }

View file

@ -29,9 +29,9 @@ use Friendica\Core\Renderer;
use Friendica\Core\Search; use Friendica\Core\Search;
use Friendica\DI; use Friendica\DI;
use Friendica\Model\Contact; use Friendica\Model\Contact;
use Friendica\Util\Network;
use Friendica\Util\Strings; use Friendica\Util\Strings;
use Friendica\Util\XML; use Friendica\Util\XML;
use GuzzleHttp\Psr7\Uri;
use League\HTMLToMarkdown\HtmlConverter; use League\HTMLToMarkdown\HtmlConverter;
use Psr\Http\Message\UriInterface; use Psr\Http\Message\UriInterface;
@ -253,13 +253,14 @@ class HTML
self::tagToBBCode($doc, 'span', ['class' => 'type-link'], '[class=type-link]', '[/class]'); self::tagToBBCode($doc, 'span', ['class' => 'type-link'], '[class=type-link]', '[/class]');
self::tagToBBCode($doc, 'span', ['class' => 'type-video'], '[class=type-video]', '[/class]'); self::tagToBBCode($doc, 'span', ['class' => 'type-video'], '[class=type-video]', '[/class]');
self::tagToBBCode($doc, 'strong', [], '[b]', '[/b]'); $elements = [
self::tagToBBCode($doc, 'em', [], '[i]', '[/i]'); 'b', 'del', 'em', 'i', 'ins', 'kbd', 'mark',
self::tagToBBCode($doc, 'b', [], '[b]', '[/b]'); 's', 'samp', 'strong', 'sub', 'sup', 'u', 'var'
self::tagToBBCode($doc, 'i', [], '[i]', '[/i]'); ];
self::tagToBBCode($doc, 'u', [], '[u]', '[/u]'); foreach ($elements as $element) {
self::tagToBBCode($doc, 's', [], '[s]', '[/s]'); self::tagToBBCode($doc, $element, [], '[' . $element . ']', '[/' . $element . ']');
self::tagToBBCode($doc, 'del', [], '[s]', '[/s]'); }
self::tagToBBCode($doc, 'strike', [], '[s]', '[/s]'); self::tagToBBCode($doc, 'strike', [], '[s]', '[/s]');
self::tagToBBCode($doc, 'big', [], "[size=large]", "[/size]"); self::tagToBBCode($doc, 'big', [], "[size=large]", "[/size]");
@ -406,7 +407,7 @@ class HTML
} }
$parts = array_merge($base, parse_url($url)); $parts = array_merge($base, parse_url($url));
$url2 = Network::unparseURL($parts); $url2 = (string)Uri::fromParts((array)$parts);
return str_replace($url, $url2, $link); return str_replace($url, $url2, $link);
} }
@ -867,7 +868,7 @@ class HTML
'$save_label' => $save_label, '$save_label' => $save_label,
'$search_hint' => DI::l10n()->t('@name, !group, #tags, content'), '$search_hint' => DI::l10n()->t('@name, !group, #tags, content'),
'$mode' => $mode, '$mode' => $mode,
'$return_url' => urlencode(Search::getSearchPath($s)), '$return_url' => bin2hex(Search::getSearchPath($s)),
]; ];
if (!$aside) { if (!$aside) {
@ -1060,4 +1061,15 @@ class HTML
return null; return null;
} }
/**
* Check if a document contains HTML or entities
*
* @param string $text
* @return boolean
*/
public static function isHTML(string $text): bool
{
return ($text != html_entity_decode($text)) || ($text != strip_tags($text));
}
} }

View file

@ -32,7 +32,7 @@ use Friendica\Model\Contact;
use Friendica\Model\Circle; use Friendica\Model\Circle;
use Friendica\Model\Item; use Friendica\Model\Item;
use Friendica\Model\Post; use Friendica\Model\Post;
use Friendica\Model\Profile; use Friendica\Security\OpenWebAuth;
use Friendica\Util\DateTimeFormat; use Friendica\Util\DateTimeFormat;
use Friendica\Util\Temporal; use Friendica\Util\Temporal;
@ -67,7 +67,7 @@ class Widget
if (DI::config()->get('system', 'invitation_only')) { if (DI::config()->get('system', 'invitation_only')) {
$x = intval(DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'invites_remaining')); $x = intval(DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'invites_remaining'));
if ($x || DI::app()->isSiteAdmin()) { if ($x || DI::userSession()->isSiteAdmin()) {
DI::page()['aside'] .= '<div class="side-link widget" id="side-invite-remain">' DI::page()['aside'] .= '<div class="side-link widget" id="side-invite-remain">'
. DI::l10n()->tt('%d invitation available', '%d invitations available', $x) . DI::l10n()->tt('%d invitation available', '%d invitations available', $x)
. '</div>'; . '</div>';
@ -85,7 +85,7 @@ class Widget
$nv['random'] = DI::l10n()->t('Random Profile'); $nv['random'] = DI::l10n()->t('Random Profile');
$nv['inv'] = DI::l10n()->t('Invite Friends'); $nv['inv'] = DI::l10n()->t('Invite Friends');
$nv['directory'] = DI::l10n()->t('Global Directory'); $nv['directory'] = DI::l10n()->t('Global Directory');
$nv['global_dir'] = Profile::zrl($global_dir, true); $nv['global_dir'] = OpenWebAuth::getZrlUrl($global_dir, true);
$nv['local_directory'] = DI::l10n()->t('Local Directory'); $nv['local_directory'] = DI::l10n()->t('Local Directory');
$aside = []; $aside = [];
@ -336,7 +336,7 @@ class Widget
*/ */
public static function categories(int $uid, string $baseurl, string $selected = ''): string public static function categories(int $uid, string $baseurl, string $selected = ''): string
{ {
if (!Feature::isEnabled($uid, 'categories')) { if (!Feature::isEnabled($uid, Feature::CATEGORIES)) {
return ''; return '';
} }
@ -398,7 +398,7 @@ class Widget
$entries[] = [ $entries[] = [
'url' => Contact::magicLinkByContact($contact), 'url' => Contact::magicLinkByContact($contact),
'name' => $contact['name'], 'name' => $contact['name'],
'photo' => Contact::getThumb($contact), 'photo' => Contact::getThumb($contact, true),
]; ];
} }
@ -428,7 +428,7 @@ class Widget
return ''; return '';
} }
if (Feature::isEnabled($uid, 'tagadelic')) { if (Feature::isEnabled($uid, Feature::TAGCLOUD)) {
$owner_id = Contact::getPublicIdByUserId($uid); $owner_id = Contact::getPublicIdByUserId($uid);
if (!$owner_id) { if (!$owner_id) {

View file

@ -61,7 +61,7 @@ class SavedSearches
'$add' => '', '$add' => '',
'$searchbox' => '', '$searchbox' => '',
'$saved' => $saved, '$saved' => $saved,
'$return_url' => urlencode($return_url), '$return_url' => bin2hex($return_url),
]); ]);
} }
} }

View file

@ -21,6 +21,7 @@
namespace Friendica\Content\Widget; namespace Friendica\Content\Widget;
use Friendica\Content\Conversation\Entity\Community;
use Friendica\Core\Renderer; use Friendica\Core\Renderer;
use Friendica\DI; use Friendica\DI;
use Friendica\Model\Tag; use Friendica\Model\Tag;
@ -39,9 +40,9 @@ class TrendingTags
* @return string Formatted HTML code * @return string Formatted HTML code
* @throws \Exception * @throws \Exception
*/ */
public static function getHTML(string $content = 'global', int $period = 24): string public static function getHTML(string $content = Community::GLOBAL, int $period = 24): string
{ {
if ($content == 'local') { if ($content == Community::LOCAL) {
$tags = Tag::getLocalTrendingHashtags($period, 20); $tags = Tag::getLocalTrendingHashtags($period, 20);
} else { } else {
$tags = Tag::getGlobalTrendingHashtags($period, 20); $tags = Tag::getGlobalTrendingHashtags($period, 20);

View file

@ -43,9 +43,10 @@ class VCard
* @template widget/vcard.tpl * @template widget/vcard.tpl
* @param array $contact * @param array $contact
* @param bool $hide_mention * @param bool $hide_mention
* @param bool $hide_follow
* @return string * @return string
*/ */
public static function getHTML(array $contact, bool $hide_mention = false): string public static function getHTML(array $contact, bool $hide_mention = false, bool $hide_follow = false): string
{ {
if (!isset($contact['network']) || !isset($contact['id'])) { if (!isset($contact['network']) || !isset($contact['id'])) {
Logger::warning('Incomplete contact', ['contact' => $contact ?? []]); Logger::warning('Incomplete contact', ['contact' => $contact ?? []]);
@ -87,7 +88,7 @@ class VCard
} }
} }
if (empty($contact['self']) && Protocol::supportsFollow($contact['network'])) { if (!$hide_follow && empty($contact['self']) && Protocol::supportsFollow($contact['network'])) {
if (in_array($rel, [Contact::SHARING, Contact::FRIEND])) { if (in_array($rel, [Contact::SHARING, Contact::FRIEND])) {
$unfollow_link = 'contact/unfollow?url=' . urlencode($contact_url) . '&auto=1'; $unfollow_link = 'contact/unfollow?url=' . urlencode($contact_url) . '&auto=1';
} elseif (!$pending) { } elseif (!$pending) {

View file

@ -140,7 +140,8 @@ class Addon
$func(); $func();
} }
Hook::delete(['file' => 'addon/' . $addon . '/' . $addon . '.php']); // Handles both relative and absolute file paths
Hook::delete(['`file` LIKE ?', "%addon/$addon/$addon.php"]);
unset(self::$addons[array_search($addon, self::$addons)]); unset(self::$addons[array_search($addon, self::$addons)]);
} }

View file

@ -47,6 +47,7 @@ Usage: bin/console [--version] [-h|--help|-?] <command> [<args>] [-v]
Commands: Commands:
addon Addon management addon Addon management
cache Manage node cache cache Manage node cache
clearavatarcache Clear the file based avatar cache
config Edit site config config Edit site config
contact Contact management contact Contact management
createdoxygen Generate Doxygen headers createdoxygen Generate Doxygen headers
@ -84,6 +85,7 @@ HELP;
'archivecontact' => Friendica\Console\ArchiveContact::class, 'archivecontact' => Friendica\Console\ArchiveContact::class,
'autoinstall' => Friendica\Console\AutomaticInstallation::class, 'autoinstall' => Friendica\Console\AutomaticInstallation::class,
'cache' => Friendica\Console\Cache::class, 'cache' => Friendica\Console\Cache::class,
'clearavatarcache' => Friendica\Console\ClearAvatarCache::class,
'config' => Friendica\Console\Config::class, 'config' => Friendica\Console\Config::class,
'contact' => Friendica\Console\Contact::class, 'contact' => Friendica\Console\Contact::class,
'createdoxygen' => Friendica\Console\CreateDoxygen::class, 'createdoxygen' => Friendica\Console\CreateDoxygen::class,

View file

@ -495,6 +495,13 @@ class Installer
); );
$returnVal = $returnVal ? $status : false; $returnVal = $returnVal ? $status : false;
$status = $this->checkFunction('idn_to_ascii',
DI::l10n()->t('IDN Functions PHP module'),
DI::l10n()->t('Error: IDN Functions PHP module required but not installed.'),
true
);
$returnVal = $returnVal ? $status : false;
return $returnVal; return $returnVal;
} }

View file

@ -75,8 +75,11 @@ class Renderer
{ {
DI::profiler()->startRecording('rendering'); DI::profiler()->startRecording('rendering');
// pass $baseurl to all templates if it isn't set // Default template variables
$vars = array_merge(['$baseurl' => DI::baseUrl(), '$APP' => DI::app()], $vars); $vars = array_merge([
'$baseurl' => DI::baseUrl(),
'$VERSION' => \Friendica\App::VERSION,
], $vars);
$t = self::getTemplateEngine(); $t = self::getTemplateEngine();
@ -84,7 +87,7 @@ class Renderer
$output = $t->replaceMacros($template, $vars); $output = $t->replaceMacros($template, $vars);
} catch (Exception $e) { } catch (Exception $e) {
DI::logger()->critical($e->getMessage(), ['template' => $template, 'vars' => $vars]); DI::logger()->critical($e->getMessage(), ['template' => $template, 'vars' => $vars]);
$message = DI::app()->isSiteAdmin() ? $message = DI::userSession()->isSiteAdmin() ?
$e->getMessage() : $e->getMessage() :
DI::l10n()->t('Friendica can\'t display this page at the moment, please contact the administrator.'); DI::l10n()->t('Friendica can\'t display this page at the moment, please contact the administrator.');
throw new ServiceUnavailableException($message); throw new ServiceUnavailableException($message);
@ -113,7 +116,7 @@ class Renderer
$template = $t->getTemplateFile($file, $subDir); $template = $t->getTemplateFile($file, $subDir);
} catch (Exception $e) { } catch (Exception $e) {
DI::logger()->critical($e->getMessage(), ['file' => $file, 'subDir' => $subDir]); DI::logger()->critical($e->getMessage(), ['file' => $file, 'subDir' => $subDir]);
$message = DI::app()->isSiteAdmin() ? $message = DI::userSession()->isSiteAdmin() ?
$e->getMessage() : $e->getMessage() :
DI::l10n()->t('Friendica can\'t display this page at the moment, please contact the administrator.'); DI::l10n()->t('Friendica can\'t display this page at the moment, please contact the administrator.');
throw new ServiceUnavailableException($message); throw new ServiceUnavailableException($message);
@ -142,7 +145,7 @@ class Renderer
} else { } else {
$admin_message = DI::l10n()->t('template engine cannot be registered without a name.'); $admin_message = DI::l10n()->t('template engine cannot be registered without a name.');
DI::logger()->critical($admin_message, ['class' => $class]); DI::logger()->critical($admin_message, ['class' => $class]);
$message = DI::app()->isSiteAdmin() ? $message = DI::userSession()->isSiteAdmin() ?
$admin_message : $admin_message :
DI::l10n()->t('Friendica can\'t display this page at the moment, please contact the administrator.'); DI::l10n()->t('Friendica can\'t display this page at the moment, please contact the administrator.');
throw new ServiceUnavailableException($message); throw new ServiceUnavailableException($message);
@ -176,7 +179,7 @@ class Renderer
$admin_message = DI::l10n()->t('template engine is not registered!'); $admin_message = DI::l10n()->t('template engine is not registered!');
DI::logger()->critical($admin_message, ['template_engine' => $template_engine]); DI::logger()->critical($admin_message, ['template_engine' => $template_engine]);
$message = DI::app()->isSiteAdmin() ? $message = DI::userSession()->isSiteAdmin() ?
$admin_message : $admin_message :
DI::l10n()->t('Friendica can\'t display this page at the moment, please contact the administrator.'); DI::l10n()->t('Friendica can\'t display this page at the moment, please contact the administrator.');
throw new ServiceUnavailableException($message); throw new ServiceUnavailableException($message);

View file

@ -24,6 +24,8 @@ namespace Friendica\Core;
use Friendica\DI; use Friendica\DI;
use Friendica\Model\Contact; use Friendica\Model\Contact;
use Friendica\Network\HTTPClient\Client\HttpClientAccept; use Friendica\Network\HTTPClient\Client\HttpClientAccept;
use Friendica\Network\HTTPClient\Client\HttpClientOptions;
use Friendica\Network\HTTPClient\Client\HttpClientRequest;
use Friendica\Network\HTTPException; use Friendica\Network\HTTPException;
use Friendica\Object\Search\ContactResult; use Friendica\Object\Search\ContactResult;
use Friendica\Object\Search\ResultList; use Friendica\Object\Search\ResultList;
@ -125,7 +127,7 @@ class Search
$searchUrl .= '&page=' . $page; $searchUrl .= '&page=' . $page;
} }
$resultJson = DI::httpClient()->fetch($searchUrl, HttpClientAccept::JSON); $resultJson = DI::httpClient()->fetch($searchUrl, HttpClientAccept::JSON, 0, '', HttpClientRequest::CONTACTDISCOVER);
$results = json_decode($resultJson, true); $results = json_decode($resultJson, true);
@ -232,7 +234,7 @@ class Search
$return = Contact::searchByName($search, $mode, true); $return = Contact::searchByName($search, $mode, true);
} else { } else {
$p = $page > 1 ? 'p=' . $page : ''; $p = $page > 1 ? 'p=' . $page : '';
$curlResult = DI::httpClient()->get(self::getGlobalDirectory() . '/search/people?' . $p . '&q=' . urlencode($search), HttpClientAccept::JSON); $curlResult = DI::httpClient()->get(self::getGlobalDirectory() . '/search/people?' . $p . '&q=' . urlencode($search), HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::CONTACTDISCOVER]);
if ($curlResult->isSuccess()) { if ($curlResult->isSuccess()) {
$searchResult = json_decode($curlResult->getBodyString(), true); $searchResult = json_decode($curlResult->getBodyString(), true);
if (!empty($searchResult['profiles'])) { if (!empty($searchResult['profiles'])) {

View file

@ -80,9 +80,9 @@ interface IHandleUserSessions extends IHandleSessions
public function getMyUrl(): string; public function getMyUrl(): string;
/** /**
* Returns if the current visitor is authenticated * Returns if the current visitor is a local user
* *
* @return bool "true" when visitor is either a local or remote user * @return bool "true" when visitor is a local user
*/ */
public function isAuthenticated(): bool; public function isAuthenticated(): bool;
@ -100,6 +100,20 @@ interface IHandleUserSessions extends IHandleSessions
*/ */
public function isModerator(): bool; public function isModerator(): bool;
/**
* Returns if the current visitor is a verified remote user
*
* @return bool "true" when visitor is a verified remote user
*/
public function isVisitor(): bool;
/**
* Returns if the current visitor is an unauthenticated user
*
* @return bool "true" when visitor is an unauthenticated user
*/
public function isUnauthenticated(): bool;
/** /**
* Returns User ID of the managed user in case it's a different identity * Returns User ID of the managed user in case it's a different identity
* *

View file

@ -130,7 +130,7 @@ class UserSession implements IHandleUserSessions
/** {@inheritDoc} */ /** {@inheritDoc} */
public function isAuthenticated(): bool public function isAuthenticated(): bool
{ {
return $this->session->get('authenticated', false); return $this->session->get('authenticated', false) && $this->getLocalUserId();
} }
/** {@inheritDoc} */ /** {@inheritDoc} */
@ -145,6 +145,18 @@ class UserSession implements IHandleUserSessions
return User::isModerator($this->getLocalUserId()); return User::isModerator($this->getLocalUserId());
} }
/** {@inheritDoc} */
public function isVisitor(): bool
{
return $this->session->get('authenticated', false) && $this->session->get('visitor_id') && !$this->session->get('uid');
}
/** {@inheritDoc} */
public function isUnauthenticated(): bool
{
return !$this->session->get('authenticated', false);
}
/** {@inheritDoc} */ /** {@inheritDoc} */
public function setVisitorsContacts(string $my_url) public function setVisitorsContacts(string $my_url)
{ {

View file

@ -21,6 +21,7 @@
namespace Friendica\Core; namespace Friendica\Core;
use Friendica\Core\Cache\Enum\Duration;
use Friendica\Core\Worker\Entity\Process; use Friendica\Core\Worker\Entity\Process;
use Friendica\Database\DBA; use Friendica\Database\DBA;
use Friendica\DI; use Friendica\DI;
@ -55,6 +56,7 @@ class Worker
const LOCK_PROCESS = 'worker_process'; const LOCK_PROCESS = 'worker_process';
const LOCK_WORKER = 'worker'; const LOCK_WORKER = 'worker';
const LAST_CHECK = 'worker::check';
private static $up_start; private static $up_start;
private static $db_duration = 0; private static $db_duration = 0;
@ -233,7 +235,7 @@ class Worker
* @return integer Number of deferred entries in the worker queue * @return integer Number of deferred entries in the worker queue
* @throws \Exception * @throws \Exception
*/ */
private static function deferredEntries(): int public static function deferredEntries(): int
{ {
$stamp = (float)microtime(true); $stamp = (float)microtime(true);
$count = DBA::count('workerqueue', ["NOT `done` AND `pid` = 0 AND `retrial` > ?", 0]); $count = DBA::count('workerqueue', ["NOT `done` AND `pid` = 0 AND `retrial` > ?", 0]);
@ -248,7 +250,7 @@ class Worker
* @return integer Number of non executed entries in the worker queue * @return integer Number of non executed entries in the worker queue
* @throws \Exception * @throws \Exception
*/ */
private static function totalEntries(): int public static function totalEntries(): int
{ {
$stamp = (float)microtime(true); $stamp = (float)microtime(true);
$count = DBA::count('workerqueue', ['done' => false, 'pid' => 0]); $count = DBA::count('workerqueue', ['done' => false, 'pid' => 0]);
@ -832,6 +834,17 @@ class Worker
} else { } else {
self::spawnWorker(); self::spawnWorker();
} }
} elseif (($active > $queues) && ($active < $maxqueues) && ($load < $maxsysload)) {
$max_idletime = DI::config()->get('system', 'worker_max_idletime');
$last_check = DI::cache()->get(self::LAST_CHECK);
$last_date = $last_check ? date('c', $last_check) : '';
if (($max_idletime > 0) && (time() > $last_check + $max_idletime) && !DBA::exists('workerqueue', ["`done` AND `executed` > ?", DateTimeFormat::utc('now - ' . $max_idletime . ' second')])) {
DI::cache()->set(self::LAST_CHECK, time(), Duration::HOUR);
Logger::info('The last worker execution had been too long ago.', ['last' => $last_check, 'last-check' => $last_date, 'seconds' => $max_idletime, 'load' => $load, 'max_load' => $maxsysload, 'active_worker' => $active, 'max_worker' => $maxqueues]);
return false;
} elseif ($max_idletime > 0) {
Logger::debug('Maximum idletime not reached.', ['last' => $last_check, 'last-check' => $last_date, 'seconds' => $max_idletime, 'load' => $load, 'max_load' => $maxsysload, 'active_worker' => $active, 'max_worker' => $maxqueues]);
}
} }
} }
@ -849,7 +862,7 @@ class Worker
* @return integer Number of active worker processes * @return integer Number of active worker processes
* @throws \Exception * @throws \Exception
*/ */
private static function activeWorkers(): int public static function activeWorkers(): int
{ {
$stamp = (float)microtime(true); $stamp = (float)microtime(true);
$count = DI::process()->countCommand('Worker.php'); $count = DI::process()->countCommand('Worker.php');

View file

@ -66,7 +66,7 @@ abstract class DI
public static function setCompositeRootDependencyByHand() public static function setCompositeRootDependencyByHand()
{ {
$database = static::dba(); $database = static::dba();
$database->setDependency(static::config(), static::profiler(), static::logger()); $database->setDependency(static::config(), static::profiler(), static::logger(), static::lock());
} }
/** /**

View file

@ -21,6 +21,7 @@
namespace Friendica\Database; namespace Friendica\Database;
use Friendica\Core\Lock\Exception\LockPersistenceException;
use Friendica\DI; use Friendica\DI;
use mysqli; use mysqli;
use mysqli_result; use mysqli_result;
@ -823,6 +824,27 @@ class DBA
return DI::dba()->optimizeTable($table); return DI::dba()->optimizeTable($table);
} }
/**
* Acquire a lock to prevent a table optimization
*
* @return bool
* @throws LockPersistenceException
*/
public static function acquireOptimizeLock(): bool
{
return DI::dba()->acquireOptimizeLock();
}
/**
* Release the table optimization lock
* @return bool
* @throws LockPersistenceException
*/
public static function releaseOptimizeLock(): bool
{
return DI::dba()->releaseOptimizeLock();
}
/** /**
* Kill sleeping database processes * Kill sleeping database processes
*/ */

View file

@ -22,6 +22,8 @@
namespace Friendica\Database; namespace Friendica\Database;
use Friendica\Core\Config\Capability\IManageConfigValues; use Friendica\Core\Config\Capability\IManageConfigValues;
use Friendica\Core\Lock\Capability\ICanLock;
use Friendica\Core\Lock\Exception\LockPersistenceException;
use Friendica\Core\System; use Friendica\Core\System;
use Friendica\Database\Definition\DbaDefinition; use Friendica\Database\Definition\DbaDefinition;
use Friendica\Database\Definition\ViewDefinition; use Friendica\Database\Definition\ViewDefinition;
@ -50,6 +52,8 @@ class Database
const INSERT_UPDATE = 1; const INSERT_UPDATE = 1;
const INSERT_IGNORE = 2; const INSERT_IGNORE = 2;
const LOCK_OPTIMIZE = 'database::optimize_tables';
protected $connected = false; protected $connected = false;
/** /**
@ -64,6 +68,11 @@ class Database
* @var LoggerInterface * @var LoggerInterface
*/ */
protected $logger = null; protected $logger = null;
/**
* @var ICanLock
*/
protected $syslock = null;
protected $server_info = ''; protected $server_info = '';
/** @var PDO|mysqli */ /** @var PDO|mysqli */
protected $connection; protected $connection;
@ -106,11 +115,12 @@ class Database
* *
* @todo Make this method obsolete - use a clean pattern instead ... * @todo Make this method obsolete - use a clean pattern instead ...
*/ */
public function setDependency(IManageConfigValues $config, Profiler $profiler, LoggerInterface $logger) public function setDependency(IManageConfigValues $config, Profiler $profiler, LoggerInterface $logger, ICanLock $lock)
{ {
$this->logger = $logger; $this->logger = $logger;
$this->profiler = $profiler; $this->profiler = $profiler;
$this->config = $config; $this->config = $config;
$this->syslock = $lock;
} }
/** /**
@ -1755,7 +1765,42 @@ class Database
*/ */
public function optimizeTable(string $table): bool public function optimizeTable(string $table): bool
{ {
return $this->e("OPTIMIZE TABLE " . DBA::buildTableString([$table])) !== false; if ($this->syslock->isLocked(self::LOCK_OPTIMIZE)) {
$this->logger->info('Optimization is locked');
return false;
}
if (!$this->acquireOptimizeLock()) {
return false;
}
$result = $this->e("OPTIMIZE TABLE " . DBA::buildTableString([$table])) !== false;
$this->releaseOptimizeLock();
return $result;
}
/**
* Acquire a lock to prevent a table optimization
*
* @return bool
* @throws LockPersistenceException
*/
public function acquireOptimizeLock(): bool
{
return $this->syslock->acquire(self::LOCK_OPTIMIZE, 0);
}
/**
* Release the table optimization lock
*
* @return bool
* @throws LockPersistenceException
*/
public function releaseOptimizeLock(): bool
{
return $this->syslock->release(self::LOCK_OPTIMIZE);
} }
/** /**

View file

@ -96,7 +96,6 @@ class Activities extends BaseFactory
break; break;
default: default:
$this->logger->warning('Unsupported verb in parent item:', ['parent_item' => $parent_item]);
break; break;
} }
} }

View file

@ -23,6 +23,7 @@ namespace Friendica\Factory\Api\Mastodon;
use Friendica\App\BaseURL; use Friendica\App\BaseURL;
use Friendica\BaseFactory; use Friendica\BaseFactory;
use Friendica\Model\Attach;
use Friendica\Model\Photo; use Friendica\Model\Photo;
use Friendica\Network\HTTPException; use Friendica\Network\HTTPException;
use Friendica\Model\Post; use Friendica\Model\Post;
@ -144,4 +145,37 @@ class Attachment extends BaseFactory
$object = new \Friendica\Object\Api\Mastodon\Attachment($attachment, 'image', $url, $preview_url, ''); $object = new \Friendica\Object\Api\Mastodon\Attachment($attachment, 'image', $url, $preview_url, '');
return $object->toArray(); return $object->toArray();
} }
/**
* @param int $id id of the attachment
*
* @return array
* @throws HTTPException\InternalServerErrorException
*/
public function createFromAttach(int $id): array
{
$media = Attach::selectFirst(['id', 'filetype'], ['id' => $id]);
if (empty($media)) {
return [];
}
$attachment = [
'id' => 'attach:' . $media['id'],
'description' => null,
'blurhash' => null,
];
$types = [Post\Media::AUDIO => 'audio', Post\Media::VIDEO => 'video', Post\Media::IMAGE => 'image'];
$type = Post\Media::getType($media['filetype']);
$url = $this->baseUrl . '/attach/' . $id;
$object = new \Friendica\Object\Api\Mastodon\Attachment($attachment, $types[$type] ?? 'unknown', $url, '', '');
return $object->toArray();
}
public function isAttach(string $id): bool
{
return substr($id, 0, 7) == 'attach:';
}
} }

View file

@ -22,6 +22,7 @@
namespace Friendica\Factory\Api\Mastodon; namespace Friendica\Factory\Api\Mastodon;
use Friendica\BaseFactory; use Friendica\BaseFactory;
use Friendica\Content\Conversation\Entity\Timeline;
use Friendica\Database\Database; use Friendica\Database\Database;
use Friendica\Network\HTTPException\InternalServerErrorException; use Friendica\Network\HTTPException\InternalServerErrorException;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
@ -45,4 +46,14 @@ class ListEntity extends BaseFactory
$circle = $this->dba->selectFirst('group', ['name'], ['id' => $id, 'deleted' => false]); $circle = $this->dba->selectFirst('group', ['name'], ['id' => $id, 'deleted' => false]);
return new \Friendica\Object\Api\Mastodon\ListEntity($id, $circle['name'] ?? '', 'list'); return new \Friendica\Object\Api\Mastodon\ListEntity($id, $circle['name'] ?? '', 'list');
} }
public function createFromChannel(Timeline $channel): \Friendica\Object\Api\Mastodon\ListEntity
{
return new \Friendica\Object\Api\Mastodon\ListEntity('channel:' . $channel->code, $channel->label, 'followed');
}
public function createFromGroup(array $group): \Friendica\Object\Api\Mastodon\ListEntity
{
return new \Friendica\Object\Api\Mastodon\ListEntity('group:' . $group['id'], $group['name'], 'followed');
}
} }

View file

@ -27,6 +27,7 @@ use Friendica\Content\Item as ContentItem;
use Friendica\Content\Smilies; use Friendica\Content\Smilies;
use Friendica\Content\Text\BBCode; use Friendica\Content\Text\BBCode;
use Friendica\Core\Logger; use Friendica\Core\Logger;
use Friendica\Core\Protocol;
use Friendica\Database\Database; use Friendica\Database\Database;
use Friendica\Database\DBA; use Friendica\Database\DBA;
use Friendica\DI; use Friendica\DI;
@ -105,7 +106,7 @@ class Status extends BaseFactory
*/ */
public function createFromUriId(int $uriId, int $uid = 0, bool $display_quote = false, bool $reblog = true, bool $in_reply_status = true): \Friendica\Object\Api\Mastodon\Status public function createFromUriId(int $uriId, int $uid = 0, bool $display_quote = false, bool $reblog = true, bool $in_reply_status = true): \Friendica\Object\Api\Mastodon\Status
{ {
$fields = ['uri-id', 'uid', 'author-id', 'causer-id', 'author-uri-id', 'author-link', 'causer-uri-id', 'post-reason', 'starred', 'app', 'title', 'body', 'raw-body', 'content-warning', 'question-id', $fields = ['uri-id', 'uid', 'author-id', 'causer-id', 'author-uri-id', 'author-link', 'author-gsid', 'causer-uri-id', 'post-reason', 'starred', 'app', 'title', 'body', 'raw-body', 'content-warning', 'question-id',
'created', 'edited', 'commented', 'received', 'changed', 'network', 'thr-parent-id', 'parent-author-id', 'language', 'uri', 'plink', 'private', 'vid', 'gravity', 'featured', 'has-media', 'quote-uri-id', 'created', 'edited', 'commented', 'received', 'changed', 'network', 'thr-parent-id', 'parent-author-id', 'language', 'uri', 'plink', 'private', 'vid', 'gravity', 'featured', 'has-media', 'quote-uri-id',
'delivery_queue_count', 'delivery_queue_done','delivery_queue_failed', 'allow_cid', 'deny_cid', 'allow_gid', 'deny_gid', 'sensitive']; 'delivery_queue_count', 'delivery_queue_done','delivery_queue_failed', 'allow_cid', 'deny_cid', 'allow_gid', 'deny_gid', 'sensitive'];
$item = Post::selectFirst($fields, ['uri-id' => $uriId, 'uid' => [0, $uid]], ['order' => ['uid' => true]]); $item = Post::selectFirst($fields, ['uri-id' => $uriId, 'uid' => [0, $uid]], ['order' => ['uid' => true]]);
@ -213,7 +214,26 @@ class Status extends BaseFactory
); );
$sensitive = (bool)$item['sensitive']; $sensitive = (bool)$item['sensitive'];
$application = new \Friendica\Object\Api\Mastodon\Application($item['app'] ?: ContactSelector::networkToName($item['network'], $item['author-link']));
$network = ContactSelector::networkToName($item['network']);
$sitename = '';
$platform = '';
$version = '';
if (in_array($item['network'], Protocol::FEDERATED)) {
$gserver = $this->dba->selectFirst('gserver', ['site_name', 'platform', 'version'], ['id' => $item['author-gsid']]);
if (!empty($gserver)) {
$platform = ucfirst($gserver['platform']);
$version = $gserver['version'];
$sitename = $gserver['site_name'];
}
}
if ($platform == '') {
$platform = ContactSelector::networkToName($item['network'], $item['author-link'], $item['network'], $item['author-gsid']);
}
$application = new \Friendica\Object\Api\Mastodon\Application($item['app'] ?: $platform);
$mentions = $this->mstdnMentionFactory->createFromUriId($uriId)->getArrayCopy(); $mentions = $this->mstdnMentionFactory->createFromUriId($uriId)->getArrayCopy();
$tags = $this->mstdnTagFactory->createFromUriId($uriId); $tags = $this->mstdnTagFactory->createFromUriId($uriId);
@ -322,7 +342,7 @@ class Status extends BaseFactory
$delivery_data = $uid != $item['uid'] ? null : new FriendicaDeliveryData($item['delivery_queue_count'], $item['delivery_queue_done'], $item['delivery_queue_failed']); $delivery_data = $uid != $item['uid'] ? null : new FriendicaDeliveryData($item['delivery_queue_count'], $item['delivery_queue_done'], $item['delivery_queue_failed']);
$visibility_data = $uid != $item['uid'] ? null : new FriendicaVisibility($this->aclFormatter->expand($item['allow_cid']), $this->aclFormatter->expand($item['deny_cid']), $this->aclFormatter->expand($item['allow_gid']), $this->aclFormatter->expand($item['deny_gid'])); $visibility_data = $uid != $item['uid'] ? null : new FriendicaVisibility($this->aclFormatter->expand($item['allow_cid']), $this->aclFormatter->expand($item['deny_cid']), $this->aclFormatter->expand($item['allow_gid']), $this->aclFormatter->expand($item['deny_gid']));
$friendica = new FriendicaExtension($item['title'] ?? '', $item['changed'], $item['commented'], $item['received'], $counts->dislikes, $origin_dislike, $delivery_data, $visibility_data); $friendica = new FriendicaExtension($item['title'] ?? '', $item['changed'], $item['commented'], $item['received'], $counts->dislikes, $origin_dislike, $network, $platform, $version, $sitename, $delivery_data, $visibility_data);
return new \Friendica\Object\Api\Mastodon\Status($item, $account, $counts, $userAttributes, $sensitive, $application, $mentions, $tags, $card, $attachments, $in_reply, $reshare, $friendica, $quote, $poll, $emojis); return new \Friendica\Object\Api\Mastodon\Status($item, $account, $counts, $userAttributes, $sensitive, $application, $mentions, $tags, $card, $attachments, $in_reply, $reshare, $friendica, $quote, $poll, $emojis);
} }
@ -393,7 +413,7 @@ class Status extends BaseFactory
$attachments = []; $attachments = [];
$in_reply = []; $in_reply = [];
$reshare = []; $reshare = [];
$friendica = new FriendicaExtension('', null, null, null, 0, false, null, null); $friendica = new FriendicaExtension('', null, null, null, 0, false, null, null, null, null, null, null);
return new \Friendica\Object\Api\Mastodon\Status($item, $account, $counts, $userAttributes, $sensitive, $application, $mentions, $tags, $card, $attachments, $in_reply, $reshare, $friendica); return new \Friendica\Object\Api\Mastodon\Status($item, $account, $counts, $userAttributes, $sensitive, $application, $mentions, $tags, $card, $attachments, $in_reply, $reshare, $friendica);
} }

View file

@ -84,13 +84,9 @@ class APContact
if (!empty($link['template']) && ($link['rel'] == ActivityNamespace::OSTATUSSUB)) { if (!empty($link['template']) && ($link['rel'] == ActivityNamespace::OSTATUSSUB)) {
$data['subscribe'] = $link['template']; $data['subscribe'] = $link['template'];
} } elseif (!empty($link['href']) && !empty($link['type']) && ($link['rel'] == 'self') && ($link['type'] == 'application/activity+json')) {
if (!empty($link['href']) && !empty($link['type']) && ($link['rel'] == 'self') && ($link['type'] == 'application/activity+json')) {
$data['url'] = $link['href']; $data['url'] = $link['href'];
} } elseif (!empty($link['href']) && !empty($link['type']) && ($link['rel'] == ActivityNamespace::WEBFINGERPROFILE) && ($link['type'] == 'text/html')) {
if (!empty($link['href']) && !empty($link['type']) && ($link['rel'] == 'http://webfinger.net/rel/profile-page') && ($link['type'] == 'text/html')) {
$data['alias'] = $link['href']; $data['alias'] = $link['href'];
} }
} }
@ -106,13 +102,11 @@ class APContact
* Fetches a profile from a given url * Fetches a profile from a given url
* *
* @param string $url profile url * @param string $url profile url
* @param boolean $update true = always update, false = never update, null = update when not found or outdated * @param ?boolean $update true = always update, false = never update, null = update when not found or outdated
* @return array profile array * @return array profile array
* @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
* @todo Rewrite parameter $update to avoid true|false|null (boolean is binary, null adds a third case)
*/ */
public static function getByURL(string $url, $update = null): array public static function getByURL(string $url, bool $update = null): array
{ {
if (empty($url) || Network::isUrlBlocked($url)) { if (empty($url) || Network::isUrlBlocked($url)) {
Logger::info('Domain is blocked', ['url' => $url]); Logger::info('Domain is blocked', ['url' => $url]);
@ -184,7 +178,7 @@ class APContact
DI::cache()->set($cachekey, System::callstack(20), Duration::FIVE_MINUTES); DI::cache()->set($cachekey, System::callstack(20), Duration::FIVE_MINUTES);
} }
if (Network::isLocalLink($url) && ($local_uid = User::getIdForURL($url))) { if (DI::baseUrl()->isLocalUrl($url) && ($local_uid = User::getIdForURL($url))) {
try { try {
$data = Transmitter::getProfile($local_uid); $data = Transmitter::getProfile($local_uid);
$local_owner = User::getOwnerDataById($local_uid); $local_owner = User::getOwnerDataById($local_uid);
@ -293,6 +287,7 @@ class APContact
} elseif ($apcontact['type'] == 'Tombstone') { } elseif ($apcontact['type'] == 'Tombstone') {
// The "inbox" field must have a content // The "inbox" field must have a content
$apcontact['inbox'] = ''; $apcontact['inbox'] = '';
$apcontact['addr'] = '';
} }
// Quit if this doesn't seem to be an account at all // Quit if this doesn't seem to be an account at all
@ -300,7 +295,7 @@ class APContact
return $fetched_contact; return $fetched_contact;
} }
if (empty($apcontact['addr'])) { if (empty($apcontact['addr']) && ($apcontact['type'] != 'Tombstone')) {
try { try {
$apcontact['addr'] = $apcontact['nick'] . '@' . (new Uri($apcontact['url']))->getAuthority(); $apcontact['addr'] = $apcontact['nick'] . '@' . (new Uri($apcontact['url']))->getAuthority();
} catch (\Throwable $e) { } catch (\Throwable $e) {

View file

@ -30,6 +30,7 @@ use Friendica\Object\Image;
use Friendica\Util\DateTimeFormat; use Friendica\Util\DateTimeFormat;
use Friendica\Util\Mimetype; use Friendica\Util\Mimetype;
use Friendica\Security\Security; use Friendica\Security\Security;
use Friendica\Util\Network;
/** /**
* Class to handle attach database table * Class to handle attach database table
@ -110,15 +111,16 @@ class Attach
* Retrieve a single record given the ID * Retrieve a single record given the ID
* *
* @param int $id Row id of the record * @param int $id Row id of the record
* @param int $uid User-Id
* *
* @return bool|array * @return bool|array
* *
* @throws \Exception * @throws \Exception
* @see \Friendica\Database\DBA::select * @see \Friendica\Database\DBA::select
*/ */
public static function getById(int $id) public static function getById(int $id, int $uid)
{ {
return self::selectFirst([], ['id' => $id]); return self::selectFirst([], ['id' => $id, 'uid' => $uid]);
} }
/** /**
@ -243,6 +245,7 @@ class Attach
* @param string $src Source file name * @param string $src Source file name
* @param int $uid User id * @param int $uid User id
* @param string $filename Optional file name * @param string $filename Optional file name
* @param string $filetype Optional file type
* @param string $allow_cid * @param string $allow_cid
* @param string $allow_gid * @param string $allow_gid
* @param string $deny_cid * @param string $deny_cid
@ -250,7 +253,7 @@ class Attach
* @return boolean|int Insert id or false on failure * @return boolean|int Insert id or false on failure
* @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/ */
public static function storeFile(string $src, int $uid, string $filename = '', string $allow_cid = '', string $allow_gid = '', string $deny_cid = '', string $deny_gid = '') public static function storeFile(string $src, int $uid, string $filename = '', string $filetype = '', string $allow_cid = '', string $allow_gid = '', string $deny_cid = '', string $deny_gid = '')
{ {
if ($filename === '') { if ($filename === '') {
$filename = basename($src); $filename = basename($src);
@ -258,7 +261,7 @@ class Attach
$data = @file_get_contents($src); $data = @file_get_contents($src);
return self::store($data, $uid, $filename, '', null, $allow_cid, $allow_gid, $deny_cid, $deny_gid); return self::store($data, $uid, $filename, $filetype, null, $allow_cid, $allow_gid, $deny_cid, $deny_gid);
} }
@ -328,4 +331,51 @@ class Attach
return DBA::delete('attach', $conditions, $options); return DBA::delete('attach', $conditions, $options);
} }
public static function setPermissionFromBody(array $post)
{
preg_match_all("/\[attachment\](.*?)\[\/attachment\]/ism", $post['body'], $matches, PREG_SET_ORDER);
foreach ($matches as $attachment) {
if (DI::baseUrl()->isLocalUrl($attachment[1]) && preg_match('|.*?/attach/(\d+)|', $attachment[1], $match)) {
$fields = [
'allow_cid' => $post['allow_cid'], 'allow_gid' => $post['allow_gid'],
'deny_cid' => $post['deny_cid'], 'deny_gid' => $post['deny_gid']
];
self::update($fields, ['id' => $match[1], 'uid' => $post['uid']]);
}
}
}
public static function setPermissionForId(int $id, int $uid, string $str_contact_allow, string $str_circle_allow, string $str_contact_deny, string $str_circle_deny)
{
$fields = [
'allow_cid' => $str_contact_allow, 'allow_gid' => $str_circle_allow,
'deny_cid' => $str_contact_deny, 'deny_gid' => $str_circle_deny,
];
self::update($fields, ['id' => $id, 'uid' => $uid]);
}
public static function addAttachmentToBody(string $body, int $uid): string
{
preg_match_all("/\[attachment\](.*?)\[\/attachment\]/ism", $body, $matches, PREG_SET_ORDER);
foreach ($matches as $attachment) {
if (DI::baseUrl()->isLocalUrl($attachment[1]) && preg_match('|.*?/attach/(\d+)|', $attachment[1], $match)) {
$attach = self::getById($match[1], $uid);
if (empty($attach)) {
return $body;
}
$media = [
'type' => Post\Media::DOCUMENT,
'url' => $attachment[1],
'size' => $attach['filesize'],
'mimetype' => $attach['filetype'],
'description' => $attach['filename']
];
$media = Post\Media::addType($media);
$body = str_replace($attachment[0], Post\Media::addAttachmentToBody($media, ''), $body);
}
}
return $body;
}
} }

View file

@ -290,12 +290,12 @@ class Circle
throw new HTTPException\NotFoundException('Circle not found.'); throw new HTTPException\NotFoundException('Circle not found.');
} }
$cdata = Contact::getPublicAndUserContactID($cid, $circle['uid']); $ucid = Contact::getUserContactId($cid, $circle['uid']);
if (empty($cdata['user'])) { if (!$ucid) {
throw new HTTPException\NotFoundException('Invalid contact.'); throw new HTTPException\NotFoundException('Invalid contact.');
} }
return DBA::insert('group_member', ['gid' => $gid, 'contact-id' => $cdata['user']], Database::INSERT_IGNORE); return DBA::insert('group_member', ['gid' => $gid, 'contact-id' => $ucid], Database::INSERT_IGNORE);
} }
/** /**
@ -318,12 +318,12 @@ class Circle
throw new HTTPException\NotFoundException('Circle not found.'); throw new HTTPException\NotFoundException('Circle not found.');
} }
$cdata = Contact::getPublicAndUserContactID($cid, $circle['uid']); $ucid = Contact::getUserContactId($cid, $circle['uid']);
if (empty($cdata['user'])) { if (!$ucid) {
throw new HTTPException\NotFoundException('Invalid contact.'); throw new HTTPException\NotFoundException('Invalid contact.');
} }
return DBA::delete('group_member', ['gid' => $gid, 'contact-id' => $cid]); return DBA::delete('group_member', ['gid' => $gid, 'contact-id' => $ucid]);
} }
/** /**
@ -347,12 +347,12 @@ class Circle
} }
foreach ($contacts as $cid) { foreach ($contacts as $cid) {
$cdata = Contact::getPublicAndUserContactID($cid, $circle['uid']); $ucid = Contact::getUserContactId($cid, $circle['uid']);
if (empty($cdata['user'])) { if (!$ucid) {
throw new HTTPException\NotFoundException('Invalid contact.'); throw new HTTPException\NotFoundException('Invalid contact.');
} }
DBA::insert('group_member', ['gid' => $gid, 'contact-id' => $cdata['user']], Database::INSERT_IGNORE); DBA::insert('group_member', ['gid' => $gid, 'contact-id' => $ucid], Database::INSERT_IGNORE);
} }
} }
@ -379,12 +379,12 @@ class Circle
$contactIds = []; $contactIds = [];
foreach ($contacts as $cid) { foreach ($contacts as $cid) {
$cdata = Contact::getPublicAndUserContactID($cid, $circle['uid']); $ucid = Contact::getUserContactId($cid, $circle['uid']);
if (empty($cdata['user'])) { if (!$ucid) {
throw new HTTPException\NotFoundException('Invalid contact.'); throw new HTTPException\NotFoundException('Invalid contact.');
} }
$contactIds[] = $cdata['user']; $contactIds[] = $ucid;
} }
// Return status of deletion // Return status of deletion

View file

@ -113,15 +113,6 @@ class Contact
* @} * @}
*/ */
/** @deprecated Use Entity\LocalRelationship::MIRROR_DEACTIVATED instead */
const MIRROR_DEACTIVATED = LocalRelationship::MIRROR_DEACTIVATED;
/** @deprecated Now does the same as MIRROR_OWN_POST */
const MIRROR_FORWARDED = 1;
/** @deprecated Use Entity\LocalRelationship::MIRROR_OWN_POST instead */
const MIRROR_OWN_POST = LocalRelationship::MIRROR_OWN_POST;
/** @deprecated Use Entity\LocalRelationship::MIRROR_NATIVE_RESHARE instead */
const MIRROR_NATIVE_RESHARE = LocalRelationship::MIRROR_NATIVE_RESHARE;
/** /**
* @param array $fields Array of selected fields, empty for all * @param array $fields Array of selected fields, empty for all
* @param array $condition Array of fields for condition * @param array $condition Array of fields for condition
@ -453,12 +444,12 @@ class Contact
return false; return false;
} }
$cdata = self::getPublicAndUserContactID($cid, $uid); $ucid = self::getUserContactId($cid, $uid);
if (empty($cdata['user'])) { if (!$ucid) {
return false; return false;
} }
$condition = ['id' => $cdata['user'], 'rel' => [self::FOLLOWER, self::FRIEND]]; $condition = ['id' => $ucid, 'rel' => [self::FOLLOWER, self::FRIEND]];
if ($strict) { if ($strict) {
$condition = array_merge($condition, ['pending' => false, 'readonly' => false, 'blocked' => false]); $condition = array_merge($condition, ['pending' => false, 'readonly' => false, 'blocked' => false]);
} }
@ -504,12 +495,12 @@ class Contact
return false; return false;
} }
$cdata = self::getPublicAndUserContactID($cid, $uid); $ucid = self::getUserContactId($cid, $uid);
if (empty($cdata['user'])) { if (!$ucid) {
return false; return false;
} }
$condition = ['id' => $cdata['user'], 'rel' => [self::SHARING, self::FRIEND]]; $condition = ['id' => $ucid, 'rel' => [self::SHARING, self::FRIEND]];
if ($strict) { if ($strict) {
$condition = array_merge($condition, ['pending' => false, 'readonly' => false, 'blocked' => false]); $condition = array_merge($condition, ['pending' => false, 'readonly' => false, 'blocked' => false]);
} }
@ -635,11 +626,8 @@ class Contact
*/ */
public static function getPublicIdByUserId(int $uid) public static function getPublicIdByUserId(int $uid)
{ {
$self = DBA::selectFirst('contact', ['url'], ['self' => true, 'uid' => $uid]); $self = self::selectFirstAccountUser(['pid'], ['self' => true, 'uid' => $uid]);
if (!DBA::isResult($self)) { return $self['pid'] ?? false;
return false;
}
return self::getIdForURL($self['url']);
} }
/** /**
@ -683,6 +671,32 @@ class Contact
return ['public' => $pcid, 'user' => $ucid]; return ['public' => $pcid, 'user' => $ucid];
} }
/**
* Returns the public contact id of a provided contact id
*
* @param integer $cid
* @param integer $uid
* @return integer
*/
public static function getPublicContactId(int $cid, int $uid): int
{
$contact = DBA::selectFirst('account-user-view', ['pid'], ['id' => $cid, 'uid' => [0, $uid]]);
return $contact['pid'] ?? 0;
}
/**
* Returns the user contact id of a provided contact id
*
* @param integer $cid
* @param integer $uid
* @return integer
*/
public static function getUserContactId(int $cid, int $uid): int
{
$data = self::getPublicAndUserContactID($cid, $uid);
return $data['user'] ?? 0;
}
/** /**
* Helper function for "getPublicAndUserContactID" * Helper function for "getPublicAndUserContactID"
* *
@ -892,10 +906,10 @@ class Contact
$fields['avatar'] = User::getAvatarUrl($user); $fields['avatar'] = User::getAvatarUrl($user);
$fields['header'] = User::getBannerUrl($user); $fields['header'] = User::getBannerUrl($user);
$fields['forum'] = $user['page-flags'] == User::PAGE_FLAGS_COMMUNITY; $fields['forum'] = in_array($user['page-flags'], [User::PAGE_FLAGS_COMMUNITY, User::PAGE_FLAGS_COMM_MAN]);
$fields['prv'] = $user['page-flags'] == User::PAGE_FLAGS_PRVGROUP; $fields['prv'] = $user['page-flags'] == User::PAGE_FLAGS_PRVGROUP;
$fields['unsearchable'] = !$profile['net-publish']; $fields['unsearchable'] = !$profile['net-publish'];
$fields['manually-approve'] = in_array($user['page-flags'], [User::PAGE_FLAGS_NORMAL, User::PAGE_FLAGS_PRVGROUP]); $fields['manually-approve'] = in_array($user['page-flags'], [User::PAGE_FLAGS_NORMAL, User::PAGE_FLAGS_PRVGROUP, User::PAGE_FLAGS_COMM_MAN]);
$fields['baseurl'] = DI::baseUrl(); $fields['baseurl'] = DI::baseUrl();
$fields['gsid'] = GServer::getID($fields['baseurl'], true); $fields['gsid'] = GServer::getID($fields['baseurl'], true);
@ -980,13 +994,13 @@ class Contact
} }
if (in_array($contact['rel'], [self::SHARING, self::FRIEND])) { if (in_array($contact['rel'], [self::SHARING, self::FRIEND])) {
$cdata = self::getPublicAndUserContactID($contact['id'], $contact['uid']); $pcid = self::getPublicContactId($contact['id'], $contact['uid']);
if (!empty($cdata['public'])) { if ($pcid) {
Worker::add(Worker::PRIORITY_HIGH, 'Contact\Unfollow', $cdata['public'], $contact['uid']); Worker::add(Worker::PRIORITY_HIGH, 'Contact\Unfollow', $pcid, $contact['uid']);
} }
} }
self::removeSharer($contact); self::removeSharer($contact, false);
} }
/** /**
@ -1010,13 +1024,13 @@ class Contact
} }
if (in_array($contact['rel'], [self::FOLLOWER, self::FRIEND])) { if (in_array($contact['rel'], [self::FOLLOWER, self::FRIEND])) {
$cdata = self::getPublicAndUserContactID($contact['id'], $contact['uid']); $pcid = self::getPublicContactId($contact['id'], $contact['uid']);
if (!empty($cdata['public'])) { if ($pcid) {
Worker::add(Worker::PRIORITY_HIGH, 'Contact\RevokeFollow', $cdata['public'], $contact['uid']); Worker::add(Worker::PRIORITY_HIGH, 'Contact\RevokeFollow', $pcid, $contact['uid']);
} }
} }
self::removeFollower($contact); self::removeFollower($contact, false);
} }
/** /**
@ -1037,14 +1051,14 @@ class Contact
throw new \InvalidArgumentException('Unexpected public contact record'); throw new \InvalidArgumentException('Unexpected public contact record');
} }
$cdata = self::getPublicAndUserContactID($contact['id'], $contact['uid']); $pcid = self::getPublicContactId($contact['id'], $contact['uid']);
if (in_array($contact['rel'], [self::SHARING, self::FRIEND]) && !empty($cdata['public'])) { if (in_array($contact['rel'], [self::SHARING, self::FRIEND]) && $pcid) {
Worker::add(Worker::PRIORITY_HIGH, 'Contact\Unfollow', $cdata['public'], $contact['uid']); Worker::add(Worker::PRIORITY_HIGH, 'Contact\Unfollow', $pcid, $contact['uid']);
} }
if (in_array($contact['rel'], [self::FOLLOWER, self::FRIEND]) && !empty($cdata['public'])) { if (in_array($contact['rel'], [self::FOLLOWER, self::FRIEND]) && $pcid) {
Worker::add(Worker::PRIORITY_HIGH, 'Contact\RevokeFollow', $cdata['public'], $contact['uid']); Worker::add(Worker::PRIORITY_HIGH, 'Contact\RevokeFollow', $pcid, $contact['uid']);
} }
self::remove($contact['id']); self::remove($contact['id']);
@ -1455,6 +1469,7 @@ class Contact
} }
} }
GServer::updateFromProbeArray($data);
self::updateFromProbeArray($contact_id, $data); self::updateFromProbeArray($contact_id, $data);
// Don't return a number for a deleted account // Don't return a number for a deleted account
@ -1559,23 +1574,24 @@ class Contact
* Returns posts from a given contact url * Returns posts from a given contact url
* *
* @param string $contact_url Contact URL * @param string $contact_url Contact URL
* @param bool $thread_mode * @param int $uid User ID
* @param int $update Update mode
* @param int $parent Item parent ID for the update mode
* @param bool $only_media Only display media content * @param bool $only_media Only display media content
* @param string $last_created Newest creation date, used for paging
* @return string posts in HTML * @return string posts in HTML
* @throws \Exception * @throws \Exception
*/ */
public static function getPostsFromUrl(string $contact_url, int $uid, bool $only_media = false): string public static function getPostsFromUrl(string $contact_url, int $uid, bool $only_media = false, string $last_created = null): string
{ {
return self::getPostsFromId(self::getIdForURL($contact_url), $uid, $only_media); return self::getPostsFromId(self::getIdForURL($contact_url), $uid, $only_media, $last_created);
} }
/** /**
* Returns posts from a given contact id * Returns posts from a given contact id
* *
* @param int $cid Contact ID * @param int $cid Contact ID
* @param int $uid User ID
* @param bool $only_media Only display media content * @param bool $only_media Only display media content
* @param string $last_created Newest creation date, used for paging
* @return string posts in HTML * @return string posts in HTML
* @throws \Exception * @throws \Exception
*/ */
@ -1793,7 +1809,7 @@ class Contact
return; return;
} }
if (Network::isLocalLink($contact['url'])) { if (DI::baseUrl()->isLocalUrl($contact['url'])) {
return; return;
} }
@ -1910,7 +1926,7 @@ class Contact
return $contact; return $contact;
} }
$local = !empty($contact['url']) && Network::isLocalLink($contact['url']); $local = !empty($contact['url']) && DI::baseUrl()->isLocalUrl($contact['url']);
if (!$local && !empty($contact['id']) && !empty($contact['avatar'])) { if (!$local && !empty($contact['id']) && !empty($contact['avatar'])) {
self::updateAvatar($contact['id'], $contact['avatar'], true); self::updateAvatar($contact['id'], $contact['avatar'], true);
@ -2300,7 +2316,7 @@ class Contact
if (($uid == 0) && !$force && empty($contact['thumb']) && empty($contact['micro']) && !$create_cache) { if (($uid == 0) && !$force && empty($contact['thumb']) && empty($contact['micro']) && !$create_cache) {
if (($contact['avatar'] != $avatar) || empty($contact['blurhash'])) { if (($contact['avatar'] != $avatar) || empty($contact['blurhash'])) {
$update_fields = ['avatar' => $avatar]; $update_fields = ['avatar' => $avatar];
if (!Network::isLocalLink($avatar)) { if (!DI::baseUrl()->isLocalUrl($avatar)) {
try { try {
$fetchResult = HTTPSignature::fetchRaw($avatar, 0, [HttpClientOptions::ACCEPT_CONTENT => [HttpClientAccept::IMAGE]]); $fetchResult = HTTPSignature::fetchRaw($avatar, 0, [HttpClientOptions::ACCEPT_CONTENT => [HttpClientAccept::IMAGE]]);
@ -2348,7 +2364,7 @@ class Contact
$cache_avatar = DI::config()->get('system', 'cache_contact_avatar'); $cache_avatar = DI::config()->get('system', 'cache_contact_avatar');
// Local contact avatars don't need to be cached // Local contact avatars don't need to be cached
if ($cache_avatar && Network::isLocalLink($contact['url'])) { if ($cache_avatar && DI::baseUrl()->isLocalUrl($contact['url'])) {
$cache_avatar = !DBA::exists('contact', ['nurl' => $contact['nurl'], 'self' => true]); $cache_avatar = !DBA::exists('contact', ['nurl' => $contact['nurl'], 'self' => true]);
} }
@ -2664,6 +2680,14 @@ class Contact
$data = Probe::uri($contact['url'], $network, $contact['uid']); $data = Probe::uri($contact['url'], $network, $contact['uid']);
if (in_array($data['network'], Protocol::FEDERATED) && (parse_url($data['url'], PHP_URL_SCHEME) == 'http')) {
$ssl_url = str_replace('http://', 'https://', $contact['url']);
$ssl_data = Probe::uri($ssl_url, $network, $contact['uid']);
if (($ssl_data['network'] == $data['network']) && (parse_url($ssl_data['url'], PHP_URL_SCHEME) != 'http')) {
$data = $ssl_data;
}
}
if ($data['network'] == Protocol::DIASPORA) { if ($data['network'] == Protocol::DIASPORA) {
try { try {
DI::dsprContact()->updateFromProbeArray($data); DI::dsprContact()->updateFromProbeArray($data);
@ -2682,6 +2706,7 @@ class Contact
} }
} }
GServer::updateFromProbeArray($data);
return self::updateFromProbeArray($id, $data); return self::updateFromProbeArray($id, $data);
} }
@ -2821,7 +2846,7 @@ class Contact
// We must not try to update relay contacts via probe. They are no real contacts. // We must not try to update relay contacts via probe. They are no real contacts.
// See Relay::updateContact() for more details. // See Relay::updateContact() for more details.
// We check after the probing to be able to correct falsely detected contact types. // We check after the probing to be able to correct falsely detected contact types.
if (($contact['contact-type'] == self::TYPE_RELAY) && Strings::compareLink($contact['url'], $contact['baseurl']) && if (($contact['contact-type'] == self::TYPE_RELAY) && Strings::compareLink($contact['url'], $contact['baseurl'] ?? '') &&
(!Strings::compareLink($ret['url'], $contact['url']) || in_array($ret['network'], [Protocol::FEED, Protocol::PHANTOM])) (!Strings::compareLink($ret['url'], $contact['url']) || in_array($ret['network'], [Protocol::FEED, Protocol::PHANTOM]))
) { ) {
if (GServer::reachable($contact)) { if (GServer::reachable($contact)) {
@ -3010,6 +3035,10 @@ class Contact
*/ */
public static function getProtocol(string $url, string $network): string public static function getProtocol(string $url, string $network): string
{ {
if (self::isLocal($url)) {
return Protocol::ACTIVITYPUB;
}
if ($network != Protocol::DFRN) { if ($network != Protocol::DFRN) {
return $network; return $network;
} }
@ -3224,6 +3253,7 @@ class Contact
} }
if ($probed) { if ($probed) {
GServer::updateFromProbeArray($ret);
self::updateFromProbeArray($contact_id, $ret); self::updateFromProbeArray($contact_id, $ret);
} else { } else {
try { try {
@ -3400,16 +3430,21 @@ class Contact
* Update the local relationship when a local user loses a follower * Update the local relationship when a local user loses a follower
* *
* @param array $contact User-specific contact (uid != 0) array * @param array $contact User-specific contact (uid != 0) array
* @param bool $delete Delete if set, otherwise set relation to "nothing" when contact had been a follower
* @return void * @return void
* @throws HTTPException\InternalServerErrorException * @throws HTTPException\InternalServerErrorException
* @throws \ImagickException * @throws \ImagickException
*/ */
public static function removeFollower(array $contact) public static function removeFollower(array $contact, bool $delete = true)
{ {
if (in_array($contact['rel'] ?? [], [self::FRIEND, self::SHARING])) { if (in_array($contact['rel'] ?? [], [self::FRIEND, self::SHARING])) {
self::update(['rel' => self::SHARING], ['id' => $contact['id']]); self::update(['rel' => self::SHARING], ['id' => $contact['id']]);
} elseif (!empty($contact['id'])) { } elseif (!empty($contact['id'])) {
if ($delete) {
self::remove($contact['id']); self::remove($contact['id']);
} else {
self::update(['rel' => self::NOTHING, 'pending' => false], ['id' => $contact['id']]);
}
} else { } else {
DI::logger()->info('Couldn\'t remove follower because of invalid contact array', ['contact' => $contact]); DI::logger()->info('Couldn\'t remove follower because of invalid contact array', ['contact' => $contact]);
return; return;
@ -3419,9 +3454,9 @@ class Contact
self::clearFollowerFollowingEndpointCache($contact['uid']); self::clearFollowerFollowingEndpointCache($contact['uid']);
$cdata = self::getPublicAndUserContactID($contact['id'], $contact['uid']); $pcid = self::getPublicContactId($contact['id'], $contact['uid']);
if (!empty($cdata['public'])) { if ($pcid) {
DI::notification()->deleteForUserByVerb($contact['uid'], Activity::FOLLOW, ['actor-id' => $cdata['public']]); DI::notification()->deleteForUserByVerb($contact['uid'], Activity::FOLLOW, ['actor-id' => $pcid]);
} }
} }
@ -3430,14 +3465,19 @@ class Contact
* Removes the contact for sharing-only protocols (feed and mail). * Removes the contact for sharing-only protocols (feed and mail).
* *
* @param array $contact User-specific contact (uid != 0) array * @param array $contact User-specific contact (uid != 0) array
* @param bool $delete Delete if set, otherwise set relation to "nothing" when contact had been a sharer
* @throws HTTPException\InternalServerErrorException * @throws HTTPException\InternalServerErrorException
*/ */
public static function removeSharer(array $contact) public static function removeSharer(array $contact, bool $delete = true)
{ {
self::clearFollowerFollowingEndpointCache($contact['uid']); self::clearFollowerFollowingEndpointCache($contact['uid']);
if ($contact['rel'] == self::SHARING || in_array($contact['network'], [Protocol::FEED, Protocol::MAIL])) { if (in_array($contact['rel'], [self::SHARING, self::NOTHING]) || in_array($contact['network'], [Protocol::FEED, Protocol::MAIL])) {
if ($delete) {
self::remove($contact['id']); self::remove($contact['id']);
} else {
self::update(['rel' => self::NOTHING, 'pending' => false], ['id' => $contact['id']]);
}
} else { } else {
self::update(['rel' => self::FOLLOWER, 'pending' => false], ['id' => $contact['id']]); self::update(['rel' => self::FOLLOWER, 'pending' => false], ['id' => $contact['id']]);
} }
@ -3563,7 +3603,14 @@ class Contact
*/ */
public static function magicLinkById(int $cid, string $url = ''): string public static function magicLinkById(int $cid, string $url = ''): string
{ {
if (($url == '') && DI::userSession()->isAuthenticated() && DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'stay_local')) {
return 'contact/' . $cid . '/conversations';
}
$contact = DBA::selectFirst('contact', ['id', 'network', 'url', 'alias', 'uid'], ['id' => $cid]); $contact = DBA::selectFirst('contact', ['id', 'network', 'url', 'alias', 'uid'], ['id' => $cid]);
if (empty($contact)) {
return $url;
}
return self::magicLinkByContact($contact, $url); return self::magicLinkByContact($contact, $url);
} }
@ -3586,15 +3633,19 @@ class Contact
return $destination; return $destination;
} }
if (DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'stay_local') && ($url == '')) {
return 'contact/' . $contact['id'] . '/conversations';
}
if (Strings::compareLink($contact['url'], $url) || Strings::compareLink($contact['alias'] ?? '', $url)) {
$url = '';
}
// Only redirections to the same host do make sense // Only redirections to the same host do make sense
if (($url != '') && (parse_url($url, PHP_URL_HOST) != parse_url($contact['url'], PHP_URL_HOST))) { if (($url != '') && (parse_url($url, PHP_URL_HOST) != parse_url($contact['url'], PHP_URL_HOST))) {
return $url; return $url;
} }
if (DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'stay_local') && ($url == '')) {
return 'contact/' . $contact['id'] . '/conversations';
}
if (!empty($contact['network']) && $contact['network'] != Protocol::DFRN) { if (!empty($contact['network']) && $contact['network'] != Protocol::DFRN) {
return $destination; return $destination;
} }
@ -3605,7 +3656,7 @@ class Contact
$redirect = 'contact/redir/' . $contact['id']; $redirect = 'contact/redir/' . $contact['id'];
if (($url != '') && !Strings::compareLink($contact['url'], $url)) { if ($url != '') {
$redirect .= '?url=' . $url; $redirect .= '?url=' . $url;
} }

View file

@ -281,12 +281,12 @@ class User
*/ */
public static function setCollapsed(int $cid, int $uid, bool $collapsed) public static function setCollapsed(int $cid, int $uid, bool $collapsed)
{ {
$cdata = Contact::getPublicAndUserContactID($cid, $uid); $pcid = Contact::getPublicContactId($cid, $uid);
if (empty($cdata)) { if (!$pcid) {
return; return;
} }
DBA::update('user-contact', ['collapsed' => $collapsed], ['cid' => $cdata['public'], 'uid' => $uid], true); DBA::update('user-contact', ['collapsed' => $collapsed], ['cid' => $pcid, 'uid' => $uid], true);
} }
/** /**
@ -300,21 +300,13 @@ class User
*/ */
public static function isCollapsed(int $cid, int $uid): bool public static function isCollapsed(int $cid, int $uid): bool
{ {
$cdata = Contact::getPublicAndUserContactID($cid, $uid); $pcid = Contact::getPublicContactId($cid, $uid);
if (empty($cdata)) { if (!$pcid) {
return false; return false;
} }
$collapsed = false; $public_contact = DBA::selectFirst('user-contact', ['collapsed'], ['cid' => $pcid, 'uid' => $uid]);
return $public_contact['collapsed'] ?? false;
if (!empty($cdata['public'])) {
$public_contact = DBA::selectFirst('user-contact', ['collapsed'], ['cid' => $cdata['public'], 'uid' => $uid]);
if (DBA::isResult($public_contact)) {
$collapsed = (bool) $public_contact['collapsed'];
}
}
return $collapsed;
} }
/** /**
@ -328,12 +320,12 @@ class User
*/ */
public static function setChannelFrequency(int $cid, int $uid, int $frequency) public static function setChannelFrequency(int $cid, int $uid, int $frequency)
{ {
$cdata = Contact::getPublicAndUserContactID($cid, $uid); $pcid = Contact::getPublicContactId($cid, $uid);
if (empty($cdata)) { if (!$pcid) {
return; return;
} }
DBA::update('user-contact', ['channel-frequency' => $frequency], ['cid' => $cdata['public'], 'uid' => $uid], true); DBA::update('user-contact', ['channel-frequency' => $frequency], ['cid' => $pcid, 'uid' => $uid], true);
} }
/** /**
@ -347,21 +339,52 @@ class User
*/ */
public static function getChannelFrequency(int $cid, int $uid): int public static function getChannelFrequency(int $cid, int $uid): int
{ {
$cdata = Contact::getPublicAndUserContactID($cid, $uid); $pcid = Contact::getPublicContactId($cid, $uid);
if (empty($cdata)) { if (!$pcid) {
return false; return false;
} }
$frequency = self::FREQUENCY_DEFAULT; $public_contact = DBA::selectFirst('user-contact', ['channel-frequency'], ['cid' => $pcid, 'uid' => $uid]);
return $public_contact['channel-frequency'] ?? self::FREQUENCY_DEFAULT;
if (!empty($cdata['public'])) {
$public_contact = DBA::selectFirst('user-contact', ['channel-frequency'], ['cid' => $cdata['public'], 'uid' => $uid]);
if (DBA::isResult($public_contact)) {
$frequency = $public_contact['channel-frequency'] ?? self::FREQUENCY_DEFAULT;
}
} }
return $frequency; /**
* Set the channel only value for contact id and user id
*
* @param int $cid Either public contact id or user's contact id
* @param int $uid User ID
* @param int $isChannelOnly Is channel only
* @return void
* @throws \Exception
*/
public static function setChannelOnly(int $cid, int $uid, bool $isChannelOnly)
{
$pcid = Contact::getPublicContactId($cid, $uid);
if (!$pcid) {
return;
}
DBA::update('user-contact', ['channel-only' => $isChannelOnly], ['cid' => $pcid, 'uid' => $uid], true);
}
/**
* Returns if the contact is channel only for contact id and user id
*
* @param int $cid Either public contact id or user's contact id
* @param int $uid User ID
* @return bool Contact is channel only
* @throws HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function getChannelOnly(int $cid, int $uid): bool
{
$pcid = Contact::getPublicContactId($cid, $uid);
if (!$pcid) {
return false;
}
$public_contact = DBA::selectFirst('user-contact', ['channel-only'], ['cid' => $pcid, 'uid' => $uid]);
return $public_contact['channel-only'] ?? false;
} }
/** /**
@ -375,12 +398,12 @@ class User
*/ */
public static function setIsBlocked(int $cid, int $uid, bool $blocked) public static function setIsBlocked(int $cid, int $uid, bool $blocked)
{ {
$cdata = Contact::getPublicAndUserContactID($cid, $uid); $pcid = Contact::getPublicContactId($cid, $uid);
if (empty($cdata)) { if (!$pcid) {
return; return;
} }
DBA::update('user-contact', ['is-blocked' => $blocked], ['cid' => $cdata['public'], 'uid' => $uid], true); DBA::update('user-contact', ['is-blocked' => $blocked], ['cid' => $pcid, 'uid' => $uid], true);
} }
/** /**
@ -393,18 +416,12 @@ class User
*/ */
public static function isIsBlocked(int $cid, int $uid): bool public static function isIsBlocked(int $cid, int $uid): bool
{ {
$cdata = Contact::getPublicAndUserContactID($cid, $uid); $pcid = Contact::getPublicContactId($cid, $uid);
if (empty($cdata)) { if (!$pcid) {
return false; return false;
} }
if (!empty($cdata['public'])) { $public_contact = DBA::selectFirst('user-contact', ['is-blocked'], ['cid' => $pcid, 'uid' => $uid]);
$public_contact = DBA::selectFirst('user-contact', ['is-blocked'], ['cid' => $cdata['public'], 'uid' => $uid]); return $public_contact['is-blocked'] ?? false;
if (DBA::isResult($public_contact)) {
return $public_contact['is-blocked'];
}
}
return false;
} }
} }

View file

@ -516,7 +516,7 @@ class Event
throw new HTTPException\UnauthorizedException(DI::l10n()->t('Access to this profile has been restricted.')); throw new HTTPException\UnauthorizedException(DI::l10n()->t('Access to this profile has been restricted.'));
} }
if (!DI::userSession()->isAuthenticated() && !Feature::isEnabled($owner['uid'], 'public_calendar')) { if (!DI::userSession()->isAuthenticated() && !Feature::isEnabled($owner['uid'], Feature::PUBLIC_CALENDAR)) {
throw new HTTPException\UnauthorizedException(DI::l10n()->t('Permission denied.')); throw new HTTPException\UnauthorizedException(DI::l10n()->t('Permission denied.'));
} }

View file

@ -34,6 +34,7 @@ use Friendica\Module\Register;
use Friendica\Network\HTTPClient\Client\HttpClientAccept; use Friendica\Network\HTTPClient\Client\HttpClientAccept;
use Friendica\Network\HTTPClient\Client\HttpClientOptions; use Friendica\Network\HTTPClient\Client\HttpClientOptions;
use Friendica\Network\HTTPClient\Capability\ICanHandleHttpResponses; use Friendica\Network\HTTPClient\Capability\ICanHandleHttpResponses;
use Friendica\Network\HTTPClient\Client\HttpClientRequest;
use Friendica\Network\Probe; use Friendica\Network\Probe;
use Friendica\Protocol\ActivityPub; use Friendica\Protocol\ActivityPub;
use Friendica\Protocol\Relay; use Friendica\Protocol\Relay;
@ -86,9 +87,11 @@ class GServer
// Standardized endpoints // Standardized endpoints
const DETECT_STATISTICS_JSON = 100; const DETECT_STATISTICS_JSON = 100;
const DETECT_NODEINFO_1 = 101; const DETECT_NODEINFO_10 = 101; // Nodeinfo Version 1.0
const DETECT_NODEINFO_2 = 102; const DETECT_NODEINFO_20 = 102; // Nodeinfo Version 2.0
const DETECT_NODEINFO_210 = 103; const DETECT_NODEINFO2_10 = 103; // Nodeinfo2 Version 1.0
const DETECT_NODEINFO_21 = 104; // Nodeinfo Version 2.1
const DETECT_NODEINFO_22 = 105; // Nodeinfo Version 2.2
/** /**
* Check for the existence of a server and adds it in the background if not existant * Check for the existence of a server and adds it in the background if not existant
@ -611,7 +614,7 @@ class GServer
$in_webroot = empty(parse_url($url, PHP_URL_PATH)); $in_webroot = empty(parse_url($url, PHP_URL_PATH));
// When a nodeinfo is present, we don't need to dig further // When a nodeinfo is present, we don't need to dig further
$curlResult = DI::httpClient()->get($url . '/.well-known/x-nodeinfo2', HttpClientAccept::JSON); $curlResult = DI::httpClient()->get($url . '/.well-known/nodeinfo', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
if ($curlResult->isTimeout()) { if ($curlResult->isTimeout()) {
self::setFailureByUrl($url); self::setFailureByUrl($url);
return false; return false;
@ -620,10 +623,11 @@ class GServer
if (!empty($network) && !in_array($network, Protocol::NATIVE_SUPPORT)) { if (!empty($network) && !in_array($network, Protocol::NATIVE_SUPPORT)) {
$serverdata = ['detection-method' => self::DETECT_MANUAL, 'network' => $network, 'platform' => '', 'version' => '', 'site_name' => '', 'info' => '']; $serverdata = ['detection-method' => self::DETECT_MANUAL, 'network' => $network, 'platform' => '', 'version' => '', 'site_name' => '', 'info' => ''];
} else { } else {
$serverdata = self::parseNodeinfo210($curlResult); $serverdata = self::parseNodeinfo($url, $curlResult);
if (empty($serverdata)) {
$curlResult = DI::httpClient()->get($url . '/.well-known/nodeinfo', HttpClientAccept::JSON); if (empty($serverdata) || !in_array($serverdata['detection-method'], [self::DETECT_NODEINFO_20, self::DETECT_NODEINFO_21, self::DETECT_NODEINFO_22])) {
$serverdata = self::fetchNodeinfo($url, $curlResult); $curlResult = DI::httpClient()->get($url . '/.well-known/x-nodeinfo2', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
$serverdata = self::parseNodeinfo2($curlResult) ?: $serverdata;
} }
} }
@ -640,9 +644,9 @@ class GServer
if ($in_webroot) { if ($in_webroot) {
// Fetch the landing page, possibly it reveals some data // Fetch the landing page, possibly it reveals some data
$accept = 'application/activity+json,application/ld+json,application/json,*/*;q=0.9'; $accept = 'application/activity+json,application/ld+json,application/json,*/*;q=0.9';
$curlResult = DI::httpClient()->get($url, $accept); $curlResult = DI::httpClient()->get($url, $accept, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
if (!$curlResult->isSuccess() && $curlResult->getReturnCode() == '406') { if (!$curlResult->isSuccess() && $curlResult->getReturnCode() == '406') {
$curlResult = DI::httpClient()->get($url, HttpClientAccept::HTML); $curlResult = DI::httpClient()->get($url, HttpClientAccept::HTML, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
$html_fetched = true; $html_fetched = true;
} else { } else {
$html_fetched = false; $html_fetched = false;
@ -655,10 +659,10 @@ class GServer
$serverdata = $data['server']; $serverdata = $data['server'];
$systemactor = $data['actor']; $systemactor = $data['actor'];
if (!$html_fetched && !in_array($serverdata['detection-method'], [self::DETECT_SYSTEM_ACTOR, self::DETECT_AP_COLLECTION])) { if (!$html_fetched && !in_array($serverdata['detection-method'], [self::DETECT_SYSTEM_ACTOR, self::DETECT_AP_COLLECTION])) {
$curlResult = DI::httpClient()->get($url, HttpClientAccept::HTML); $curlResult = DI::httpClient()->get($url, HttpClientAccept::HTML, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
} }
} elseif (!$html_fetched && (strlen($curlResult->getBodyString()) < 1000)) { } elseif (!$html_fetched && (strlen($curlResult->getBodyString()) < 1000)) {
$curlResult = DI::httpClient()->get($url, HttpClientAccept::HTML); $curlResult = DI::httpClient()->get($url, HttpClientAccept::HTML, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
} }
if ($serverdata['detection-method'] != self::DETECT_SYSTEM_ACTOR) { if ($serverdata['detection-method'] != self::DETECT_SYSTEM_ACTOR) {
@ -736,6 +740,10 @@ class GServer
} }
} }
if (in_array($serverdata['platform'] ?? '', ['friendica', 'hubzilla', 'streams', 'osada', 'mistpark', 'roadhouse', 'zap'])) {
$serverdata = self::getZotData($url, $serverdata);
}
// When we hadn't been able to detect the network type, we use the hint from the parameter // When we hadn't been able to detect the network type, we use the hint from the parameter
if (($serverdata['network'] == Protocol::PHANTOM) && !empty($network)) { if (($serverdata['network'] == Protocol::PHANTOM) && !empty($network)) {
$serverdata['network'] = $network; $serverdata['network'] = $network;
@ -867,7 +875,7 @@ class GServer
{ {
Logger::info('Discover relay data', ['server' => $server_url]); Logger::info('Discover relay data', ['server' => $server_url]);
$curlResult = DI::httpClient()->get($server_url . '/.well-known/x-social-relay', HttpClientAccept::JSON); $curlResult = DI::httpClient()->get($server_url . '/.well-known/x-social-relay', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
if (!$curlResult->isSuccess()) { if (!$curlResult->isSuccess()) {
return; return;
} }
@ -962,7 +970,7 @@ class GServer
*/ */
private static function fetchStatistics(string $url, array $serverdata): array private static function fetchStatistics(string $url, array $serverdata): array
{ {
$curlResult = DI::httpClient()->get($url . '/statistics.json', HttpClientAccept::JSON); $curlResult = DI::httpClient()->get($url . '/statistics.json', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
if (!$curlResult->isSuccess()) { if (!$curlResult->isSuccess()) {
return $serverdata; return $serverdata;
} }
@ -1044,7 +1052,9 @@ class GServer
} }
/** /**
* Detect server type by using the nodeinfo data * Parses Nodeinfo
*
* @see https://github.com/jhass/nodeinfo
* *
* @param string $url address of the server * @param string $url address of the server
* @param ICanHandleHttpResponses $httpResult * @param ICanHandleHttpResponses $httpResult
@ -1053,7 +1063,7 @@ class GServer
* *
* @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/ */
private static function fetchNodeinfo(string $url, ICanHandleHttpResponses $httpResult): array private static function parseNodeinfo(string $url, ICanHandleHttpResponses $httpResult): array
{ {
if (!$httpResult->isSuccess()) { if (!$httpResult->isSuccess()) {
return []; return [];
@ -1067,6 +1077,7 @@ class GServer
$nodeinfo1_url = ''; $nodeinfo1_url = '';
$nodeinfo2_url = ''; $nodeinfo2_url = '';
$detection_method = self::DETECT_MANUAL;
foreach ($nodeinfo['links'] as $link) { foreach ($nodeinfo['links'] as $link) {
if (!is_array($link) || empty($link['rel']) || empty($link['href'])) { if (!is_array($link) || empty($link['rel']) || empty($link['href'])) {
@ -1076,8 +1087,15 @@ class GServer
if ($link['rel'] == 'http://nodeinfo.diaspora.software/ns/schema/1.0') { if ($link['rel'] == 'http://nodeinfo.diaspora.software/ns/schema/1.0') {
$nodeinfo1_url = Network::addBasePath($link['href'], $httpResult->getUrl()); $nodeinfo1_url = Network::addBasePath($link['href'], $httpResult->getUrl());
} elseif ($link['rel'] == 'http://nodeinfo.diaspora.software/ns/schema/2.0') { } elseif (($detection_method < self::DETECT_NODEINFO_20) && ($link['rel'] == 'http://nodeinfo.diaspora.software/ns/schema/2.0')) {
$nodeinfo2_url = Network::addBasePath($link['href'], $httpResult->getUrl()); $nodeinfo2_url = Network::addBasePath($link['href'], $httpResult->getUrl());
$detection_method = self::DETECT_NODEINFO_20;
} elseif (($detection_method < self::DETECT_NODEINFO_21) && ($link['rel'] == 'http://nodeinfo.diaspora.software/ns/schema/2.1')) {
$nodeinfo2_url = Network::addBasePath($link['href'], $httpResult->getUrl());
$detection_method = self::DETECT_NODEINFO_21;
} elseif (($detection_method < self::DETECT_NODEINFO_22) && ($link['rel'] == 'http://nodeinfo.diaspora.software/ns/schema/2.2')) {
$nodeinfo2_url = Network::addBasePath($link['href'], $httpResult->getUrl());
$detection_method = self::DETECT_NODEINFO_22;
} }
} }
@ -1088,18 +1106,20 @@ class GServer
$server = []; $server = [];
if (!empty($nodeinfo2_url)) { if (!empty($nodeinfo2_url)) {
$server = self::parseNodeinfo2($nodeinfo2_url); $server = self::parseNodeinfo_2($nodeinfo2_url, $detection_method);
} }
if (empty($server) && !empty($nodeinfo1_url)) { if (empty($server) && !empty($nodeinfo1_url)) {
$server = self::parseNodeinfo1($nodeinfo1_url); $server = self::parseNodeinfo_1($nodeinfo1_url);
} }
return $server; return $server;
} }
/** /**
* Parses Nodeinfo 1 * Parses Nodeinfo with the version 1.0
*
* @see https://github.com/jhass/nodeinfo/tree/main/schemas/1.0
* *
* @param string $nodeinfo_url address of the nodeinfo path * @param string $nodeinfo_url address of the nodeinfo path
* *
@ -1107,9 +1127,9 @@ class GServer
* *
* @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/ */
private static function parseNodeinfo1(string $nodeinfo_url): array private static function parseNodeinfo_1(string $nodeinfo_url): array
{ {
$curlResult = DI::httpClient()->get($nodeinfo_url, HttpClientAccept::JSON); $curlResult = DI::httpClient()->get($nodeinfo_url, HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
if (!$curlResult->isSuccess()) { if (!$curlResult->isSuccess()) {
return []; return [];
} }
@ -1120,8 +1140,10 @@ class GServer
return []; return [];
} }
$server = ['detection-method' => self::DETECT_NODEINFO_1, $server = [
'register_policy' => Register::CLOSED]; 'detection-method' => self::DETECT_NODEINFO_10,
'register_policy' => Register::CLOSED
];
if (!empty($nodeinfo['openRegistrations'])) { if (!empty($nodeinfo['openRegistrations'])) {
$server['register_policy'] = Register::OPEN; $server['register_policy'] = Register::OPEN;
@ -1197,19 +1219,22 @@ class GServer
} }
/** /**
* Parses Nodeinfo 2 * Parses Nodeinfo with the versions 2.0, 2.1 and 2.2
* *
* @see https://git.feneas.org/jaywink/nodeinfo2 * @see https://github.com/jhass/nodeinfo/tree/main/schemas/2.0
* @see https://github.com/jhass/nodeinfo/tree/main/schemas/2.1
* @see https://github.com/jhass/nodeinfo/tree/main/schemas/2.2
* *
* @param string $nodeinfo_url address of the nodeinfo path * @param string $nodeinfo_url address of the nodeinfo path
* @param int $detection_method nodeinfo version
* *
* @return array Server data * @return array Server data
* *
* @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/ */
private static function parseNodeinfo2(string $nodeinfo_url): array private static function parseNodeinfo_2(string $nodeinfo_url, int $detection_method): array
{ {
$curlResult = DI::httpClient()->get($nodeinfo_url, HttpClientAccept::JSON); $curlResult = DI::httpClient()->get($nodeinfo_url, HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
if (!$curlResult->isSuccess()) { if (!$curlResult->isSuccess()) {
return []; return [];
} }
@ -1220,7 +1245,7 @@ class GServer
} }
$server = [ $server = [
'detection-method' => self::DETECT_NODEINFO_2, 'detection-method' => $detection_method,
'register_policy' => Register::CLOSED, 'register_policy' => Register::CLOSED,
'platform' => 'unknown', 'platform' => 'unknown',
]; ];
@ -1229,6 +1254,15 @@ class GServer
$server['register_policy'] = Register::OPEN; $server['register_policy'] = Register::OPEN;
} }
if (!empty($nodeinfo['instance'])) {
if (!empty($nodeinfo['instance']['name'])) {
$server['site_name'] = $nodeinfo['instance']['name'];
}
if (!empty($nodeinfo['instance']['description'])) {
$server['info'] = $nodeinfo['instance']['description'];
}
}
if (!empty($nodeinfo['software'])) { if (!empty($nodeinfo['software'])) {
if (isset($nodeinfo['software']['name'])) { if (isset($nodeinfo['software']['name'])) {
$server['platform'] = strtolower($nodeinfo['software']['name']); $server['platform'] = strtolower($nodeinfo['software']['name']);
@ -1244,6 +1278,13 @@ class GServer
if (($server['platform'] == 'mastodon') && substr($nodeinfo['software']['version'], -5) == '-qoto') { if (($server['platform'] == 'mastodon') && substr($nodeinfo['software']['version'], -5) == '-qoto') {
$server['platform'] = 'qoto'; $server['platform'] = 'qoto';
} }
if (isset($nodeinfo['software']['repository'])) {
$server['repository'] = strtolower($nodeinfo['software']['repository']);
}
if (isset($nodeinfo['software']['homepage'])) {
$server['homepage'] = strtolower($nodeinfo['software']['homepage']);
}
} }
} }
@ -1255,6 +1296,9 @@ class GServer
if (!empty($nodeinfo['metadata']['nodeName'])) { if (!empty($nodeinfo['metadata']['nodeName'])) {
$server['site_name'] = $nodeinfo['metadata']['nodeName']; $server['site_name'] = $nodeinfo['metadata']['nodeName'];
} }
if (!empty($nodeinfo['metadata']['nodeDescription'])) {
$server['info'] = $nodeinfo['metadata']['nodeDescription'];
}
if (!empty($nodeinfo['usage']['users']['total'])) { if (!empty($nodeinfo['usage']['users']['total'])) {
$server['registered-users'] = max($nodeinfo['usage']['users']['total'], 1); $server['registered-users'] = max($nodeinfo['usage']['users']['total'], 1);
@ -1315,9 +1359,9 @@ class GServer
} }
/** /**
* Parses NodeInfo2 protocol 1.0 * Parses NodeInfo2
* *
* @see https://github.com/jaywink/nodeinfo2/blob/master/PROTOCOL.md * @see https://github.com/jaywink/nodeinfo2
* *
* @param string $nodeinfo_url address of the nodeinfo path * @param string $nodeinfo_url address of the nodeinfo path
* *
@ -1325,7 +1369,7 @@ class GServer
* *
* @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/ */
private static function parseNodeinfo210(ICanHandleHttpResponses $httpResult): array private static function parseNodeinfo2(ICanHandleHttpResponses $httpResult): array
{ {
if (!$httpResult->isSuccess()) { if (!$httpResult->isSuccess()) {
return []; return [];
@ -1337,8 +1381,10 @@ class GServer
return []; return [];
} }
$server = ['detection-method' => self::DETECT_NODEINFO_210, $server = [
'register_policy' => Register::CLOSED]; 'detection-method' => self::DETECT_NODEINFO2_10,
'register_policy' => Register::CLOSED
];
if (!empty($nodeinfo['openRegistrations'])) { if (!empty($nodeinfo['openRegistrations'])) {
$server['register_policy'] = Register::OPEN; $server['register_policy'] = Register::OPEN;
@ -1429,7 +1475,7 @@ class GServer
*/ */
private static function fetchSiteinfo(string $url, array $serverdata): array private static function fetchSiteinfo(string $url, array $serverdata): array
{ {
$curlResult = DI::httpClient()->get($url . '/siteinfo.json', HttpClientAccept::JSON); $curlResult = DI::httpClient()->get($url . '/siteinfo.json', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
if (!$curlResult->isSuccess()) { if (!$curlResult->isSuccess()) {
return $serverdata; return $serverdata;
} }
@ -1586,8 +1632,11 @@ class GServer
private static function getNomadName(string $url): string private static function getNomadName(string $url): string
{ {
$name = 'nomad'; $name = 'nomad';
$curlResult = DI::httpClient()->get($url . '/manifest', 'application/manifest+json'); $curlResult = DI::httpClient()->get($url . '/manifest', 'application/manifest+json', [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
if (!$curlResult->isSuccess() || ($curlResult->getBodyString() == '')) { if (!$curlResult->isSuccess() || ($curlResult->getBodyString() == '')) {
if ($curlResult->getReturnCode() == 418) {
$name = 'streams';
}
return $name; return $name;
} }
@ -1607,7 +1656,7 @@ class GServer
*/ */
private static function getNomadVersion(string $url): string private static function getNomadVersion(string $url): string
{ {
$curlResult = DI::httpClient()->get($url . '/api/z/1.0/version', HttpClientAccept::JSON); $curlResult = DI::httpClient()->get($url . '/api/z/1.0/version', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
if (!$curlResult->isSuccess() || ($curlResult->getBodyString() == '')) { if (!$curlResult->isSuccess() || ($curlResult->getBodyString() == '')) {
return ''; return '';
} }
@ -1619,6 +1668,77 @@ class GServer
return $data ?? ''; return $data ?? '';
} }
private static function getZotData(string $url, array $serverdata): array
{
$curlResult = DI::httpClient()->get($url, 'application/x-zot+json');
if (!$curlResult->isSuccess()) {
return $serverdata;
}
$json = $curlResult->getBodyString();
$data = json_decode($json, true);
if (empty($data)) {
return $serverdata;
}
if (!empty($data['site'])) {
$serverdata = self::getFromZotData($data['site'], $serverdata);
} else {
$serverdata = self::getFromZotData($data, $serverdata);
}
return $serverdata;
}
private static function getFromZotData(array $data, array $serverdata): array
{
if (!empty($data['version'])) {
$serverdata['version'] = $data['version'];
}
if (!empty($data['openWebAuth'])) {
$serverdata['openwebauth'] = $data['openWebAuth'];
}
if (!empty($data['authRedirect'])) {
$serverdata['authredirect'] = $data['authRedirect'];
}
if (!empty($data['sitename'])) {
$serverdata['site_name'] = $data['sitename'];
}
if (!empty($data['about'])) {
$serverdata['info'] = $data['about'];
}
if (empty($serverdata['info']) && !empty($data['location'])) {
$serverdata['info'] = $data['location'];
}
if (!empty($data['project']) && in_array($data['project'], ['friendica', 'hubzilla', 'streams', 'osada', 'mistpark', 'roadhouse', 'zap'])) {
$serverdata['platform'] = $data['project'];
}
if (!empty($data['accounts'])) {
$serverdata['registered-users'] = $data['accounts'];
}
if (!empty($data['register_policy'])) {
switch ($data['register_policy']) {
case 'open':
$serverdata['register_policy'] = Register::OPEN;
break;
case 'closed':
$serverdata['register_policy'] = Register::CLOSED;
break;
case 'approve':
$serverdata['register_policy'] = Register::APPROVE;
break;
}
}
return $serverdata;
}
/** /**
* Checks if the server contains a valid host meta file * Checks if the server contains a valid host meta file
* *
@ -1629,7 +1749,7 @@ class GServer
private static function validHostMeta(string $url): bool private static function validHostMeta(string $url): bool
{ {
$xrd_timeout = DI::config()->get('system', 'xrd_timeout'); $xrd_timeout = DI::config()->get('system', 'xrd_timeout');
$curlResult = DI::httpClient()->get($url . Probe::HOST_META, HttpClientAccept::XRD_XML, [HttpClientOptions::TIMEOUT => $xrd_timeout]); $curlResult = DI::httpClient()->get($url . Probe::HOST_META, HttpClientAccept::XRD_XML, [HttpClientOptions::TIMEOUT => $xrd_timeout, HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
if (!$curlResult->isSuccess()) { if (!$curlResult->isSuccess()) {
return false; return false;
} }
@ -1728,7 +1848,7 @@ class GServer
{ {
$serverdata['poco'] = ''; $serverdata['poco'] = '';
$curlResult = DI::httpClient()->get($url . '/poco', HttpClientAccept::JSON); $curlResult = DI::httpClient()->get($url . '/poco', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
if (!$curlResult->isSuccess()) { if (!$curlResult->isSuccess()) {
return $serverdata; return $serverdata;
} }
@ -1758,7 +1878,7 @@ class GServer
*/ */
public static function checkMastodonDirectory(string $url, array $serverdata): array public static function checkMastodonDirectory(string $url, array $serverdata): array
{ {
$curlResult = DI::httpClient()->get($url . '/api/v1/directory?limit=1', HttpClientAccept::JSON); $curlResult = DI::httpClient()->get($url . '/api/v1/directory?limit=1', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
if (!$curlResult->isSuccess()) { if (!$curlResult->isSuccess()) {
return $serverdata; return $serverdata;
} }
@ -1785,7 +1905,7 @@ class GServer
*/ */
private static function detectPeertube(string $url, array $serverdata): array private static function detectPeertube(string $url, array $serverdata): array
{ {
$curlResult = DI::httpClient()->get($url . '/api/v1/config', HttpClientAccept::JSON); $curlResult = DI::httpClient()->get($url . '/api/v1/config', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
if (!$curlResult->isSuccess() || ($curlResult->getBodyString() == '')) { if (!$curlResult->isSuccess() || ($curlResult->getBodyString() == '')) {
return $serverdata; return $serverdata;
} }
@ -1833,7 +1953,7 @@ class GServer
*/ */
private static function detectNextcloud(string $url, array $serverdata, bool $validHostMeta): array private static function detectNextcloud(string $url, array $serverdata, bool $validHostMeta): array
{ {
$curlResult = DI::httpClient()->get($url . '/status.php', HttpClientAccept::JSON); $curlResult = DI::httpClient()->get($url . '/status.php', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
if (!$curlResult->isSuccess() || ($curlResult->getBodyString() == '')) { if (!$curlResult->isSuccess() || ($curlResult->getBodyString() == '')) {
return $serverdata; return $serverdata;
} }
@ -1869,7 +1989,7 @@ class GServer
*/ */
private static function fetchWeeklyUsage(string $url, array $serverdata): array private static function fetchWeeklyUsage(string $url, array $serverdata): array
{ {
$curlResult = DI::httpClient()->get($url . '/api/v1/instance/activity', HttpClientAccept::JSON); $curlResult = DI::httpClient()->get($url . '/api/v1/instance/activity', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
if (!$curlResult->isSuccess() || ($curlResult->getBodyString() == '')) { if (!$curlResult->isSuccess() || ($curlResult->getBodyString() == '')) {
return $serverdata; return $serverdata;
} }
@ -1909,7 +2029,7 @@ class GServer
*/ */
private static function detectMastodonAlikes(string $url, array $serverdata): array private static function detectMastodonAlikes(string $url, array $serverdata): array
{ {
$curlResult = DI::httpClient()->get($url . '/api/v1/instance', HttpClientAccept::JSON); $curlResult = DI::httpClient()->get($url . '/api/v1/instance', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
if (!$curlResult->isSuccess() || ($curlResult->getBodyString() == '')) { if (!$curlResult->isSuccess() || ($curlResult->getBodyString() == '')) {
return $serverdata; return $serverdata;
} }
@ -1981,7 +2101,7 @@ class GServer
*/ */
private static function detectHubzilla(string $url, array $serverdata): array private static function detectHubzilla(string $url, array $serverdata): array
{ {
$curlResult = DI::httpClient()->get($url . '/api/statusnet/config.json', HttpClientAccept::JSON); $curlResult = DI::httpClient()->get($url . '/api/statusnet/config.json', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
if (!$curlResult->isSuccess() || ($curlResult->getBodyString() == '')) { if (!$curlResult->isSuccess() || ($curlResult->getBodyString() == '')) {
return $serverdata; return $serverdata;
} }
@ -2078,7 +2198,7 @@ class GServer
private static function detectGNUSocial(string $url, array $serverdata): array private static function detectGNUSocial(string $url, array $serverdata): array
{ {
// Test for GNU Social // Test for GNU Social
$curlResult = DI::httpClient()->get($url . '/api/gnusocial/version.json', HttpClientAccept::JSON); $curlResult = DI::httpClient()->get($url . '/api/gnusocial/version.json', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
if ($curlResult->isSuccess() && ($curlResult->getBodyString() != '{"error":"not implemented"}') && if ($curlResult->isSuccess() && ($curlResult->getBodyString() != '{"error":"not implemented"}') &&
($curlResult->getBodyString() != '') && (strlen($curlResult->getBodyString()) < 30)) { ($curlResult->getBodyString() != '') && (strlen($curlResult->getBodyString()) < 30)) {
$serverdata['platform'] = 'gnusocial'; $serverdata['platform'] = 'gnusocial';
@ -2096,7 +2216,7 @@ class GServer
} }
// Test for Statusnet // Test for Statusnet
$curlResult = DI::httpClient()->get($url . '/api/statusnet/version.json', HttpClientAccept::JSON); $curlResult = DI::httpClient()->get($url . '/api/statusnet/version.json', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
if ($curlResult->isSuccess() && ($curlResult->getBodyString() != '{"error":"not implemented"}') && if ($curlResult->isSuccess() && ($curlResult->getBodyString() != '{"error":"not implemented"}') &&
($curlResult->getBodyString() != '') && (strlen($curlResult->getBodyString()) < 30)) { ($curlResult->getBodyString() != '') && (strlen($curlResult->getBodyString()) < 30)) {
@ -2134,9 +2254,9 @@ class GServer
{ {
// There is a bug in some versions of Friendica that will return an ActivityStream actor when the content type "application/json" is requested. // There is a bug in some versions of Friendica that will return an ActivityStream actor when the content type "application/json" is requested.
// Because of this me must not use ACCEPT_JSON here. // Because of this me must not use ACCEPT_JSON here.
$curlResult = DI::httpClient()->get($url . '/friendica/json'); $curlResult = DI::httpClient()->get($url . '/friendica/json', HttpClientAccept::DEFAULT, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
if (!$curlResult->isSuccess()) { if (!$curlResult->isSuccess()) {
$curlResult = DI::httpClient()->get($url . '/friendika/json'); $curlResult = DI::httpClient()->get($url . '/friendika/json', HttpClientAccept::DEFAULT, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
$friendika = true; $friendika = true;
$platform = 'Friendika'; $platform = 'Friendika';
} else { } else {
@ -2447,7 +2567,7 @@ class GServer
$protocols = ['activitypub', 'diaspora', 'dfrn', 'ostatus']; $protocols = ['activitypub', 'diaspora', 'dfrn', 'ostatus'];
foreach ($protocols as $protocol) { foreach ($protocols as $protocol) {
$query = '{nodes(protocol:"' . $protocol . '"){host}}'; $query = '{nodes(protocol:"' . $protocol . '"){host}}';
$curlResult = DI::httpClient()->fetch('https://the-federation.info/graphql?query=' . urlencode($query), HttpClientAccept::JSON); $curlResult = DI::httpClient()->fetch('https://the-federation.info/graphql?query=' . urlencode($query), HttpClientAccept::JSON, 0, '', HttpClientRequest::SERVERDISCOVER);
if (!empty($curlResult)) { if (!empty($curlResult)) {
$data = json_decode($curlResult, true); $data = json_decode($curlResult, true);
if (!empty($data['data']['nodes'])) { if (!empty($data['data']['nodes'])) {
@ -2464,7 +2584,7 @@ class GServer
if (!empty($accesstoken)) { if (!empty($accesstoken)) {
$api = 'https://instances.social/api/1.0/instances/list?count=0'; $api = 'https://instances.social/api/1.0/instances/list?count=0';
$curlResult = DI::httpClient()->get($api, HttpClientAccept::JSON, [HttpClientOptions::HEADERS => ['Authorization' => ['Bearer ' . $accesstoken]]]); $curlResult = DI::httpClient()->get($api, HttpClientAccept::JSON, [HttpClientOptions::HEADERS => ['Authorization' => ['Bearer ' . $accesstoken], HttpClientOptions::REQUEST => HttpClientRequest::SERVERDISCOVER]]);
if ($curlResult->isSuccess()) { if ($curlResult->isSuccess()) {
$servers = json_decode($curlResult->getBodyString(), true); $servers = json_decode($curlResult->getBodyString(), true);
@ -2480,6 +2600,29 @@ class GServer
DI::keyValue()->set('poco_last_federation_discovery', time()); DI::keyValue()->set('poco_last_federation_discovery', time());
} }
public static function updateFromProbeArray(array $data)
{
if (empty($data['gsid']) || empty($data['openwebauth'])) {
return;
}
$gserver = DBA::selectFirst('gserver', ['url', 'openwebauth'], ['id' => $data['gsid']]);
if (!DBA::isResult($gserver)) {
return;
}
if ($data['openwebauth'] == $gserver['openwebauth']) {
return;
}
$serverdata = self::getZotData($gserver['url'], []);
if (empty($serverdata)) {
$serverdata = ['openwebauth' => $data['openwebauth']];
}
self::update($serverdata, ['id' => $data['gsid']]);
}
/** /**
* Set the protocol for the given server * Set the protocol for the given server
* *

View file

@ -39,6 +39,7 @@ use Friendica\DI;
use Friendica\Model\Post\Category; use Friendica\Model\Post\Category;
use Friendica\Network\HTTPClient\Client\HttpClientAccept; use Friendica\Network\HTTPClient\Client\HttpClientAccept;
use Friendica\Network\HTTPClient\Client\HttpClientOptions; use Friendica\Network\HTTPClient\Client\HttpClientOptions;
use Friendica\Network\HTTPClient\Client\HttpClientRequest;
use Friendica\Network\HTTPException\InternalServerErrorException; use Friendica\Network\HTTPException\InternalServerErrorException;
use Friendica\Network\HTTPException\ServiceUnavailableException; use Friendica\Network\HTTPException\ServiceUnavailableException;
use Friendica\Protocol\Activity; use Friendica\Protocol\Activity;
@ -108,7 +109,7 @@ class Item
'owner-id', 'owner-link', 'owner-alias', 'owner-name', 'owner-avatar', 'owner-network', 'owner-contact-type', 'owner-updated', 'owner-gsid', 'owner-id', 'owner-link', 'owner-alias', 'owner-name', 'owner-avatar', 'owner-network', 'owner-contact-type', 'owner-updated', 'owner-gsid',
'causer-id', 'causer-link', 'causer-alias', 'causer-name', 'causer-avatar', 'causer-contact-type', 'causer-network', 'causer-gsid', 'causer-id', 'causer-link', 'causer-alias', 'causer-name', 'causer-avatar', 'causer-contact-type', 'causer-network', 'causer-gsid',
'contact-id', 'contact-uid', 'contact-link', 'contact-name', 'contact-avatar', 'contact-id', 'contact-uid', 'contact-link', 'contact-name', 'contact-avatar',
'writable', 'self', 'cid', 'alias', 'writable', 'restrictions', 'self', 'cid', 'alias', 'post-reason',
'event-created', 'event-edited', 'event-start', 'event-finish', 'event-created', 'event-edited', 'event-start', 'event-finish',
'event-summary', 'event-desc', 'event-location', 'event-type', 'event-summary', 'event-desc', 'event-location', 'event-type',
'event-nofinish', 'event-ignore', 'event-id', 'event-nofinish', 'event-ignore', 'event-id',
@ -121,7 +122,7 @@ class Item
const DELIVER_FIELDLIST = [ const DELIVER_FIELDLIST = [
'uid', 'id', 'parent', 'uri-id', 'uri', 'thr-parent', 'parent-uri', 'guid', 'uid', 'id', 'parent', 'uri-id', 'uri', 'thr-parent', 'parent-uri', 'guid',
'parent-guid', 'conversation', 'received', 'created', 'edited', 'verb', 'object-type', 'object', 'target', 'parent-guid', 'conversation', 'received', 'created', 'edited', 'verb', 'object-type', 'object', 'target',
'private', 'title', 'body', 'raw-body', 'language', 'location', 'coord', 'app', 'sensitive', 'private', 'title', 'content-warning', 'body', 'raw-body', 'language', 'location', 'coord', 'app', 'sensitive',
'inform', 'deleted', 'extid', 'post-type', 'post-reason', 'gravity', 'inform', 'deleted', 'extid', 'post-type', 'post-reason', 'gravity',
'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid', 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid',
'author-id', 'author-addr', 'author-link', 'author-name', 'author-avatar', 'owner-id', 'owner-link', 'contact-uid', 'author-id', 'author-addr', 'author-link', 'author-name', 'author-avatar', 'owner-id', 'owner-link', 'contact-uid',
@ -168,6 +169,11 @@ class Item
const GRAVITY_COMMENT = 6; const GRAVITY_COMMENT = 6;
const GRAVITY_UNKNOWN = 9; const GRAVITY_UNKNOWN = 9;
// Restrictions
const CANT_REPLY = 1;
const CANT_LIKE = 2;
const CANT_ANNOUNCE = 4;
/** /**
* Update existing item entries * Update existing item entries
* *
@ -192,6 +198,10 @@ class Item
$fields['external-id'] = ItemURI::getIdByURI($fields['extid']); $fields['external-id'] = ItemURI::getIdByURI($fields['extid']);
} }
if (!empty($fields['replies'])) {
$fields['replies-id'] = ItemURI::getIdByURI($fields['replies']);
}
if (!empty($fields['verb'])) { if (!empty($fields['verb'])) {
$fields['vid'] = Verb::getID($fields['verb']); $fields['vid'] = Verb::getID($fields['verb']);
} }
@ -409,8 +419,24 @@ class Item
self::markForDeletion(['parent' => $item['parent'], 'deleted' => false], $priority); self::markForDeletion(['parent' => $item['parent'], 'deleted' => false], $priority);
} }
if ($item['uid'] == 0 && $item['gravity'] == self::GRAVITY_PARENT) {
$posts = DI::keyValue()->get('nodeinfo_total_posts') ?? 0;
DI::keyValue()->set('nodeinfo_total_posts', $posts - 1);
} elseif ($item['uid'] == 0 && $item['gravity'] == self::GRAVITY_COMMENT) {
$comments = DI::keyValue()->get('nodeinfo_total_comments') ?? 0;
DI::keyValue()->set('nodeinfo_total_comments', $comments - 1);
}
// Is it our comment and/or our thread? // Is it our comment and/or our thread?
if (($item['origin'] || $parent['origin']) && ($item['uid'] != 0)) { if (($item['origin'] || $parent['origin']) && ($item['uid'] != 0)) {
if ($item['origin'] && $item['gravity'] == self::GRAVITY_PARENT) {
$posts = DI::keyValue()->get('nodeinfo_local_posts') ?? 0;
DI::keyValue()->set('nodeinfo_local_posts', $posts - 1);
} elseif ($item['origin'] && $item['gravity'] == self::GRAVITY_COMMENT) {
$comments = DI::keyValue()->get('nodeinfo_local_comments') ?? 0;
DI::keyValue()->set('nodeinfo_local_comments', $comments - 1);
}
// When we delete the original post we will delete all existing copies on the server as well // When we delete the original post we will delete all existing copies on the server as well
self::markForDeletion(['uri-id' => $item['uri-id'], 'deleted' => false], $priority); self::markForDeletion(['uri-id' => $item['uri-id'], 'deleted' => false], $priority);
@ -530,9 +556,9 @@ class Item
} }
if (!empty($item['causer-id']) && Contact::isSharing($item['causer-id'], $item['uid'], true)) { if (!empty($item['causer-id']) && Contact::isSharing($item['causer-id'], $item['uid'], true)) {
$cdata = Contact::getPublicAndUserContactID($item['causer-id'], $item['uid']); $ucid = Contact::getUserContactId($item['causer-id'], $item['uid']);
if (!empty($cdata['user'])) { if ($ucid) {
return $cdata['user']; return $ucid;
} }
} }
@ -759,7 +785,7 @@ class Item
{ {
$fields = [ $fields = [
'uid', 'uri', 'parent-uri', 'id', 'deleted', 'uid', 'uri', 'parent-uri', 'id', 'deleted',
'uri-id', 'parent-uri-id', 'uri-id', 'parent-uri-id', 'restrictions', 'verb',
'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid', 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid',
'wall', 'private', 'origin', 'author-id' 'wall', 'private', 'origin', 'author-id'
]; ];
@ -783,6 +809,11 @@ class Item
return []; return [];
} }
if (self::hasRestrictions($item, $parent['author-id'], $parent['restrictions'])) {
Logger::notice('Restrictions apply - ignoring item', ['restrictions' => $parent['restrictions'], 'verb' => $parent['verb'], 'uri-id' => $item['uri-id'], 'thr-parent-id' => $item['thr-parent-id'], 'uid' => $item['uid']]);
return [];
}
if ($parent['uri-id'] == $parent['parent-uri-id']) { if ($parent['uri-id'] == $parent['parent-uri-id']) {
return $parent; return $parent;
} }
@ -838,7 +869,7 @@ class Item
private static function prepareOriginPost(array $item): array private static function prepareOriginPost(array $item): array
{ {
$item = DI::contentItem()->initializePost($item); $item = DI::contentItem()->initializePost($item);
$item = DI::contentItem()->finalizePost($item); $item = DI::contentItem()->finalizePost($item, false);
return $item; return $item;
} }
@ -1066,6 +1097,10 @@ class Item
$parent_id = 0; $parent_id = 0;
$parent_origin = $item['origin']; $parent_origin = $item['origin'];
if ($item['wall'] && empty($item['context'])) {
$item['context'] = $item['parent-uri'] . '#context';
}
if ($item['wall'] && empty($item['conversation'])) { if ($item['wall'] && empty($item['conversation'])) {
$item['conversation'] = $item['parent-uri'] . '#context'; $item['conversation'] = $item['parent-uri'] . '#context';
} }
@ -1078,8 +1113,6 @@ class Item
) { ) {
$item['object-type'] = Activity\ObjectType::IMAGE; $item['object-type'] = Activity\ObjectType::IMAGE;
} }
$item = DI::contentItem()->moveAttachmentsFromBodyToAttach($item);
} }
$item['parent-uri-id'] = ItemURI::getIdByURI($item['parent-uri']); $item['parent-uri-id'] = ItemURI::getIdByURI($item['parent-uri']);
@ -1089,6 +1122,10 @@ class Item
$item['conversation-id'] = ItemURI::getIdByURI($item['conversation']); $item['conversation-id'] = ItemURI::getIdByURI($item['conversation']);
} }
if (!empty($item['context']) && empty($item['context-id'])) {
$item['context-id'] = ItemURI::getIdByURI($item['context']);
}
// Is this item available in the global items (with uid=0)? // Is this item available in the global items (with uid=0)?
if ($item['uid'] == 0) { if ($item['uid'] == 0) {
$item['global'] = true; $item['global'] = true;
@ -1156,6 +1193,10 @@ class Item
$item['external-id'] = ItemURI::getIdByURI($item['extid']); $item['external-id'] = ItemURI::getIdByURI($item['extid']);
} }
if (!empty($item['replies'])) {
$item['replies-id'] = ItemURI::getIdByURI($item['replies']);
}
if ($item['verb'] == Activity::ANNOUNCE) { if ($item['verb'] == Activity::ANNOUNCE) {
self::setOwnerforResharedItem($item); self::setOwnerforResharedItem($item);
} }
@ -1325,6 +1366,16 @@ class Item
return 0; return 0;
} }
if ($posted_item['origin'] && $posted_item['gravity'] == self::GRAVITY_PARENT) {
$posts = (int)(DI::keyValue()->get('nodeinfo_local_posts') ?? 0);
DI::keyValue()->set('nodeinfo_local_posts', $posts + 1);
} elseif ($posted_item['origin'] && $posted_item['gravity'] == self::GRAVITY_COMMENT) {
$comments = (int)(DI::keyValue()->get('nodeinfo_local_comments') ?? 0);
DI::keyValue()->set('nodeinfo_local_comments', $comments + 1);
}
Post\Origin::insert($posted_item);
// update the commented timestamp on the parent // update the commented timestamp on the parent
if (DI::config()->get('system', 'like_no_comment')) { if (DI::config()->get('system', 'like_no_comment')) {
// Update when it is a comment // Update when it is a comment
@ -1412,6 +1463,14 @@ class Item
} }
if ($inserted) { if ($inserted) {
if ($posted_item['gravity'] == self::GRAVITY_PARENT) {
$posts = (int)(DI::keyValue()->get('nodeinfo_total_posts') ?? 0);
DI::keyValue()->set('nodeinfo_total_posts', $posts + 1);
} elseif ($posted_item['gravity'] == self::GRAVITY_COMMENT) {
$comments = (int)(DI::keyValue()->get('nodeinfo_total_comments') ?? 0);
DI::keyValue()->set('nodeinfo_total_comments', $comments + 1);
}
// Fill the cache with the rendered content. // Fill the cache with the rendered content.
if (in_array($posted_item['gravity'], [self::GRAVITY_PARENT, self::GRAVITY_COMMENT])) { if (in_array($posted_item['gravity'], [self::GRAVITY_PARENT, self::GRAVITY_COMMENT])) {
self::updateDisplayCache($posted_item['uri-id']); self::updateDisplayCache($posted_item['uri-id']);
@ -1439,6 +1498,27 @@ class Item
return $post_user_id; return $post_user_id;
} }
private static function hasRestrictions(array $item, int $author_id, int $restrictions = null): bool
{
if (empty($restrictions) || ($author_id == $item['author-id'])) {
return false;
}
if (($restrictions & self::CANT_REPLY) && ($item['verb'] == Activity::POST)) {
return true;
}
if (($restrictions & self::CANT_ANNOUNCE) && ($item['verb'] == Activity::ANNOUNCE)) {
return true;
}
if (($restrictions & self::CANT_LIKE) && in_array($item['verb'], [Activity::LIKE, Activity::DISLIKE, Activity::ATTEND, Activity::ATTENDMAYBE, Activity::ATTENDNO])) {
return true;
}
return false;
}
private static function reshareChannelPost(int $uri_id, int $reshare_id = 0) private static function reshareChannelPost(int $uri_id, int $reshare_id = 0)
{ {
if (!DI::config()->get('system', 'allow_relay_channels')) { if (!DI::config()->get('system', 'allow_relay_channels')) {
@ -2552,12 +2632,12 @@ class Item
return; return;
} }
$cdata = Contact::getPublicAndUserContactID($item['author-id'], $item['uid']); $ucid = Contact::getUserContactId($item['author-id'], $item['uid']);
if (empty($cdata['user']) || ($cdata['user'] != $item['contact-id'])) { if (!$ucid || ($ucid != $item['contact-id'])) {
return; return;
} }
if (!DBA::exists('contact', ['id' => $cdata['user'], 'remote_self' => LocalRelationship::MIRROR_NATIVE_RESHARE])) { if (!DBA::exists('contact', ['id' => $ucid, 'remote_self' => LocalRelationship::MIRROR_NATIVE_RESHARE])) {
return; return;
} }
@ -3315,7 +3395,6 @@ class Item
$item['tags'] = $tags['tags']; $item['tags'] = $tags['tags'];
$item['hashtags'] = $tags['hashtags']; $item['hashtags'] = $tags['hashtags'];
$item['mentions'] = $tags['mentions']; $item['mentions'] = $tags['mentions'];
$sensitive = $item['sensitive'] && !DI::pConfig()->get($uid, 'system', 'display_sensitive', false);
if (!$is_preview) { if (!$is_preview) {
$item['body'] = preg_replace("#\s*\[attachment .*?].*?\[/attachment]\s*#ism", "\n", $item['body']); $item['body'] = preg_replace("#\s*\[attachment .*?].*?\[/attachment]\s*#ism", "\n", $item['body']);
@ -3383,11 +3462,11 @@ class Item
$shared_links = array_merge($shared_links, $sharedSplitAttachments['visual']->column('url')); $shared_links = array_merge($shared_links, $sharedSplitAttachments['visual']->column('url'));
$shared_links = array_merge($shared_links, $sharedSplitAttachments['link']->column('url')); $shared_links = array_merge($shared_links, $sharedSplitAttachments['link']->column('url'));
$shared_links = array_merge($shared_links, $sharedSplitAttachments['additional']->column('url')); $shared_links = array_merge($shared_links, $sharedSplitAttachments['additional']->column('url'));
$item['body'] = self::replaceVisualAttachments($sharedSplitAttachments['visual'], $item['body'], $sensitive); $item['body'] = self::replaceVisualAttachments($sharedSplitAttachments['visual'], $item['body']);
} }
$itemSplitAttachments = DI::postMediaRepository()->splitAttachments($item['uri-id'], $shared_links, $item['has-media'] ?? false); $itemSplitAttachments = DI::postMediaRepository()->splitAttachments($item['uri-id'], $shared_links, $item['has-media'] ?? false);
$item['body'] = self::replaceVisualAttachments($itemSplitAttachments['visual'], $item['body'] ?? '', $sensitive); $item['body'] = self::replaceVisualAttachments($itemSplitAttachments['visual'], $item['body'] ?? '');
self::putInCache($item); self::putInCache($item);
$item['body'] = $body; $item['body'] = $body;
@ -3408,8 +3487,8 @@ class Item
$filter_reasons[] = DI::l10n()->t('Content from %s is collapsed', $item['author-name']); $filter_reasons[] = DI::l10n()->t('Content from %s is collapsed', $item['author-name']);
} }
if (!empty($item['content-warning']) && (!$uid || !DI::pConfig()->get($uid, 'system', 'disable_cw', false))) { if ($item['sensitive'] && (!$uid || !DI::pConfig()->get($uid, 'system', 'disable_cw', false))) {
$filter_reasons[] = DI::l10n()->t('Content warning: %s', $item['content-warning']); $filter_reasons[] = DI::l10n()->t('Sensitive content');
} }
$item['attachments'] = $itemSplitAttachments; $item['attachments'] = $itemSplitAttachments;
@ -3442,9 +3521,9 @@ class Item
} }
if (!empty($sharedSplitAttachments)) { if (!empty($sharedSplitAttachments)) {
$s = self::addGallery($s, $sharedSplitAttachments['visual'], $sensitive); $s = self::addGallery($s, $sharedSplitAttachments['visual']);
$s = self::addVisualAttachments($sharedSplitAttachments['visual'], $shared_item, $s, true, $sensitive); $s = self::addVisualAttachments($sharedSplitAttachments['visual'], $shared_item, $s, true);
$s = self::addLinkAttachment($shared_uri_id ?: $item['uri-id'], $sharedSplitAttachments, $body, $s, true, $quote_shared_links, $sensitive); $s = self::addLinkAttachment($shared_uri_id ?: $item['uri-id'], $sharedSplitAttachments, $body, $s, true, $quote_shared_links);
$s = self::addNonVisualAttachments($sharedSplitAttachments['additional'], $item, $s, true); $s = self::addNonVisualAttachments($sharedSplitAttachments['additional'], $item, $s, true);
$body = BBCode::removeSharedData($body); $body = BBCode::removeSharedData($body);
} }
@ -3455,9 +3534,9 @@ class Item
$s = substr($s, 0, $pos); $s = substr($s, 0, $pos);
} }
$s = self::addGallery($s, $itemSplitAttachments['visual'], $sensitive); $s = self::addGallery($s, $itemSplitAttachments['visual']);
$s = self::addVisualAttachments($itemSplitAttachments['visual'], $item, $s, false, $sensitive); $s = self::addVisualAttachments($itemSplitAttachments['visual'], $item, $s, false);
$s = self::addLinkAttachment($item['uri-id'], $itemSplitAttachments, $body, $s, false, $shared_links, $sensitive); $s = self::addLinkAttachment($item['uri-id'], $itemSplitAttachments, $body, $s, false, $shared_links);
$s = self::addNonVisualAttachments($itemSplitAttachments['additional'], $item, $s, false); $s = self::addNonVisualAttachments($itemSplitAttachments['additional'], $item, $s, false);
$s = self::addQuestions($item, $s); $s = self::addQuestions($item, $s);
@ -3491,10 +3570,9 @@ class Item
* *
* @param string $s * @param string $s
* @param PostMedias $PostMedias * @param PostMedias $PostMedias
* @param bool $sensitive
* @return string * @return string
*/ */
private static function addGallery(string $s, PostMedias $PostMedias, bool $sensitive): string private static function addGallery(string $s, PostMedias $PostMedias): string
{ {
foreach ($PostMedias as $PostMedia) { foreach ($PostMedias as $PostMedia) {
if (!$PostMedia->preview || ($PostMedia->type !== Post\Media::IMAGE)) { if (!$PostMedia->preview || ($PostMedia->type !== Post\Media::IMAGE)) {
@ -3504,10 +3582,9 @@ class Item
if ($PostMedia->hasDimensions()) { if ($PostMedia->hasDimensions()) {
$pattern = '#<a href="' . preg_quote($PostMedia->url) . '">(.*?)"></a>#'; $pattern = '#<a href="' . preg_quote($PostMedia->url) . '">(.*?)"></a>#';
$s = preg_replace_callback($pattern, function () use ($PostMedia, $sensitive) { $s = preg_replace_callback($pattern, function () use ($PostMedia) {
return Renderer::replaceMacros(Renderer::getMarkupTemplate('content/image/single_with_height_allocation.tpl'), [ return Renderer::replaceMacros(Renderer::getMarkupTemplate('content/image/single_with_height_allocation.tpl'), [
'$image' => $PostMedia, '$image' => $PostMedia,
'$sensitive' => $sensitive,
'$allocated_height' => $PostMedia->getAllocatedHeight(), '$allocated_height' => $PostMedia->getAllocatedHeight(),
'$allocated_max_width' => ($PostMedia->previewWidth ?? $PostMedia->width) . 'px', '$allocated_max_width' => ($PostMedia->previewWidth ?? $PostMedia->width) . 'px',
]); ]);
@ -3576,10 +3653,9 @@ class Item
* *
* @param PostMedias $PostMedias * @param PostMedias $PostMedias
* @param string $body * @param string $body
* @param bool $sensitive
* @return string modified body * @return string modified body
*/ */
private static function replaceVisualAttachments(PostMedias $PostMedias, string $body, bool $sensitive): string private static function replaceVisualAttachments(PostMedias $PostMedias, string $body): string
{ {
DI::profiler()->startRecording('rendering'); DI::profiler()->startRecording('rendering');
@ -3588,7 +3664,7 @@ class Item
if (DI::baseUrl()->isLocalUri($PostMedia->preview)) { if (DI::baseUrl()->isLocalUri($PostMedia->preview)) {
continue; continue;
} }
$proxy = DI::baseUrl() . $PostMedia->getPreviewPath(Proxy::SIZE_LARGE, $sensitive); $proxy = DI::baseUrl() . $PostMedia->getPreviewPath(Proxy::SIZE_LARGE);
$search = ['[img=' . $PostMedia->preview . ']', ']' . $PostMedia->preview . '[/img]']; $search = ['[img=' . $PostMedia->preview . ']', ']' . $PostMedia->preview . '[/img]'];
$replace = ['[img=' . $proxy . ']', ']' . $proxy . '[/img]']; $replace = ['[img=' . $proxy . ']', ']' . $proxy . '[/img]'];
@ -3597,7 +3673,7 @@ class Item
if (DI::baseUrl()->isLocalUri($PostMedia->url)) { if (DI::baseUrl()->isLocalUri($PostMedia->url)) {
continue; continue;
} }
$proxy = DI::baseUrl() . $PostMedia->getPreviewPath(Proxy::SIZE_LARGE, $sensitive); $proxy = DI::baseUrl() . $PostMedia->getPreviewPath(Proxy::SIZE_LARGE);
$search = ['[img=' . $PostMedia->url . ']', ']' . $PostMedia->url . '[/img]']; $search = ['[img=' . $PostMedia->url . ']', ']' . $PostMedia->url . '[/img]'];
$replace = ['[img=' . $proxy . ']', ']' . $proxy . '[/img]']; $replace = ['[img=' . $proxy . ']', ']' . $proxy . '[/img]'];
@ -3615,11 +3691,10 @@ class Item
* @param array $item * @param array $item
* @param string $content * @param string $content
* @param bool $shared * @param bool $shared
* @param bool $sensitive
* @return string modified content * @return string modified content
* @throws ServiceUnavailableException * @throws ServiceUnavailableException
*/ */
private static function addVisualAttachments(PostMedias $PostMedias, array $item, string $content, bool $shared, bool $sensitive): string private static function addVisualAttachments(PostMedias $PostMedias, array $item, string $content, bool $shared): string
{ {
DI::profiler()->startRecording('rendering'); DI::profiler()->startRecording('rendering');
$leading = ''; $leading = '';
@ -3634,7 +3709,7 @@ class Item
if ($PostMedia->mimetype->type == 'image' || $PostMedia->preview) { if ($PostMedia->mimetype->type == 'image' || $PostMedia->preview) {
$preview_size = Proxy::SIZE_MEDIUM; $preview_size = Proxy::SIZE_MEDIUM;
$preview_url = DI::baseUrl() . $PostMedia->getPreviewPath($preview_size, $sensitive); $preview_url = DI::baseUrl() . $PostMedia->getPreviewPath($preview_size);
} else { } else {
$preview_size = 0; $preview_size = 0;
$preview_url = ''; $preview_url = '';
@ -3662,6 +3737,7 @@ class Item
'mime' => (string)$PostMedia->mimetype, 'mime' => (string)$PostMedia->mimetype,
'height' => $height, 'height' => $height,
'width' => $width, 'width' => $width,
'description' => $PostMedia->description,
], ],
]); ]);
if (($item['post-type'] ?? null) == Item::PT_VIDEO) { if (($item['post-type'] ?? null) == Item::PT_VIDEO) {
@ -3728,12 +3804,11 @@ class Item
* @param string $content * @param string $content
* @param bool $shared * @param bool $shared
* @param array $ignore_links A list of URLs to ignore * @param array $ignore_links A list of URLs to ignore
* @param bool $sensitive
* @return string modified content * @return string modified content
* @throws InternalServerErrorException * @throws InternalServerErrorException
* @throws ServiceUnavailableException * @throws ServiceUnavailableException
*/ */
private static function addLinkAttachment(int $uriid, array $attachments, string $body, string $content, bool $shared, array $ignore_links, bool $sensitive): string private static function addLinkAttachment(int $uriid, array $attachments, string $body, string $content, bool $shared, array $ignore_links): string
{ {
DI::profiler()->startRecording('rendering'); DI::profiler()->startRecording('rendering');
// Don't show a preview when there is a visual attachment (audio or video) // Don't show a preview when there is a visual attachment (audio or video)
@ -3776,9 +3851,9 @@ class Item
if ($preview && $attachment->preview) { if ($preview && $attachment->preview) {
if ($attachment->previewWidth >= 500) { if ($attachment->previewWidth >= 500) {
$data['image'] = DI::baseUrl() . $attachment->getPreviewPath(Proxy::SIZE_MEDIUM, $sensitive); $data['image'] = DI::baseUrl() . $attachment->getPreviewPath(Proxy::SIZE_MEDIUM);
} else { } else {
$data['preview'] = DI::baseUrl() . $attachment->getPreviewPath(Proxy::SIZE_MEDIUM, $sensitive); $data['preview'] = DI::baseUrl() . $attachment->getPreviewPath(Proxy::SIZE_MEDIUM);
} }
} }
@ -3803,11 +3878,6 @@ class Item
$data = BBCode::getAttachmentData($match[1]); $data = BBCode::getAttachmentData($match[1]);
} }
if ($sensitive) {
$data['image'] = '';
$data['preview'] = '';
}
DI::profiler()->stopRecording(); DI::profiler()->stopRecording();
if (isset($data['url']) && !in_array(strtolower($data['url']), $ignore_links)) { if (isset($data['url']) && !in_array(strtolower($data['url']), $ignore_links)) {
@ -3830,6 +3900,11 @@ class Item
$preview_mode = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'preview_mode', BBCode::PREVIEW_LARGE); $preview_mode = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'preview_mode', BBCode::PREVIEW_LARGE);
if ($preview_mode != BBCode::PREVIEW_NONE) { if ($preview_mode != BBCode::PREVIEW_NONE) {
$rendered = BBCode::convertAttachment('', BBCode::INTERNAL, false, $data, $uriid, $preview_mode); $rendered = BBCode::convertAttachment('', BBCode::INTERNAL, false, $data, $uriid, $preview_mode);
} elseif (!self::containsLink($content, $data['url'], Post\Media::HTML)) {
$rendered = Renderer::replaceMacros(Renderer::getMarkupTemplate('content/link.tpl'), [
'$url' => $data['url'],
'$title' => $data['title'],
]);
} else { } else {
$rendered = ''; $rendered = '';
} }
@ -3950,10 +4025,17 @@ class Item
{ {
if (!empty($item['plink']) && Network::isValidHttpUrl($item['plink'])) { if (!empty($item['plink']) && Network::isValidHttpUrl($item['plink'])) {
$plink = $item['plink']; $plink = $item['plink'];
} elseif (!empty($item['uri']) && Network::isValidHttpUrl($item['uri']) && !Network::isLocalLink($item['uri'])) { } elseif (!empty($item['uri']) && Network::isValidHttpUrl($item['uri']) && !DI::baseUrl()->isLocalUrl($item['uri'])) {
$plink = $item['uri']; $plink = $item['uri'];
} }
if (($item['post-reason'] == self::PR_ANNOUNCEMENT) && ($item['owner-contact-type'] == Contact::TYPE_COMMUNITY) && ($item['owner-network'] == Protocol::DFRN)) {
$contact = Contact::getById($item['owner-id'], ['baseurl']);
if (!empty($contact['baseurl'])) {
$plink = $contact['baseurl'] . '/display/' . $item['guid'];
}
}
if (DI::userSession()->getLocalUserId()) { if (DI::userSession()->getLocalUserId()) {
$ret = [ $ret = [
'href' => "display/" . $item['guid'], 'href' => "display/" . $item['guid'],
@ -4084,6 +4166,10 @@ class Item
return $item_id; return $item_id;
} }
if (ActivityPub\Processor::alreadyKnown($uri, '')) {
return 0;
}
$hookData = [ $hookData = [
'uri' => $uri, 'uri' => $uri,
'uid' => $uid, 'uid' => $uid,
@ -4096,10 +4182,15 @@ class Item
return is_numeric($hookData['item_id']) ? $hookData['item_id'] : 0; return is_numeric($hookData['item_id']) ? $hookData['item_id'] : 0;
} }
$curlResult = DI::httpClient()->head($uri, [HttpClientOptions::ACCEPT_CONTENT => HttpClientAccept::JSON_AS]); try {
$curlResult = DI::httpClient()->head($uri, [HttpClientOptions::ACCEPT_CONTENT => HttpClientAccept::JSON_AS, HttpClientOptions::REQUEST => HttpClientRequest::ACTIVITYPUB]);
if (HTTPSignature::isValidContentType($curlResult->getContentType(), $uri)) { if (HTTPSignature::isValidContentType($curlResult->getContentType(), $uri)) {
$fetched_uri = ActivityPub\Processor::fetchMissingActivity($uri, [], '', $completion, $uid); $fetched_uri = ActivityPub\Processor::fetchMissingActivity($uri, [], '', $completion, $uid);
} }
} catch (\Throwable $th) {
Logger::info('Invalid link', ['uid' => $uid, 'uri' => $uri, 'code' => $th->getCode(), 'message' => $th->getMessage()]);
return 0;
}
if (!empty($fetched_uri)) { if (!empty($fetched_uri)) {
$item_id = self::searchByLink($fetched_uri, $uid); $item_id = self::searchByLink($fetched_uri, $uid);
@ -4174,4 +4265,22 @@ class Item
Logger::warning('Post does not exist although it was supposed to had been fetched.', ['id' => $id, 'url' => $url, 'uid' => $uid]); Logger::warning('Post does not exist although it was supposed to had been fetched.', ['id' => $id, 'url' => $url, 'uid' => $uid]);
return 0; return 0;
} }
public static function incrementInbound(string $network)
{
$packets = (int)(DI::keyValue()->get('stats_packets_inbound_' . $network) ?? 0);
if ($packets >= PHP_INT_MAX) {
$packets = 0;
}
DI::keyValue()->set('stats_packets_inbound_' . $network, $packets + 1);
}
public static function incrementOutbound(string $network)
{
$packets = (int)(DI::keyValue()->get('stats_packets_outbound_' . $network) ?? 0);
if ($packets >= PHP_INT_MAX) {
$packets = 0;
}
DI::keyValue()->set('stats_packets_outbound_' . $network, $packets + 1);
}
} }

View file

@ -53,6 +53,8 @@ class Nodeinfo
return; return;
} }
$logger->info('User statistics - start');
$userStats = User::getStatistics(); $userStats = User::getStatistics();
DI::keyValue()->set('nodeinfo_total_users', $userStats['total_users']); DI::keyValue()->set('nodeinfo_total_users', $userStats['total_users']);
@ -60,14 +62,19 @@ class Nodeinfo
DI::keyValue()->set('nodeinfo_active_users_monthly', $userStats['active_users_monthly']); DI::keyValue()->set('nodeinfo_active_users_monthly', $userStats['active_users_monthly']);
DI::keyValue()->set('nodeinfo_active_users_weekly', $userStats['active_users_weekly']); DI::keyValue()->set('nodeinfo_active_users_weekly', $userStats['active_users_weekly']);
$logger->info('user statistics', $userStats); $logger->info('user statistics - done', $userStats);
$posts = DBA::count('post-thread', ["`uri-id` IN (SELECT `uri-id` FROM `post-user` WHERE NOT `deleted` AND `origin`)"]); $posts = DBA::count('post-thread', ["`uri-id` IN (SELECT `uri-id` FROM `post-user` WHERE NOT `deleted` AND `origin`)"]);
$comments = DBA::count('post', ["NOT `deleted` AND `gravity` = ? AND `uri-id` IN (SELECT `uri-id` FROM `post-user` WHERE `origin`)", Item::GRAVITY_COMMENT]); $comments = DBA::count('post', ["NOT `deleted` AND `gravity` = ? AND `uri-id` IN (SELECT `uri-id` FROM `post-user` WHERE `origin`)", Item::GRAVITY_COMMENT]);
DI::keyValue()->set('nodeinfo_local_posts', $posts); DI::keyValue()->set('nodeinfo_local_posts', $posts);
DI::keyValue()->set('nodeinfo_local_comments', $comments); DI::keyValue()->set('nodeinfo_local_comments', $comments);
$logger->info('User activity', ['posts' => $posts, 'comments' => $comments]); $posts = DBA::count('post', ['deleted' => false, 'gravity' => Item::GRAVITY_COMMENT]);
$comments = DBA::count('post', ['deleted' => false, 'gravity' => Item::GRAVITY_COMMENT]);
DI::keyValue()->set('nodeinfo_total_posts', $posts);
DI::keyValue()->set('nodeinfo_total_comments', $comments);
$logger->info('Post statistics - done', ['posts' => $posts, 'comments' => $comments]);
} }
/** /**

View file

@ -32,6 +32,8 @@ use Friendica\Core\Storage\Exception\ReferenceStorageException;
use Friendica\Core\Storage\Exception\StorageException; use Friendica\Core\Storage\Exception\StorageException;
use Friendica\Core\Storage\Type\SystemResource; use Friendica\Core\Storage\Type\SystemResource;
use Friendica\Network\HTTPClient\Client\HttpClientAccept; use Friendica\Network\HTTPClient\Client\HttpClientAccept;
use Friendica\Network\HTTPClient\Client\HttpClientOptions;
use Friendica\Network\HTTPClient\Client\HttpClientRequest;
use Friendica\Object\Image; use Friendica\Object\Image;
use Friendica\Util\DateTimeFormat; use Friendica\Util\DateTimeFormat;
use Friendica\Util\Images; use Friendica\Util\Images;
@ -234,7 +236,8 @@ class Photo
FROM `photo` WHERE `uid` = ? AND NOT `photo-type` IN (?, ?) $sqlExtra FROM `photo` WHERE `uid` = ? AND NOT `photo-type` IN (?, ?) $sqlExtra
GROUP BY `resource-id` $sqlExtra2", GROUP BY `resource-id` $sqlExtra2",
$values $values
)); )
);
} }
/** /**
@ -580,7 +583,9 @@ class Photo
$micro = ''; $micro = '';
$photo = DBA::selectFirst( $photo = DBA::selectFirst(
'photo', ['resource-id'], ['uid' => $uid, 'contact-id' => $cid, 'scale' => 4, 'photo-type' => self::CONTACT_AVATAR] 'photo',
['resource-id'],
['uid' => $uid, 'contact-id' => $cid, 'scale' => 4, 'photo-type' => self::CONTACT_AVATAR]
); );
if (!empty($photo['resource-id'])) { if (!empty($photo['resource-id'])) {
$resource_id = $photo['resource-id']; $resource_id = $photo['resource-id'];
@ -597,7 +602,7 @@ class Photo
$filename = basename($image_url); $filename = basename($image_url);
if (!empty($image_url)) { if (!empty($image_url)) {
$ret = DI::httpClient()->get($image_url, HttpClientAccept::IMAGE); $ret = DI::httpClient()->get($image_url, HttpClientAccept::IMAGE, [HttpClientOptions::REQUEST => HttpClientRequest::MEDIAPROXY]);
Logger::debug('Got picture', ['Content-Type' => $ret->getHeader('Content-Type'), 'url' => $image_url]); Logger::debug('Got picture', ['Content-Type' => $ret->getHeader('Content-Type'), 'url' => $image_url]);
$img_str = $ret->getBodyString(); $img_str = $ret->getBodyString();
$type = $ret->getContentType(); $type = $ret->getContentType();
@ -681,7 +686,9 @@ class Photo
} }
$photo = DBA::selectFirst( $photo = DBA::selectFirst(
'photo', ['blurhash'], ['uid' => $uid, 'contact-id' => $cid, 'scale' => 4, 'photo-type' => self::CONTACT_AVATAR] 'photo',
['blurhash'],
['uid' => $uid, 'contact-id' => $cid, 'scale' => 4, 'photo-type' => self::CONTACT_AVATAR]
); );
return [$image_url, $thumb, $micro, $photo['blurhash']]; return [$image_url, $thumb, $micro, $photo['blurhash']];
@ -751,7 +758,8 @@ class Photo
if (!DI::config()->get('system', 'no_count', false)) { if (!DI::config()->get('system', 'no_count', false)) {
/// @todo This query needs to be renewed. It is really slow /// @todo This query needs to be renewed. It is really slow
// At this time we just store the data in the cache // At this time we just store the data in the cache
$albums = DBA::toArray(DBA::p("SELECT COUNT(DISTINCT `resource-id`) AS `total`, `album`, MIN(`created`) AS `created` $albums = DBA::toArray(DBA::p(
"SELECT COUNT(DISTINCT `resource-id`) AS `total`, `album`, MIN(`created`) AS `created`
FROM `photo` FROM `photo`
WHERE `uid` = ? AND `photo-type` IN (?, ?, ?) $sql_extra WHERE `uid` = ? AND `photo-type` IN (?, ?, ?) $sql_extra
GROUP BY `album` ORDER BY `created` DESC", GROUP BY `album` ORDER BY `created` DESC",
@ -762,7 +770,8 @@ class Photo
)); ));
} else { } else {
// This query doesn't do the count and is much faster // This query doesn't do the count and is much faster
$albums = DBA::toArray(DBA::p("SELECT '' AS `total`, `album`, MIN(`created`) AS `created` $albums = DBA::toArray(DBA::p(
"SELECT '' AS `total`, `album`, MIN(`created`) AS `created`
FROM `photo` USE INDEX (`uid_album_scale_created`) FROM `photo` USE INDEX (`uid_album_scale_created`)
WHERE `uid` = ? AND `photo-type` IN (?, ?, ?) $sql_extra WHERE `uid` = ? AND `photo-type` IN (?, ?, ?) $sql_extra
GROUP BY `album` ORDER BY `created` DESC", GROUP BY `album` ORDER BY `created` DESC",
@ -902,9 +911,11 @@ class Photo
*/ */
public static function setPermissionForResource(string $image_rid, int $uid, string $str_contact_allow, string $str_circle_allow, string $str_contact_deny, string $str_circle_deny) public static function setPermissionForResource(string $image_rid, int $uid, string $str_contact_allow, string $str_circle_allow, string $str_contact_deny, string $str_circle_deny)
{ {
$fields = ['allow_cid' => $str_contact_allow, 'allow_gid' => $str_circle_allow, $fields = [
'allow_cid' => $str_contact_allow, 'allow_gid' => $str_circle_allow,
'deny_cid' => $str_contact_deny, 'deny_gid' => $str_circle_deny, 'deny_cid' => $str_contact_deny, 'deny_gid' => $str_circle_deny,
'accessible' => DI::pConfig()->get($uid, 'system', 'accessible-photos', false)]; 'accessible' => DI::pConfig()->get($uid, 'system', 'accessible-photos', false)
];
$condition = ['resource-id' => $image_rid, 'uid' => $uid]; $condition = ['resource-id' => $image_rid, 'uid' => $uid];
Logger::info('Set permissions', ['condition' => $condition, 'permissions' => $fields]); Logger::info('Set permissions', ['condition' => $condition, 'permissions' => $fields]);
@ -1046,7 +1057,7 @@ class Photo
{ {
$filename = basename($image_url); $filename = basename($image_url);
if (!empty($image_url)) { if (!empty($image_url)) {
$ret = DI::httpClient()->get($image_url, HttpClientAccept::IMAGE); $ret = DI::httpClient()->get($image_url, HttpClientAccept::IMAGE, [HttpClientOptions::REQUEST => HttpClientRequest::MEDIAPROXY]);
Logger::debug('Got picture', ['Content-Type' => $ret->getHeader('Content-Type'), 'url' => $image_url]); Logger::debug('Got picture', ['Content-Type' => $ret->getHeader('Content-Type'), 'url' => $image_url]);
$img_str = $ret->getBodyString(); $img_str = $ret->getBodyString();
$type = $ret->getContentType(); $type = $ret->getContentType();

View file

@ -379,6 +379,21 @@ class Post
return self::selectView('post-user-view', $selected, $condition, $params); return self::selectView('post-user-view', $selected, $condition, $params);
} }
/**
* Select rows from the post-origin-view view
*
* @param array $selected Array of selected fields, empty for all
* @param array $condition Array of fields for condition
* @param array $params Array of several parameters
*
* @return boolean|object
* @throws \Exception
*/
public static function selectOrigin(array $selected = [], array $condition = [], array $params = [])
{
return self::selectView('post-origin-view', $selected, $condition, $params);
}
/** /**
* Select rows from the post-view view * Select rows from the post-view view
* *
@ -424,6 +439,21 @@ class Post
return self::selectView('post-thread-view', $selected, $condition, $params); return self::selectView('post-thread-view', $selected, $condition, $params);
} }
/**
* Select rows from the post-thread-origin-view view
*
* @param array $selected Array of selected fields, empty for all
* @param array $condition Array of fields for condition
* @param array $params Array of several parameters
*
* @return boolean|object
* @throws \Exception
*/
public static function selectOriginThread(array $selected = [], array $condition = [], array $params = [])
{
return self::selectView('post-thread-origin-view', $selected, $condition, $params);
}
/** /**
* Select rows from the given view for a given user * Select rows from the given view for a given user
* *
@ -513,6 +543,11 @@ class Post
return self::selectViewForUser('post-timeline-view', $uid, $selected, $condition, $params); return self::selectViewForUser('post-timeline-view', $uid, $selected, $condition, $params);
} }
public static function selectLocalTimelineForUser(int $uid, array $selected = [], array $condition = [], array $params = [])
{
return self::selectViewForUser('post-timeline-origin-view', $uid, $selected, $condition, $params);
}
/** /**
* Select rows from the post-thread-user-view view for a given user * Select rows from the post-thread-user-view view for a given user
* *

View file

@ -124,7 +124,7 @@ class Content
'limit' => [$start, $limit] 'limit' => [$start, $limit]
]; ];
$tags = DBA::select('post-searchindex', ['uri-id'], $condition, $params); $tags = DBA::select(SearchIndex::getSearchTable(), ['uri-id'], $condition, $params);
$uriids = []; $uriids = [];
while ($tag = DBA::fetch($tags)) { while ($tag = DBA::fetch($tags)) {
@ -141,8 +141,8 @@ class Content
if ($uid != 0) { if ($uid != 0) {
$condition = ["MATCH (`searchtext`) AGAINST (? IN BOOLEAN MODE) AND (NOT `restricted` OR `uri-id` IN (SELECT `uri-id` FROM `post-user` WHERE `uid` = ?))", $search, $uid]; $condition = ["MATCH (`searchtext`) AGAINST (? IN BOOLEAN MODE) AND (NOT `restricted` OR `uri-id` IN (SELECT `uri-id` FROM `post-user` WHERE `uid` = ?))", $search, $uid];
} else { } else {
$condition = ["MATCH (`searchtext`) AGAINST (? IN BOOLEAN MODE) AND NOT `restricted", $search]; $condition = ["MATCH (`searchtext`) AGAINST (? IN BOOLEAN MODE) AND NOT `restricted`", $search];
} }
return DBA::count('post-searchindex', $condition); return DBA::count(SearchIndex::getSearchTable(), $condition);
} }
} }

View file

@ -418,4 +418,12 @@ class Engagement
} }
return $fullTextSearch; return $fullTextSearch;
} }
public static function unescapeKeywords(string $fullTextSearch): string
{
foreach (self::KEYWORDS as $keyword) {
$fullTextSearch = preg_replace('~(' . $keyword . ')_(.[\w\*@\.-]+)~', '$1:$2', $fullTextSearch);
}
return $fullTextSearch;
}
} }

View file

@ -28,6 +28,7 @@ use Friendica\Core\Protocol;
use Friendica\Database\Database; use Friendica\Database\Database;
use Friendica\Database\DBA; use Friendica\Database\DBA;
use Friendica\DI; use Friendica\DI;
use Friendica\Model\Attach;
use Friendica\Model\Contact; use Friendica\Model\Contact;
use Friendica\Model\Item; use Friendica\Model\Item;
use Friendica\Model\ItemURI; use Friendica\Model\ItemURI;
@ -35,6 +36,7 @@ use Friendica\Model\Photo;
use Friendica\Model\Post; use Friendica\Model\Post;
use Friendica\Network\HTTPClient\Client\HttpClientAccept; use Friendica\Network\HTTPClient\Client\HttpClientAccept;
use Friendica\Network\HTTPClient\Client\HttpClientOptions; use Friendica\Network\HTTPClient\Client\HttpClientOptions;
use Friendica\Network\HTTPClient\Client\HttpClientRequest;
use Friendica\Protocol\ActivityPub; use Friendica\Protocol\ActivityPub;
use Friendica\Util\Images; use Friendica\Util\Images;
use Friendica\Util\Network; use Friendica\Util\Network;
@ -86,8 +88,8 @@ class Media
// "document" has got the lowest priority. So when the same file is both attached as document // "document" has got the lowest priority. So when the same file is both attached as document
// and embedded as picture then we only store the picture or replace the document // and embedded as picture then we only store the picture or replace the document
$found = DBA::selectFirst('post-media', ['type'], ['uri-id' => $media['uri-id'], 'url' => $media['url']]); $found = DBA::selectFirst('post-media', ['type'], ['uri-id' => $media['uri-id'], 'url' => $media['url']]);
if (!$force && !empty($found) && (($found['type'] != self::DOCUMENT) || ($media['type'] == self::DOCUMENT))) { if (!$force && !empty($found) && (!in_array($found['type'], [self::UNKNOWN, self::DOCUMENT]) || ($media['type'] == self::DOCUMENT))) {
Logger::info('Media already exists', ['uri-id' => $media['uri-id'], 'url' => $media['url']]); Logger::info('Media already exists', ['uri-id' => $media['uri-id'], 'url' => $media['url'], 'found' => $found['type'], 'new' => $media['type']]);
return false; return false;
} }
@ -180,14 +182,20 @@ class Media
*/ */
public static function fetchAdditionalData(array $media): array public static function fetchAdditionalData(array $media): array
{ {
if (Network::isLocalLink($media['url'])) { if (DI::baseUrl()->isLocalUrl($media['url'])) {
$media = self::fetchLocalData($media); $media = self::fetchLocalData($media);
if (preg_match('|.*?/search\?(.+)|', $media['url'], $matches)) {
return $media;
}
if (empty($media['mimetype']) || empty($media['size'])) {
Logger::debug('Unknown local link', ['url' => $media['url']]);
}
} }
// Fetch the mimetype or size if missing. // Fetch the mimetype or size if missing.
if (Network::isValidHttpUrl($media['url']) && (empty($media['mimetype']) || empty($media['size']))) { if (Network::isValidHttpUrl($media['url']) && (empty($media['mimetype']) || empty($media['size']))) {
$timeout = DI::config()->get('system', 'xrd_timeout'); $timeout = DI::config()->get('system', 'xrd_timeout');
$curlResult = DI::httpClient()->head($media['url'], [HttpClientOptions::TIMEOUT => $timeout]); $curlResult = DI::httpClient()->head($media['url'], [HttpClientOptions::TIMEOUT => $timeout, HttpClientOptions::REQUEST => HttpClientRequest::CONTENTTYPE]);
// Workaround for systems that can't handle a HEAD request // Workaround for systems that can't handle a HEAD request
if (!$curlResult->isSuccess() && ($curlResult->getReturnCode() == 405)) { if (!$curlResult->isSuccess() && ($curlResult->getReturnCode() == 405)) {
@ -389,7 +397,17 @@ class Media
*/ */
private static function fetchLocalData(array $media): array private static function fetchLocalData(array $media): array
{ {
if (!preg_match('|.*?/photo/(.*[a-fA-F0-9])\-(.*[0-9])\..*[\w]|', $media['url'] ?? '', $matches)) { if (preg_match('|.*?/attach/(\d+)|', $media['url'], $matches)) {
$attachment = Attach::selectFirst(['filename', 'filetype', 'filesize'], ['id' => $matches[1]]);
if (!empty($attachment)) {
$media['name'] = $attachment['filename'];
$media['mimetype'] = $attachment['filetype'];
$media['size'] = $attachment['filesize'];
}
return $media;
}
if (!preg_match('|.*?/photo/(.*[a-fA-F0-9])\-(.*[0-9])\..*[\w]|', $media['url'], $matches)) {
return $media; return $media;
} }
$photo = Photo::selectFirst([], ['resource-id' => $matches[1], 'scale' => $matches[2]]); $photo = Photo::selectFirst([], ['resource-id' => $matches[1], 'scale' => $matches[2]]);
@ -426,42 +444,46 @@ class Media
return $data; return $data;
} }
$type = explode('/', current(explode(';', $data['mimetype']))); $data['type'] = self::getType($data['mimetype']);
if (count($type) < 2) {
Logger::info('Unknown MimeType', ['type' => $type, 'media' => $data]);
$data['type'] = self::UNKNOWN;
return $data; return $data;
} }
public static function getType(string $mimeType): int
{
$type = explode('/', current(explode(';', $mimeType)));
if (count($type) < 2) {
Logger::info('Unknown MimeType', ['type' => $type, 'media' => $mimeType]);
return self::UNKNOWN;
}
$filetype = strtolower($type[0]); $filetype = strtolower($type[0]);
$subtype = strtolower($type[1]); $subtype = strtolower($type[1]);
if ($filetype == 'image') { if ($filetype == 'image') {
$data['type'] = self::IMAGE; $type = self::IMAGE;
} elseif ($filetype == 'video') { } elseif ($filetype == 'video') {
$data['type'] = self::VIDEO; $type = self::VIDEO;
} elseif ($filetype == 'audio') { } elseif ($filetype == 'audio') {
$data['type'] = self::AUDIO; $type = self::AUDIO;
} elseif (($filetype == 'text') && ($subtype == 'html')) { } elseif (($filetype == 'text') && ($subtype == 'html')) {
$data['type'] = self::HTML; $type = self::HTML;
} elseif (($filetype == 'text') && ($subtype == 'xml')) { } elseif (($filetype == 'text') && ($subtype == 'xml')) {
$data['type'] = self::XML; $type = self::XML;
} elseif (($filetype == 'text') && ($subtype == 'plain')) { } elseif (($filetype == 'text') && ($subtype == 'plain')) {
$data['type'] = self::PLAIN; $type = self::PLAIN;
} elseif ($filetype == 'text') { } elseif ($filetype == 'text') {
$data['type'] = self::TEXT; $type = self::TEXT;
} elseif (($filetype == 'application') && ($subtype == 'x-bittorrent')) { } elseif (($filetype == 'application') && ($subtype == 'x-bittorrent')) {
$data['type'] = self::TORRENT; $type = self::TORRENT;
} elseif ($filetype == 'application') { } elseif ($filetype == 'application') {
$data['type'] = self::APPLICATION; $type = self::APPLICATION;
} else { } else {
$data['type'] = self::UNKNOWN; $type = self::UNKNOWN;
Logger::info('Unknown type', ['filetype' => $filetype, 'subtype' => $subtype, 'media' => $data]); Logger::info('Unknown type', ['filetype' => $filetype, 'subtype' => $subtype, 'media' => $mimeType]);
return $data;
} }
Logger::debug('Detected type', ['filetype' => $filetype, 'subtype' => $subtype, 'media' => $data]); Logger::debug('Detected type', ['filetype' => $filetype, 'subtype' => $subtype, 'media' => $mimeType]);
return $data; return $type;
} }
/** /**
@ -902,8 +924,20 @@ class Media
$body = BBCode::removeAttachment($body); $body = BBCode::removeAttachment($body);
foreach (self::getByURIId($uriid, $types) as $media) { foreach (self::getByURIId($uriid, $types) as $media) {
$body = self::addAttachmentToBody($media, $body);
}
if (preg_match("/.*(\[attachment.*?\].*?\[\/attachment\]).*/ism", $original_body, $match)) {
$body .= "\n" . $match[1];
}
return $body;
}
public static function addAttachmentToBody(array $media, string $body): string
{
if (Item::containsLink($body, $media['preview'] ?? $media['url'], $media['type'])) { if (Item::containsLink($body, $media['preview'] ?? $media['url'], $media['type'])) {
continue; return $body;
} }
if ($media['type'] == self::IMAGE) { if ($media['type'] == self::IMAGE) {
@ -915,12 +949,6 @@ class Media
} else { } else {
$body .= "\n[url]" . $media['url'] . "[/url]\n"; $body .= "\n[url]" . $media['url'] . "[/url]\n";
} }
}
if (preg_match("/.*(\[attachment.*?\].*?\[\/attachment\]).*/ism", $original_body, $match)) {
$body .= "\n" . $match[1];
}
return $body; return $body;
} }

93
src/Model/Post/Origin.php Normal file
View file

@ -0,0 +1,93 @@
<?php
/**
* @copyright Copyright (C) 2010-2024, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Model\Post;
use Friendica\Database\DBA;
use \BadMethodCallException;
use Friendica\Database\Database;
use Friendica\DI;
class Origin
{
/**
* Insert a new post origin entry
*
* @param array $fields
* @return boolean was the insert successful?
* @throws \Exception
*/
public static function insert(array $data = []): bool
{
if (!$data['origin'] || ($data['uid'] == 0)) {
return false;
}
$fields = DI::dbaDefinition()->truncateFieldsForTable('post-origin', $data);
return DBA::insert('post-origin', $fields, Database::INSERT_IGNORE);
}
/**
* Update a post origin entry
*
* @param integer $uri_id
* @param integer $uid
* @param array $data
* @param bool $insert_if_missing
* @return bool
* @throws \Exception
*/
public static function update(int $uri_id, int $uid, array $data = [], bool $insert_if_missing = false)
{
if (empty($uri_id)) {
throw new BadMethodCallException('Empty URI_id');
}
$fields = DI::dbaDefinition()->truncateFieldsForTable('post-origin', $data);
// Remove the key fields
unset($fields['uri-id']);
unset($fields['uid']);
if (empty($fields)) {
return true;
}
return DBA::update('post-origin', $fields, ['uri-id' => $uri_id, 'uid' => $uid], $insert_if_missing ? true : []);
}
/**
* Delete a row from the post-origin table
*
* @param array $conditions Field condition(s)
* @param array $options
* - cascade: If true we delete records in other tables that depend on the one we're deleting through
* relations (default: true)
*
* @return boolean was the delete successful?
* @throws \Exception
*/
public static function delete(array $conditions, array $options = [])
{
return DBA::delete('post-origin', $conditions, $options);
}
}

View file

@ -99,4 +99,14 @@ class SearchIndex
} }
return DateTimeFormat::utc('now - ' . $days . ' day'); return DateTimeFormat::utc('now - ' . $days . ' day');
} }
public static function getSearchTable(): string
{
return DI::config()->get('system', 'limited_search_scope') ? 'post-engagement' : 'post-searchindex';
}
public static function getSearchView(): string
{
return DI::config()->get('system', 'limited_search_scope') ? 'post-engagement-user-view' : 'post-searchindex-user-view';
}
} }

View file

@ -31,20 +31,14 @@ use Friendica\Core\Logger;
use Friendica\Core\Protocol; use Friendica\Core\Protocol;
use Friendica\Core\Renderer; use Friendica\Core\Renderer;
use Friendica\Core\Search; use Friendica\Core\Search;
use Friendica\Core\System;
use Friendica\Core\Worker; use Friendica\Core\Worker;
use Friendica\Database\DBA; use Friendica\Database\DBA;
use Friendica\DI; use Friendica\DI;
use Friendica\Network\HTTPClient\Client\HttpClientAccept;
use Friendica\Network\HTTPClient\Client\HttpClientOptions;
use Friendica\Network\HTTPException; use Friendica\Network\HTTPException;
use Friendica\Network\HTTPException\InternalServerErrorException;
use Friendica\Protocol\Activity; use Friendica\Protocol\Activity;
use Friendica\Protocol\Diaspora; use Friendica\Protocol\Diaspora;
use Friendica\Security\PermissionSet\Entity\PermissionSet; use Friendica\Security\PermissionSet\Entity\PermissionSet;
use Friendica\Util\DateTimeFormat; use Friendica\Util\DateTimeFormat;
use Friendica\Util\HTTPSignature;
use Friendica\Util\Network;
use Friendica\Util\Proxy; use Friendica\Util\Proxy;
use Friendica\Util\Strings; use Friendica\Util\Strings;
@ -296,7 +290,7 @@ class Profile
if (DI::userSession()->getLocalUserId() && ($profile['uid'] ?? 0) != DI::userSession()->getLocalUserId()) { if (DI::userSession()->getLocalUserId() && ($profile['uid'] ?? 0) != DI::userSession()->getLocalUserId()) {
$profile_contact = Contact::getByURL($profile['nurl'], null, [], DI::userSession()->getLocalUserId()); $profile_contact = Contact::getByURL($profile['nurl'], null, [], DI::userSession()->getLocalUserId());
} }
if (!empty($profile['cid']) && self::getMyURL()) { if (!empty($profile['cid']) && DI::userSession()->getMyUrl()) {
$profile_contact = Contact::selectFirst([], ['id' => $profile['cid']]); $profile_contact = Contact::selectFirst([], ['id' => $profile['cid']]);
} }
@ -321,19 +315,19 @@ class Profile
// Who is the logged-in user to this profile? // Who is the logged-in user to this profile?
$visitor_contact = []; $visitor_contact = [];
if (!empty($profile['uid']) && self::getMyURL()) { if (!empty($profile['uid']) && DI::userSession()->getMyUrl()) {
$visitor_contact = Contact::selectFirst(['rel'], ['uid' => $profile['uid'], 'nurl' => Strings::normaliseLink(self::getMyURL())]); $visitor_contact = Contact::selectFirst(['rel'], ['uid' => $profile['uid'], 'nurl' => Strings::normaliseLink(DI::userSession()->getMyUrl())]);
} }
$local_user_is_self = self::getMyURL() && ($profile['url'] == self::getMyURL()); $local_user_is_self = DI::userSession()->getMyUrl() && ($profile['url'] == DI::userSession()->getMyUrl());
$visitor_is_authenticated = (bool)self::getMyURL(); $visitor_is_authenticated = (bool)DI::userSession()->getMyUrl();
$visitor_is_following = $visitor_is_following =
in_array($visitor_contact['rel'] ?? 0, [Contact::FOLLOWER, Contact::FRIEND]) in_array($visitor_contact['rel'] ?? 0, [Contact::FOLLOWER, Contact::FRIEND])
|| in_array($profile_contact['rel'] ?? 0, [Contact::SHARING, Contact::FRIEND]); || in_array($profile_contact['rel'] ?? 0, [Contact::SHARING, Contact::FRIEND]);
$visitor_is_followed = $visitor_is_followed =
in_array($visitor_contact['rel'] ?? 0, [Contact::SHARING, Contact::FRIEND]) in_array($visitor_contact['rel'] ?? 0, [Contact::SHARING, Contact::FRIEND])
|| in_array($profile_contact['rel'] ?? 0, [Contact::FOLLOWER, Contact::FRIEND]); || in_array($profile_contact['rel'] ?? 0, [Contact::FOLLOWER, Contact::FRIEND]);
$visitor_base_path = self::getMyURL() ? preg_replace('=/profile/(.*)=ism', '', self::getMyURL()) : ''; $visitor_base_path = DI::userSession()->getMyUrl() ? preg_replace('=/profile/(.*)=ism', '', DI::userSession()->getMyUrl()) : '';
if (!$local_user_is_self) { if (!$local_user_is_self) {
if (!$visitor_is_authenticated) { if (!$visitor_is_authenticated) {
@ -628,8 +622,10 @@ class Profile
$bd_format = DI::l10n()->t('g A l F d'); // 8 AM Friday January 18 $bd_format = DI::l10n()->t('g A l F d'); // 8 AM Friday January 18
$classtoday = ''; $classtoday = '';
$condition = ["`uid` = ? AND `type` != 'birthday' AND `start` < ? AND `start` >= ?", $condition = [
$uid, DateTimeFormat::utc('now + 7 days'), DateTimeFormat::utc('now - 1 days')]; "`uid` = ? AND `type` != 'birthday' AND `start` < ? AND `start` >= ?",
$uid, DateTimeFormat::utc('now + 7 days'), DateTimeFormat::utc('now - 1 days')
];
$s = DBA::select('event', [], $condition, ['order' => ['start']]); $s = DBA::select('event', [], $condition, ['order' => ['start']]);
$r = []; $r = [];
@ -639,9 +635,11 @@ class Profile
$total = 0; $total = 0;
while ($rr = DBA::fetch($s)) { while ($rr = DBA::fetch($s)) {
$condition = ['parent-uri' => $rr['uri'], 'uid' => $rr['uid'], 'author-id' => $pcid, $condition = [
'parent-uri' => $rr['uri'], 'uid' => $rr['uid'], 'author-id' => $pcid,
'vid' => [Verb::getID(Activity::ATTEND), Verb::getID(Activity::ATTENDMAYBE)], 'vid' => [Verb::getID(Activity::ATTEND), Verb::getID(Activity::ATTENDMAYBE)],
'visible' => true, 'deleted' => false]; 'visible' => true, 'deleted' => false
];
if (!Post::exists($condition)) { if (!Post::exists($condition)) {
continue; continue;
} }
@ -695,235 +693,6 @@ class Profile
]); ]);
} }
/**
* Retrieves the my_url session variable
*
* @return string
* @deprecated since version 2022.12, please use UserSession->getMyUrl instead
*/
public static function getMyURL(): string
{
return DI::userSession()->getMyUrl();
}
/**
* Process the 'zrl' parameter and initiate the remote authentication.
*
* This method checks if the visitor has a public contact entry and
* redirects the visitor to his/her instance to start the magic auth (Authentication)
* process.
*
* Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/channel.php
*
* The implementation for Friendica sadly differs in some points from the one for Hubzilla:
* - Hubzilla uses the "zid" parameter, while for Friendica it had been replaced with "zrl"
* - There seem to be some reverse authentication (rmagic) that isn't implemented in Friendica at all
*
* It would be favourable to harmonize the two implementations.
*
* @param App $a Application instance.
*
* @return void
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function zrlInit(App $a)
{
$my_url = self::getMyURL();
$my_url = Network::isUrlValid($my_url);
if (empty($my_url) || DI::userSession()->getLocalUserId()) {
return;
}
$addr = $_GET['addr'] ?? $my_url;
$arr = ['zrl' => $my_url, 'url' => DI::args()->getCommand()];
Hook::callAll('zrl_init', $arr);
// Try to find the public contact entry of the visitor.
$cid = Contact::getIdForURL($my_url);
if (!$cid) {
Logger::info('No contact record found for ' . $my_url);
return;
}
$contact = DBA::selectFirst('contact',['id', 'url'], ['id' => $cid]);
if (DBA::isResult($contact) && DI::userSession()->getRemoteUserId() && DI::userSession()->getRemoteUserId() == $contact['id']) {
Logger::info('The visitor ' . $my_url . ' is already authenticated');
return;
}
// Avoid endless loops
$cachekey = 'zrlInit:' . $my_url;
if (DI::cache()->get($cachekey)) {
Logger::info('URL ' . $my_url . ' already tried to authenticate.');
return;
} else {
DI::cache()->set($cachekey, true, Duration::MINUTE);
}
Logger::info('Not authenticated. Invoking reverse magic-auth for ' . $my_url);
// Remove the "addr" parameter from the destination. It is later added as separate parameter again.
$addr_request = 'addr=' . urlencode($addr);
$query = rtrim(str_replace($addr_request, '', DI::args()->getQueryString()), '?&');
// The other instance needs to know where to redirect.
$dest = urlencode(DI::baseUrl() . '/' . $query);
// We need to extract the basebath from the profile url
// to redirect the visitors '/magic' module.
$basepath = Contact::getBasepath($contact['url']);
if ($basepath != DI::baseUrl() && !strstr($dest, '/magic')) {
$magic_path = $basepath . '/magic' . '?owa=1&dest=' . $dest . '&' . $addr_request;
// We have to check if the remote server does understand /magic without invoking something
$serverret = DI::httpClient()->head($basepath . '/magic', [HttpClientOptions::ACCEPT_CONTENT => HttpClientAccept::HTML]);
if ($serverret->isSuccess()) {
Logger::info('Doing magic auth for visitor ' . $my_url . ' to ' . $magic_path);
System::externalRedirect($magic_path);
}
}
}
/**
* Set the visitor cookies (see remote_user()) for the given handle
*
* @param string $handle Visitor handle
*
* @return array Visitor contact array
*/
public static function addVisitorCookieForHandle(string $handle): array
{
$a = DI::app();
// Try to find the public contact entry of the visitor.
$cid = Contact::getIdForURL($handle);
if (!$cid) {
Logger::info('Handle not found', ['handle' => $handle]);
return [];
}
$visitor = Contact::getById($cid);
// Authenticate the visitor.
DI::userSession()->setMultiple([
'authenticated' => 1,
'visitor_id' => $visitor['id'],
'visitor_handle' => $visitor['addr'],
'visitor_home' => $visitor['url'],
'my_url' => $visitor['url'],
'remote_comment' => $visitor['subscribe'],
]);
DI::userSession()->setVisitorsContacts($visitor['url']);
$a->setContactId($visitor['id']);
Logger::info('Authenticated visitor', ['url' => $visitor['url']]);
return $visitor;
}
/**
* Set the visitor cookies (see remote_user()) for signed HTTP requests
*
* @param array $server The content of the $_SERVER superglobal
* @return array Visitor contact array
* @throws InternalServerErrorException
*/
public static function addVisitorCookieForHTTPSigner(array $server): array
{
$requester = HTTPSignature::getSigner('', $server);
if (empty($requester)) {
return [];
}
return Profile::addVisitorCookieForHandle($requester);
}
/**
* OpenWebAuth authentication.
*
* Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/zid.php
*
* @param string $token
*
* @return void
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function openWebAuthInit(string $token)
{
$a = DI::app();
// Clean old OpenWebAuthToken entries.
OpenWebAuthToken::purge('owt', '3 MINUTE');
// Check if the token we got is the same one
// we have stored in the database.
$visitor_handle = OpenWebAuthToken::getMeta('owt', 0, $token);
if ($visitor_handle === false) {
return;
}
$visitor = self::addVisitorCookieForHandle($visitor_handle);
if (empty($visitor)) {
return;
}
$arr = [
'visitor' => $visitor,
'url' => DI::args()->getQueryString()
];
/**
* @hooks magic_auth_success
* Called when a magic-auth was successful.
* * \e array \b visitor
* * \e string \b url
*/
Hook::callAll('magic_auth_success', $arr);
$a->setContactId($arr['visitor']['id']);
DI::sysmsg()->addInfo(DI::l10n()->t('OpenWebAuth: %1$s welcomes %2$s', DI::baseUrl()->getHost(), $visitor['name']));
Logger::info('OpenWebAuth: auth success from ' . $visitor['addr']);
}
/**
* Returns URL with URL-encoded zrl parameter
*
* @param string $url URL to enhance
* @param bool $force Either to force adding zrl parameter
*
* @return string URL with 'zrl' parameter or original URL in case of no Friendica profile URL
*/
public static function zrl(string $url, bool $force = false): string
{
if (!strlen($url)) {
return $url;
}
if (!strpos($url, '/profile/') && !$force) {
return $url;
}
if ($force && substr($url, -1, 1) !== '/') {
$url = $url . '/';
}
$achar = strpos($url, '?') ? '&' : '?';
$mine = self::getMyURL();
if ($mine && !Strings::compareLink($mine, $url)) {
return $url . $achar . 'zrl=' . urlencode($mine);
}
return $url;
}
/** /**
* Get the user ID of the page owner. * Get the user ID of the page owner.
* *
@ -959,7 +728,8 @@ class Profile
if (!empty($search)) { if (!empty($search)) {
$publish = (DI::config()->get('system', 'publish_all') ? '' : "AND `publish` "); $publish = (DI::config()->get('system', 'publish_all') ? '' : "AND `publish` ");
$searchTerm = '%' . $search . '%'; $searchTerm = '%' . $search . '%';
$condition = ["`verified` AND NOT `blocked` AND NOT `account_removed` AND NOT `account_expired` $condition = [
"`verified` AND NOT `blocked` AND NOT `account_removed` AND NOT `account_expired`
$publish $publish
AND ((`name` LIKE ?) OR AND ((`name` LIKE ?) OR
(`nickname` LIKE ?) OR (`nickname` LIKE ?) OR
@ -970,7 +740,8 @@ class Profile
(`pub_keywords` LIKE ?) OR (`pub_keywords` LIKE ?) OR
(`prv_keywords` LIKE ?))", (`prv_keywords` LIKE ?))",
$searchTerm, $searchTerm, $searchTerm, $searchTerm, $searchTerm, $searchTerm, $searchTerm, $searchTerm,
$searchTerm, $searchTerm, $searchTerm, $searchTerm]; $searchTerm, $searchTerm, $searchTerm, $searchTerm
];
} else { } else {
$condition = ['verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => false]; $condition = ['verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => false];
if (!DI::config()->get('system', 'publish_all')) { if (!DI::config()->get('system', 'publish_all')) {
@ -1073,4 +844,44 @@ class Profile
DBA::delete('profile', ['id' => $profile['id']]); DBA::delete('profile', ['id' => $profile['id']]);
} }
} }
/**
* Get "about" field with the added responsible relay contact if appropriate.
*
* @param string $about
* @param integer|null $parent_uid
* @param integer $account_type
* @param string $language
* @return string
*/
public static function addResponsibleRelayContact(string $about = null, int $parent_uid = null, int $account_type, string $language): ?string
{
if (($account_type != User::ACCOUNT_TYPE_RELAY) || empty($parent_uid)) {
return $about;
}
$parent = User::getOwnerDataById($parent_uid);
if (strpos($about, $parent['addr']) || strpos($about, $parent['url'])) {
return $about;
}
$l10n = DI::l10n()->withLang($language);
return $about .= "\n" . $l10n->t('Responsible account: %s', $parent['addr']);
}
/**
* Set "about" field with the added responsible relay contact if appropriate.
*
* @param integer $uid
* @return void
*/
public static function setResponsibleRelayContact(int $uid)
{
$owner = User::getOwnerDataById($uid);
$about = self::addResponsibleRelayContact($owner['about'], $owner['parent-uid'], $owner['account-type'], $owner['language']);
if ($about != $owner['about']) {
self::update(['about' => $about], $uid);
}
}
} }

View file

@ -25,7 +25,7 @@ use Friendica\Core\Logger;
use Friendica\Core\Worker; use Friendica\Core\Worker;
use Friendica\Database\DBA; use Friendica\Database\DBA;
use Friendica\Util\DateTimeFormat; use Friendica\Util\DateTimeFormat;
use Friendica\Util\Network; use GuzzleHttp\Psr7\Uri;
class PushSubscriber class PushSubscriber
{ {
@ -179,7 +179,7 @@ class PushSubscriber
$parts = parse_url($subscriber['callback_url']); $parts = parse_url($subscriber['callback_url']);
unset($parts['path']); unset($parts['path']);
$server_url = Network::unparseURL($parts); $server_url = (string)Uri::fromParts((array)$parts);
$gsid = GServer::getID($server_url, true); $gsid = GServer::getID($server_url, true);
if (!empty($gsid)) { if (!empty($gsid)) {
GServer::setProtocol($gsid, Post\DeliveryData::OSTATUS); GServer::setProtocol($gsid, Post\DeliveryData::OSTATUS);

View file

@ -25,6 +25,7 @@ use DivineOmega\DOFileCachePSR6\CacheItemPool;
use DivineOmega\PasswordExposed; use DivineOmega\PasswordExposed;
use ErrorException; use ErrorException;
use Exception; use Exception;
use Friendica\App;
use Friendica\Content\Pager; use Friendica\Content\Pager;
use Friendica\Core\Hook; use Friendica\Core\Hook;
use Friendica\Core\L10n; use Friendica\Core\L10n;
@ -37,6 +38,8 @@ use Friendica\Database\DBA;
use Friendica\DI; use Friendica\DI;
use Friendica\Module; use Friendica\Module;
use Friendica\Network\HTTPClient\Client\HttpClientAccept; use Friendica\Network\HTTPClient\Client\HttpClientAccept;
use Friendica\Network\HTTPClient\Client\HttpClientOptions;
use Friendica\Network\HTTPClient\Client\HttpClientRequest;
use Friendica\Network\HTTPException; use Friendica\Network\HTTPException;
use Friendica\Object\Image; use Friendica\Object\Image;
use Friendica\Protocol\Delivery; use Friendica\Protocol\Delivery;
@ -72,6 +75,7 @@ class User
const PAGE_FLAGS_FREELOVE = 3; const PAGE_FLAGS_FREELOVE = 3;
const PAGE_FLAGS_BLOG = 4; const PAGE_FLAGS_BLOG = 4;
const PAGE_FLAGS_PRVGROUP = 5; const PAGE_FLAGS_PRVGROUP = 5;
const PAGE_FLAGS_COMM_MAN = 6;
/** /**
* @} * @}
*/ */
@ -161,6 +165,7 @@ class User
} }
} }
$system['name'] = App::PLATFORM . " '" . App::CODENAME . "' " . App::VERSION . '-' . DB_UPDATE_VERSION;
$system['sprvkey'] = $system['uprvkey'] = $system['prvkey']; $system['sprvkey'] = $system['uprvkey'] = $system['prvkey'];
$system['spubkey'] = $system['upubkey'] = $system['pubkey']; $system['spubkey'] = $system['upubkey'] = $system['pubkey'];
$system['nickname'] = $system['nick']; $system['nickname'] = $system['nick'];
@ -407,6 +412,29 @@ class User
return 0; return 0;
} }
/**
* Returns the user id of a given contact id
*
* @param int $cid
*
* @return integer user id
* @throws Exception
*/
public static function getIdForContactId(int $cid): int
{
$account = Contact::selectFirstAccountUser(['pid', 'self', 'uid'], ['id' => $cid]);
if (empty($account['pid'])) {
return 0;
}
if ($account['self']) {
return $account['uid'];
}
$self = Contact::selectFirstAccountUser(['uid'], ['pid' => $cid, 'self' => true]);
return $self['uid'] ?? 0;
}
/** /**
* Get a user based on its email * Get a user based on its email
* *
@ -1397,7 +1425,7 @@ class User
$photo_failure = false; $photo_failure = false;
$filename = basename($photo); $filename = basename($photo);
$curlResult = DI::httpClient()->get($photo, HttpClientAccept::IMAGE); $curlResult = DI::httpClient()->get($photo, HttpClientAccept::IMAGE, [HttpClientOptions::REQUEST => HttpClientRequest::CONTENTTYPE]);
if ($curlResult->isSuccess()) { if ($curlResult->isSuccess()) {
Logger::debug('Got picture', ['Content-Type' => $curlResult->getHeader('Content-Type'), 'url' => $photo]); Logger::debug('Got picture', ['Content-Type' => $curlResult->getHeader('Content-Type'), 'url' => $photo]);
$img_str = $curlResult->getBodyString(); $img_str = $curlResult->getBodyString();
@ -1487,6 +1515,12 @@ class User
*/ */
public static function block(int $uid, bool $block = true): bool public static function block(int $uid, bool $block = true): bool
{ {
$self = Contact::getPublicIdByUserId($uid);
if ($block) {
Contact::block($self);
} else {
Contact::unblock($self);
}
return DBA::update('user', ['blocked' => $block], ['uid' => $uid]); return DBA::update('user', ['blocked' => $block], ['uid' => $uid]);
} }

View file

@ -22,9 +22,11 @@
namespace Friendica\Module\ActivityPub; namespace Friendica\Module\ActivityPub;
use Friendica\Core\Logger; use Friendica\Core\Logger;
use Friendica\Core\Protocol;
use Friendica\Core\System; use Friendica\Core\System;
use Friendica\Database\DBA; use Friendica\Database\DBA;
use Friendica\DI; use Friendica\DI;
use Friendica\Model\Item;
use Friendica\Model\User; use Friendica\Model\User;
use Friendica\Module\BaseApi; use Friendica\Module\BaseApi;
use Friendica\Module\Special\HTTPException; use Friendica\Module\Special\HTTPException;
@ -77,6 +79,11 @@ class Inbox extends BaseApi
throw new \Friendica\Network\HTTPException\BadRequestException(); throw new \Friendica\Network\HTTPException\BadRequestException();
} }
if (!HTTPSignature::isValidContentType($this->server['CONTENT_TYPE'] ?? '')) {
Logger::notice('Unexpected content type', ['content-type' => $this->server['CONTENT_TYPE'] ?? '', 'agent' => $this->server['HTTP_USER_AGENT'] ?? '']);
throw new \Friendica\Network\HTTPException\UnsupportedMediaTypeException();
}
if (DI::config()->get('debug', 'ap_inbox_log')) { if (DI::config()->get('debug', 'ap_inbox_log')) {
if (HTTPSignature::getSigner($postdata, $_SERVER)) { if (HTTPSignature::getSigner($postdata, $_SERVER)) {
$filename = 'signed-activitypub'; $filename = 'signed-activitypub';
@ -98,6 +105,7 @@ class Inbox extends BaseApi
$uid = 0; $uid = 0;
} }
Item::incrementInbound(Protocol::ACTIVITYPUB);
ActivityPub\Receiver::processInbox($postdata, $_SERVER, $uid); ActivityPub\Receiver::processInbox($postdata, $_SERVER, $uid);
throw new \Friendica\Network\HTTPException\AcceptedException(); throw new \Friendica\Network\HTTPException\AcceptedException();

View file

@ -51,7 +51,7 @@ class Whoami extends BaseApi
$data['name'] = $owner['name']; $data['name'] = $owner['name'];
$data['preferredUsername'] = $owner['nick']; $data['preferredUsername'] = $owner['nick'];
$data['alsoKnownAs'] = []; $data['alsoKnownAs'] = [];
$data['manuallyApprovesFollowers'] = in_array($owner['page-flags'], [User::PAGE_FLAGS_NORMAL, User::PAGE_FLAGS_PRVGROUP]); $data['manuallyApprovesFollowers'] = in_array($owner['page-flags'], [User::PAGE_FLAGS_NORMAL, User::PAGE_FLAGS_PRVGROUP, User::PAGE_FLAGS_COMM_MAN]);
$data['discoverable'] = (bool)$owner['net-publish']; $data['discoverable'] = (bool)$owner['net-publish'];
$data['tag'] = []; $data['tag'] = [];

View file

@ -28,6 +28,12 @@ use Friendica\Module\BaseAdmin;
class Index extends BaseAdmin class Index extends BaseAdmin
{ {
protected function post(array $request = [])
{
// @todo check if POST is really used here
$this->content($request);
}
protected function content(array $request = []): string protected function content(array $request = []): string
{ {
parent::content(); parent::content();

View file

@ -34,25 +34,24 @@ class Features extends BaseAdmin
self::checkFormSecurityTokenRedirectOnError('/admin/features', 'admin_manage_features'); self::checkFormSecurityTokenRedirectOnError('/admin/features', 'admin_manage_features');
$features = Feature::get(false); foreach (Feature::get(false) as $fdata) {
foreach ($features as $fname => $fdata) {
foreach (array_slice($fdata, 1) as $f) { foreach (array_slice($fdata, 1) as $f) {
$feature = $f[0]; $feature = $f[0];
$feature_state = 'feature_' . $feature; switch ($_POST['featureselect_' . $feature]) {
$featurelock = 'featurelock_' . $feature; case 0:
DI::config()->set('feature', $feature, false);
if (!empty($_POST[$feature_state])) {
$val = intval($_POST[$feature_state]);
} else {
$val = 0;
}
DI::config()->set('feature', $feature, $val);
if (!empty($_POST[$featurelock])) {
DI::config()->set('feature_lock', $feature, 1);
} else {
DI::config()->delete('feature_lock', $feature); DI::config()->delete('feature_lock', $feature);
break;
case 1:
DI::config()->set('feature', $feature, true);
DI::config()->delete('feature_lock', $feature);
break;
case 2:
DI::config()->delete('feature', $feature);
DI::config()->set('feature_lock', $feature, true);
break;
} }
} }
} }
@ -65,16 +64,14 @@ class Features extends BaseAdmin
parent::content(); parent::content();
$features = []; $features = [];
$selection = [DI::l10n()->t('No'), DI::l10n()->t('Yes'), DI::l10n()->t('Locked')];
foreach (Feature::get(false) as $fname => $fdata) { foreach (Feature::get(false) as $fname => $fdata) {
$features[$fname] = []; $features[$fname] = [];
$features[$fname][0] = $fdata[0]; $features[$fname][0] = $fdata[0];
foreach (array_slice($fdata, 1) as $f) { foreach (array_slice($fdata, 1) as $f) {
$set = DI::config()->get('feature', $f[0], $f[3]); $set = DI::config()->get('feature', $f[0], $f[3]);
$features[$fname][1][] = [ $selected = $f[4] ? 2 : (int)$set;
['feature_' . $f[0], $f[1], $set, $f[2]], $features[$fname][1][] = ['featureselect_' . $f[0], $f[1], $selected, $f[2], $selection];
['featurelock_' . $f[0], DI::l10n()->t('Lock feature %s', $f[1]), $f[4], '']
];
} }
} }

View file

@ -221,7 +221,6 @@ class Federation extends BaseAdmin
'$page' => DI::l10n()->t('Federation Statistics'), '$page' => DI::l10n()->t('Federation Statistics'),
'$intro' => $intro, '$intro' => $intro,
'$counts' => $counts, '$counts' => $counts,
'$version' => App::VERSION,
'$legendtext' => DI::l10n()->tt('Currently this node is aware of %2$s node (%3$s active users last month, %4$s active users last six months, %5$s registered users in total) from the following platforms:', 'Currently this node is aware of %2$s nodes (%3$s active users last month, %4$s active users last six months, %5$s registered users in total) from the following platforms:', $total, number_format($total), number_format($month), number_format($halfyear), number_format($users)), '$legendtext' => DI::l10n()->tt('Currently this node is aware of %2$s node (%3$s active users last month, %4$s active users last six months, %5$s registered users in total) from the following platforms:', 'Currently this node is aware of %2$s nodes (%3$s active users last month, %4$s active users last six months, %5$s registered users in total) from the following platforms:', $total, number_format($total), number_format($month), number_format($halfyear), number_format($users)),
]); ]);
} }

View file

@ -66,6 +66,9 @@ class Settings extends BaseAdmin
parent::content(); parent::content();
$log_choices = [ $log_choices = [
LogLevel::EMERGENCY => 'Emergency',
LogLevel::ALERT => 'Alert',
LogLevel::CRITICAL => 'Critical',
LogLevel::ERROR => 'Error', LogLevel::ERROR => 'Error',
LogLevel::WARNING => 'Warning', LogLevel::WARNING => 'Warning',
LogLevel::NOTICE => 'Notice', LogLevel::NOTICE => 'Notice',

View file

@ -47,6 +47,8 @@ class View extends BaseAdmin
$filters_valid_values = [ $filters_valid_values = [
'level' => [ 'level' => [
'', '',
LogLevel::EMERGENCY,
LogLevel::ALERT,
LogLevel::CRITICAL, LogLevel::CRITICAL,
LogLevel::ERROR, LogLevel::ERROR,
LogLevel::WARNING, LogLevel::WARNING,
@ -54,7 +56,7 @@ class View extends BaseAdmin
LogLevel::INFO, LogLevel::INFO,
LogLevel::DEBUG, LogLevel::DEBUG,
], ],
'context' => ['', 'index', 'worker'], 'context' => ['', 'index', 'worker', 'daemon'],
]; ];
$filters = [ $filters = [
'level' => $_GET['level'] ?? '', 'level' => $_GET['level'] ?? '',

View file

@ -56,13 +56,14 @@ class Queue extends BaseAdmin
} }
// @TODO Move to Model\WorkerQueue::getEntries() // @TODO Move to Model\WorkerQueue::getEntries()
$entries = DBA::select('workerqueue', ['id', 'parameter', 'created', 'priority', 'command'], $condition, ['limit' => 999, 'order' => ['created']]); $entries = DBA::select('workerqueue', ['id', 'parameter', 'created', 'next_try', 'priority', 'command'], $condition, ['limit' => 999, 'order' => ['created']]);
$r = []; $r = [];
while ($entry = DBA::fetch($entries)) { while ($entry = DBA::fetch($entries)) {
// fix GH-5469. ref: src/Core/Worker.php:217 // fix GH-5469. ref: src/Core/Worker.php:217
$entry['parameter'] = Arrays::recursiveImplode(json_decode($entry['parameter'], true), ': '); $entry['parameter'] = Arrays::recursiveImplode(json_decode($entry['parameter'], true), ': ');
$entry['created'] = DateTimeFormat::local($entry['created']); $entry['created'] = DateTimeFormat::local($entry['created']);
$entry['next_try'] = DateTimeFormat::local($entry['next_try']);
$r[] = $entry; $r[] = $entry;
} }
DBA::close($entries); DBA::close($entries);
@ -76,8 +77,10 @@ class Queue extends BaseAdmin
'$command_header' => DI::l10n()->t('Command'), '$command_header' => DI::l10n()->t('Command'),
'$param_header' => DI::l10n()->t('Job Parameters'), '$param_header' => DI::l10n()->t('Job Parameters'),
'$created_header' => DI::l10n()->t('Created'), '$created_header' => DI::l10n()->t('Created'),
'$next_try_header' => DI::l10n()->t('Next Try'),
'$prio_header' => DI::l10n()->t('Priority'), '$prio_header' => DI::l10n()->t('Priority'),
'$info' => $info, '$info' => $info,
'$status' => $status,
'$entries' => $r, '$entries' => $r,
]); ]);
} }

View file

@ -97,7 +97,6 @@ class Site extends BaseAdmin
$adjust_poll_frequency = !empty($_POST['adjust_poll_frequency']); $adjust_poll_frequency = !empty($_POST['adjust_poll_frequency']);
$min_poll_interval = (!empty($_POST['min_poll_interval']) ? intval(trim($_POST['min_poll_interval'])) : 0); $min_poll_interval = (!empty($_POST['min_poll_interval']) ? intval(trim($_POST['min_poll_interval'])) : 0);
$explicit_content = !empty($_POST['explicit_content']); $explicit_content = !empty($_POST['explicit_content']);
$proxify_content = !empty($_POST['proxify_content']);
$local_search = !empty($_POST['local_search']); $local_search = !empty($_POST['local_search']);
$blocked_tags = (!empty($_POST['blocked_tags']) ? trim($_POST['blocked_tags']) : ''); $blocked_tags = (!empty($_POST['blocked_tags']) ? trim($_POST['blocked_tags']) : '');
$cache_contact_avatar = !empty($_POST['cache_contact_avatar']); $cache_contact_avatar = !empty($_POST['cache_contact_avatar']);
@ -141,6 +140,7 @@ class Site extends BaseAdmin
$temppath = (!empty($_POST['temppath']) ? trim($_POST['temppath']) : ''); $temppath = (!empty($_POST['temppath']) ? trim($_POST['temppath']) : '');
$singleuser = (!empty($_POST['singleuser']) ? trim($_POST['singleuser']) : ''); $singleuser = (!empty($_POST['singleuser']) ? trim($_POST['singleuser']) : '');
$only_tag_search = !empty($_POST['only_tag_search']); $only_tag_search = !empty($_POST['only_tag_search']);
$limited_search_scope = !empty($_POST['limited_search_scope']);
$search_age_days = (!empty($_POST['search_age_days']) ? intval($_POST['search_age_days']) : 0); $search_age_days = (!empty($_POST['search_age_days']) ? intval($_POST['search_age_days']) : 0);
$compute_circle_counts = !empty($_POST['compute_circle_counts']); $compute_circle_counts = !empty($_POST['compute_circle_counts']);
$process_view = !empty($_POST['process_view']); $process_view = !empty($_POST['process_view']);
@ -271,7 +271,6 @@ class Site extends BaseAdmin
$transactionConfig->set('system', 'adjust_poll_frequency' , $adjust_poll_frequency); $transactionConfig->set('system', 'adjust_poll_frequency' , $adjust_poll_frequency);
$transactionConfig->set('system', 'min_poll_interval' , $min_poll_interval); $transactionConfig->set('system', 'min_poll_interval' , $min_poll_interval);
$transactionConfig->set('system', 'explicit_content' , $explicit_content); $transactionConfig->set('system', 'explicit_content' , $explicit_content);
$transactionConfig->set('system', 'proxify_content' , $proxify_content);
$transactionConfig->set('system', 'local_search' , $local_search); $transactionConfig->set('system', 'local_search' , $local_search);
$transactionConfig->set('system', 'blocked_tags' , Strings::cleanTags($blocked_tags)); $transactionConfig->set('system', 'blocked_tags' , Strings::cleanTags($blocked_tags));
$transactionConfig->set('system', 'cache_contact_avatar' , $cache_contact_avatar); $transactionConfig->set('system', 'cache_contact_avatar' , $cache_contact_avatar);
@ -319,6 +318,7 @@ class Site extends BaseAdmin
$transactionConfig->set('system', 'temppath', $temppath); $transactionConfig->set('system', 'temppath', $temppath);
$transactionConfig->set('system', 'only_tag_search', $only_tag_search); $transactionConfig->set('system', 'only_tag_search', $only_tag_search);
$transactionConfig->set('system', 'limited_search_scope', $limited_search_scope);
$transactionConfig->set('system', 'search_age_days', $search_age_days); $transactionConfig->set('system', 'search_age_days', $search_age_days);
$transactionConfig->set('system', 'compute_circle_counts', $compute_circle_counts); $transactionConfig->set('system', 'compute_circle_counts', $compute_circle_counts);
$transactionConfig->set('system', 'process_view', $process_view); $transactionConfig->set('system', 'process_view', $process_view);
@ -518,7 +518,6 @@ class Site extends BaseAdmin
'$private_addons' => ['private_addons', DI::l10n()->t('Disallow public access to addons listed in the apps menu.'), DI::config()->get('config', 'private_addons'), DI::l10n()->t('Checking this box will restrict addons listed in the apps menu to members only.')], '$private_addons' => ['private_addons', DI::l10n()->t('Disallow public access to addons listed in the apps menu.'), DI::config()->get('config', 'private_addons'), DI::l10n()->t('Checking this box will restrict addons listed in the apps menu to members only.')],
'$disable_embedded' => ['disable_embedded', DI::l10n()->t('Don\'t embed private images in posts'), DI::config()->get('system', 'disable_embedded'), DI::l10n()->t('Don\'t replace locally-hosted private photos in posts with an embedded copy of the image. This means that contacts who receive posts containing private photos will have to authenticate and load each image, which may take a while.')], '$disable_embedded' => ['disable_embedded', DI::l10n()->t('Don\'t embed private images in posts'), DI::config()->get('system', 'disable_embedded'), DI::l10n()->t('Don\'t replace locally-hosted private photos in posts with an embedded copy of the image. This means that contacts who receive posts containing private photos will have to authenticate and load each image, which may take a while.')],
'$explicit_content' => ['explicit_content', DI::l10n()->t('Explicit Content'), DI::config()->get('system', 'explicit_content'), DI::l10n()->t('Set this to announce that your node is used mostly for explicit content that might not be suited for minors. This information will be published in the node information and might be used, e.g. by the global directory, to filter your node from listings of nodes to join. Additionally a note about this will be shown at the user registration page.')], '$explicit_content' => ['explicit_content', DI::l10n()->t('Explicit Content'), DI::config()->get('system', 'explicit_content'), DI::l10n()->t('Set this to announce that your node is used mostly for explicit content that might not be suited for minors. This information will be published in the node information and might be used, e.g. by the global directory, to filter your node from listings of nodes to join. Additionally a note about this will be shown at the user registration page.')],
'$proxify_content' => ['proxify_content', DI::l10n()->t('Proxify external content'), DI::config()->get('system', 'proxify_content'), DI::l10n()->t('Route external content via the proxy functionality. This is used for example for some OEmbed accesses and in some other rare cases.')],
'$local_search' => ['local_search', DI::l10n()->t('Only local search'), DI::config()->get('system', 'local_search'), DI::l10n()->t('Blocks search for users who are not logged in to prevent crawlers from blocking your system.')], '$local_search' => ['local_search', DI::l10n()->t('Only local search'), DI::config()->get('system', 'local_search'), DI::l10n()->t('Blocks search for users who are not logged in to prevent crawlers from blocking your system.')],
'$blocked_tags' => ['blocked_tags', DI::l10n()->t('Blocked tags for trending tags'), DI::config()->get('system', 'blocked_tags'), DI::l10n()->t("Comma separated list of hashtags that shouldn't be displayed in the trending tags.")], '$blocked_tags' => ['blocked_tags', DI::l10n()->t('Blocked tags for trending tags'), DI::config()->get('system', 'blocked_tags'), DI::l10n()->t("Comma separated list of hashtags that shouldn't be displayed in the trending tags.")],
'$cache_contact_avatar' => ['cache_contact_avatar', DI::l10n()->t('Cache contact avatars'), DI::config()->get('system', 'cache_contact_avatar'), DI::l10n()->t('Locally store the avatar pictures of the contacts. This uses a lot of storage space but it increases the performance.')], '$cache_contact_avatar' => ['cache_contact_avatar', DI::l10n()->t('Cache contact avatars'), DI::config()->get('system', 'cache_contact_avatar'), DI::l10n()->t('Locally store the avatar pictures of the contacts. This uses a lot of storage space but it increases the performance.')],
@ -574,6 +573,7 @@ class Site extends BaseAdmin
'$itemspage_network_mobile' => ['itemspage_network_mobile', DI::l10n()->t('Items per page for mobile devices'), DI::config()->get('system', 'itemspage_network_mobile'), DI::l10n()->t('Number of items per page in stream pages (network, community, profile/contact statuses, search) for mobile devices.')], '$itemspage_network_mobile' => ['itemspage_network_mobile', DI::l10n()->t('Items per page for mobile devices'), DI::config()->get('system', 'itemspage_network_mobile'), DI::l10n()->t('Number of items per page in stream pages (network, community, profile/contact statuses, search) for mobile devices.')],
'$temppath' => ['temppath', DI::l10n()->t('Temp path'), DI::config()->get('system', 'temppath'), DI::l10n()->t('If you have a restricted system where the webserver can\'t access the system temp path, enter another path here.')], '$temppath' => ['temppath', DI::l10n()->t('Temp path'), DI::config()->get('system', 'temppath'), DI::l10n()->t('If you have a restricted system where the webserver can\'t access the system temp path, enter another path here.')],
'$only_tag_search' => ['only_tag_search', DI::l10n()->t('Only search in tags'), DI::config()->get('system', 'only_tag_search'), DI::l10n()->t('On large systems the text search can slow down the system extremely.')], '$only_tag_search' => ['only_tag_search', DI::l10n()->t('Only search in tags'), DI::config()->get('system', 'only_tag_search'), DI::l10n()->t('On large systems the text search can slow down the system extremely.')],
'$limited_search_scope' => ['limited_search_scope', DI::l10n()->t('Limited search scope'), DI::config()->get('system', 'limited_search_scope'), DI::l10n()->t('If enabled, searches will only be performed in the data used for the channels and not in all posts.')],
'$search_age_days' => ['search_age_days', DI::l10n()->t('Maximum age of items in the search table'), DI::config()->get('system', 'search_age_days'), DI::l10n()->t('Maximum age of items in the search table in days. Lower values will increase the performance and reduce disk usage. 0 means no age restriction.')], '$search_age_days' => ['search_age_days', DI::l10n()->t('Maximum age of items in the search table'), DI::config()->get('system', 'search_age_days'), DI::l10n()->t('Maximum age of items in the search table in days. Lower values will increase the performance and reduce disk usage. 0 means no age restriction.')],
'$compute_circle_counts' => ['compute_circle_counts', DI::l10n()->t('Generate counts per contact circle when calculating network count'), DI::config()->get('system', 'compute_circle_counts'), DI::l10n()->t('On systems with users that heavily use contact circles the query can be very expensive.')], '$compute_circle_counts' => ['compute_circle_counts', DI::l10n()->t('Generate counts per contact circle when calculating network count'), DI::config()->get('system', 'compute_circle_counts'), DI::l10n()->t('On systems with users that heavily use contact circles the query can be very expensive.')],
'$process_view' => ['process_view', DI::l10n()->t('Process "view" activities'), DI::config()->get('system', 'process_view'), DI::l10n()->t('"view" activities are mostly geberated by Peertube systems. Per default they are not processed for performance reasons. Only activate this option on performant system.')], '$process_view' => ['process_view', DI::l10n()->t('Process "view" activities'), DI::config()->get('system', 'process_view'), DI::l10n()->t('"view" activities are mostly geberated by Peertube systems. Per default they are not processed for performance reasons. Only activate this option on performant system.')],

View file

@ -196,7 +196,7 @@ class Summary extends BaseAdmin
'$title' => DI::l10n()->t('Administration'), '$title' => DI::l10n()->t('Administration'),
'$page' => DI::l10n()->t('Summary'), '$page' => DI::l10n()->t('Summary'),
'$queues' => $queues, '$queues' => $queues,
'$version' => [DI::l10n()->t('Version'), App::VERSION], '$version_label' => DI::l10n()->t('Version'),
'$platform' => App::PLATFORM, '$platform' => App::PLATFORM,
'$codename' => App::CODENAME, '$codename' => App::CODENAME,
'$build' => DI::config()->get('system', 'build'), '$build' => DI::config()->get('system', 'build'),

View file

@ -30,6 +30,12 @@ use Friendica\Util\Strings;
class Details extends BaseAdmin class Details extends BaseAdmin
{ {
protected function post(array $request = [])
{
// @todo check if POST is really used here
$this->content($request);
}
protected function content(array $request = []): string protected function content(array $request = []): string
{ {
parent::content(); parent::content();

Some files were not shown because too many files have changed in this diff Show more