Merge remote-tracking branch 'upstream/2023.09-rc' into smilies

This commit is contained in:
Michael 2023-10-12 20:49:20 +00:00
commit 19529e2aa1
417 changed files with 48140 additions and 31688 deletions

View file

@ -1 +1 @@
2023.09-dev 2023.09-rc

View file

@ -1,6 +1,6 @@
-- ------------------------------------------ -- ------------------------------------------
-- Friendica 2023.09-dev (Giant Rhubarb) -- Friendica 2023.09-rc (Giant Rhubarb)
-- DB_UPDATE_VERSION 1529 -- DB_UPDATE_VERSION 1536
-- ------------------------------------------ -- ------------------------------------------
@ -492,6 +492,25 @@ CREATE TABLE IF NOT EXISTS `cache` (
INDEX `k_expires` (`k`,`expires`) INDEX `k_expires` (`k`,`expires`)
) DEFAULT COLLATE utf8mb4_general_ci COMMENT='Stores temporary data'; ) DEFAULT COLLATE utf8mb4_general_ci COMMENT='Stores temporary data';
--
-- TABLE channel
--
CREATE TABLE IF NOT EXISTS `channel` (
`id` int unsigned NOT NULL auto_increment COMMENT '',
`uid` mediumint unsigned NOT NULL COMMENT 'User id',
`label` varchar(64) NOT NULL COMMENT 'Channel label',
`description` varchar(64) COMMENT 'Channel description',
`circle` int COMMENT 'Circle or channel that this channel is based on',
`access-key` varchar(1) COMMENT 'Access key',
`include-tags` varchar(255) COMMENT 'Comma separated list of tags that will be included in the channel',
`exclude-tags` varchar(255) COMMENT 'Comma separated list of tags that aren\'t allowed in the channel',
`full-text-search` varchar(255) COMMENT 'Full text search pattern, see https://mariadb.com/kb/en/full-text-index-overview/#in-boolean-mode',
`media-type` smallint unsigned COMMENT 'Filtered media types',
PRIMARY KEY(`id`),
INDEX `uid` (`uid`),
FOREIGN KEY (`uid`) REFERENCES `user` (`uid`) ON UPDATE RESTRICT ON DELETE CASCADE
) DEFAULT COLLATE utf8mb4_general_ci COMMENT='User defined Channels';
-- --
-- TABLE config -- TABLE config
-- --
@ -510,9 +529,13 @@ CREATE TABLE IF NOT EXISTS `config` (
CREATE TABLE IF NOT EXISTS `contact-relation` ( CREATE TABLE IF NOT EXISTS `contact-relation` (
`cid` int unsigned NOT NULL DEFAULT 0 COMMENT 'contact the related contact had interacted with', `cid` int unsigned NOT NULL DEFAULT 0 COMMENT 'contact the related contact had interacted with',
`relation-cid` int unsigned NOT NULL DEFAULT 0 COMMENT 'related contact who had interacted with the contact', `relation-cid` int unsigned NOT NULL DEFAULT 0 COMMENT 'related contact who had interacted with the contact',
`last-interaction` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT 'Date of the last interaction', `last-interaction` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT 'Date of the last interaction by relation-cid on cid',
`follow-updated` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT 'Date of the last update of the contact relationship', `follow-updated` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT 'Date of the last update of the contact relationship',
`follows` boolean NOT NULL DEFAULT '0' COMMENT '', `follows` boolean NOT NULL DEFAULT '0' COMMENT 'if true, relation-cid follows cid',
`score` smallint unsigned COMMENT 'score for interactions of cid on relation-cid',
`relation-score` smallint unsigned COMMENT 'score for interactions of relation-cid on cid',
`thread-score` smallint unsigned COMMENT 'score for interactions of cid on threads of relation-cid',
`relation-thread-score` smallint unsigned COMMENT 'score for interactions of relation-cid on threads of cid',
PRIMARY KEY(`cid`,`relation-cid`), PRIMARY KEY(`cid`,`relation-cid`),
INDEX `relation-cid` (`relation-cid`), INDEX `relation-cid` (`relation-cid`),
FOREIGN KEY (`cid`) REFERENCES `contact` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE, FOREIGN KEY (`cid`) REFERENCES `contact` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE,
@ -1296,6 +1319,28 @@ CREATE TABLE IF NOT EXISTS `post-delivery-data` (
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
) DEFAULT COLLATE utf8mb4_general_ci COMMENT='Delivery data for items'; ) DEFAULT COLLATE utf8mb4_general_ci COMMENT='Delivery data for items';
--
-- TABLE post-engagement
--
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',
`owner-id` int unsigned NOT NULL DEFAULT 0 COMMENT 'Item owner',
`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',
`language` varbinary(128) COMMENT 'Language information about this post',
`searchtext` mediumtext COMMENT 'Simplified text for the full text search',
`created` datetime COMMENT '',
`restricted` boolean NOT NULL DEFAULT '0' COMMENT 'If true, this post is either unlisted or not from a federated network',
`comments` mediumint unsigned COMMENT 'Number of comments',
`activities` mediumint unsigned COMMENT 'Number of activities (like, dislike, ...)',
PRIMARY KEY(`uri-id`),
INDEX `owner-id` (`owner-id`),
INDEX `created` (`created`),
FULLTEXT INDEX `searchtext` (`searchtext`),
FOREIGN KEY (`uri-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE,
FOREIGN KEY (`owner-id`) REFERENCES `contact` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE
) DEFAULT COLLATE utf8mb4_general_ci COMMENT='Engagement data per post';
-- --
-- TABLE post-history -- TABLE post-history
-- --
@ -1549,6 +1594,7 @@ CREATE TABLE IF NOT EXISTS `post-thread-user` (
INDEX `psid` (`psid`), INDEX `psid` (`psid`),
INDEX `post-user-id` (`post-user-id`), INDEX `post-user-id` (`post-user-id`),
INDEX `commented` (`commented`), INDEX `commented` (`commented`),
INDEX `received` (`received`),
INDEX `uid_received` (`uid`,`received`), INDEX `uid_received` (`uid`,`received`),
INDEX `uid_wall_received` (`uid`,`wall`,`received`), INDEX `uid_wall_received` (`uid`,`wall`,`received`),
INDEX `uid_commented` (`uid`,`commented`), INDEX `uid_commented` (`uid`,`commented`),
@ -1710,15 +1756,15 @@ CREATE TABLE IF NOT EXISTS `report` (
`cid` int unsigned NOT NULL COMMENT 'Reported contact', `cid` int unsigned NOT NULL COMMENT 'Reported contact',
`gsid` int unsigned COMMENT 'Reported contact server', `gsid` int unsigned COMMENT 'Reported contact server',
`comment` text COMMENT 'Report', `comment` text COMMENT 'Report',
`category-id` int unsigned NOT NULL DEFAULT 1 COMMENT 'Report category, one of Entity\Report::CATEGORY_*', `category-id` int unsigned NOT NULL DEFAULT 1 COMMENT 'Report category, one of Entity Report::CATEGORY_*',
`forward` boolean COMMENT 'Forward the report to the remote server', `forward` boolean COMMENT 'Forward the report to the remote server',
`public-remarks` text COMMENT 'Remarks shared with the reporter', `public-remarks` text COMMENT 'Remarks shared with the reporter',
`private-remarks` text COMMENT 'Remarks shared with the moderation team', `private-remarks` text COMMENT 'Remarks shared with the moderation team',
`last-editor-uid` mediumint unsigned COMMENT 'Last editor user', `last-editor-uid` mediumint unsigned COMMENT 'Last editor user',
`assigned-uid` mediumint unsigned COMMENT 'Assigned moderator user', `assigned-uid` mediumint unsigned COMMENT 'Assigned moderator user',
`status` tinyint unsigned NOT NULL COMMENT 'Status of the report, one of Entity\Report::STATUS_*', `status` tinyint unsigned NOT NULL COMMENT 'Status of the report, one of Entity Report::STATUS_*',
`resolution` tinyint unsigned COMMENT 'Resolution of the report, one of Entity\Report::RESOLUTION_*', `resolution` tinyint unsigned COMMENT 'Resolution of the report, one of Entity Report::RESOLUTION_*',
`created` datetime(6) NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT '', `created` datetime(6) NOT NULL DEFAULT '0001-01-01 00:00:00.000000' COMMENT '',
`edited` datetime(6) COMMENT 'Last time the report has been edited', `edited` datetime(6) COMMENT 'Last time the report has been edited',
PRIMARY KEY(`id`), PRIMARY KEY(`id`),
INDEX `uid` (`uid`), INDEX `uid` (`uid`),
@ -1843,6 +1889,7 @@ CREATE TABLE IF NOT EXISTS `user-contact` (
`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',
`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',
`pending` boolean COMMENT '', `pending` boolean COMMENT '',
`rel` tinyint unsigned COMMENT 'The kind of the relation between the user and the contact', `rel` tinyint unsigned COMMENT 'The kind of the relation between the user and the contact',
`info` mediumtext COMMENT '', `info` mediumtext COMMENT '',
@ -2044,6 +2091,7 @@ CREATE VIEW `post-user-view` AS SELECT
`author`.`blocked` AS `author-blocked`, `author`.`blocked` AS `author-blocked`,
`author`.`hidden` AS `author-hidden`, `author`.`hidden` AS `author-hidden`,
`author`.`updated` AS `author-updated`, `author`.`updated` AS `author-updated`,
`author`.`contact-type` AS `author-contact-type`,
`author`.`gsid` AS `author-gsid`, `author`.`gsid` AS `author-gsid`,
`author`.`baseurl` AS `author-baseurl`, `author`.`baseurl` AS `author-baseurl`,
`post-user`.`owner-id` AS `owner-id`, `post-user`.`owner-id` AS `owner-id`,
@ -2228,6 +2276,7 @@ CREATE VIEW `post-thread-user-view` AS SELECT
`author`.`blocked` AS `author-blocked`, `author`.`blocked` AS `author-blocked`,
`author`.`hidden` AS `author-hidden`, `author`.`hidden` AS `author-hidden`,
`author`.`updated` AS `author-updated`, `author`.`updated` AS `author-updated`,
`author`.`contact-type` AS `author-contact-type`,
`author`.`gsid` AS `author-gsid`, `author`.`gsid` AS `author-gsid`,
`post-thread-user`.`owner-id` AS `owner-id`, `post-thread-user`.`owner-id` AS `owner-id`,
`owner`.`uri-id` AS `owner-uri-id`, `owner`.`uri-id` AS `owner-uri-id`,
@ -2396,6 +2445,7 @@ CREATE VIEW `post-view` AS SELECT
`author`.`blocked` AS `author-blocked`, `author`.`blocked` AS `author-blocked`,
`author`.`hidden` AS `author-hidden`, `author`.`hidden` AS `author-hidden`,
`author`.`updated` AS `author-updated`, `author`.`updated` AS `author-updated`,
`author`.`contact-type` AS `author-contact-type`,
`author`.`gsid` AS `author-gsid`, `author`.`gsid` AS `author-gsid`,
`post`.`owner-id` AS `owner-id`, `post`.`owner-id` AS `owner-id`,
`owner`.`uri-id` AS `owner-uri-id`, `owner`.`uri-id` AS `owner-uri-id`,
@ -2541,6 +2591,7 @@ CREATE VIEW `post-thread-view` AS SELECT
`author`.`blocked` AS `author-blocked`, `author`.`blocked` AS `author-blocked`,
`author`.`hidden` AS `author-hidden`, `author`.`hidden` AS `author-hidden`,
`author`.`updated` AS `author-updated`, `author`.`updated` AS `author-updated`,
`author`.`contact-type` AS `author-contact-type`,
`author`.`gsid` AS `author-gsid`, `author`.`gsid` AS `author-gsid`,
`post-thread`.`owner-id` AS `owner-id`, `post-thread`.`owner-id` AS `owner-id`,
`owner`.`uri-id` AS `owner-uri-id`, `owner`.`uri-id` AS `owner-uri-id`,

View file

@ -16,6 +16,7 @@ General
------- -------
* p - Profile * p - Profile
* n - Network * n - Network
* l - Channel
* c - Community * c - Community
* s - Search * s - Search
* a - Admin * a - Admin
@ -28,6 +29,18 @@ General
* l - Local community * l - Local community
* g - Global community * g - Global community
../channel
--------
* y - for you
* f - followers
* r - sharers of sharers
* h - what's hot
* i - Images
* v - Videos
* d - Audio
* g - Posts in your language
* o - Hot posts in your language
../profile ../profile
-------- --------
* m - Status Messages and Posts * m - Status Messages and Posts

View file

@ -221,6 +221,15 @@ Please note: body contents are bbcode - not HTML
Called when receiving a post from another source. This may also be used to post local activity or system generated messages. Called when receiving a post from another source. This may also be used to post local activity or system generated messages.
`$b` is the item array of information to be stored in the database and the item body is bbcode. `$b` is the item array of information to be stored in the database and the item body is bbcode.
### detect_languages
Called after the language detection. This can be used for alternative language detection methods.
`$data` is an array:
- **text**: The text that is analyzed.
- **detected**: (input/output) Array of language codes detected in the related text. The array key is the language code, the array value the probability.
- **uri-id**: The Uri-Id of the item.
- **author-id**: The id of the author contact.
### addon_settings ### addon_settings
Called when generating the HTML for the addon settings page. Called when generating the HTML for the addon settings page.
`$data` is an array containing: `$data` is an array containing:
@ -800,6 +809,7 @@ Here is a complete list of all hook callbacks with file locations (as of 24-Sep-
### src/Model/Item.php ### src/Model/Item.php
Hook::callAll('detect_languages', $item);
Hook::callAll('post_local', $item); Hook::callAll('post_local', $item);
Hook::callAll('post_remote', $item); Hook::callAll('post_remote', $item);
Hook::callAll('post_local_end', $posted_item); Hook::callAll('post_local_end', $posted_item);

77
doc/Channels.md Normal file
View file

@ -0,0 +1,77 @@
Channels
=====
* [Home](help)
Channels are a way to discover new content or to display content that you might have missed otherwise.
There are several predefined channels, additionally you can create your own channels, based on some rules.
Channels only display posts from the last 24 hours (this value can be changed by the admin).
In the display settings in the section "Timelines" you can define which channels and other timelines you want to see in the "Channels" widget on the network page and which channels should appear in the menu bar at the top of the page.
Also in the display settings in the section "Channels" you can define all the languages that you want to see in your channels. Here you can select more than one language.
On the contact page you can define the channel frequency for every contact. The options are:
* Default frequency: Posts by this contact are displayed in the "for you" channel if you interact often with this contact or if a post reached some level of interaction.
* Display all posts of this contact: All posts from this contact will appear on the "for you" channel.
* Display only few posts: When a contact creates a lot of posts in a short period, this setting reduces the number of displayed posts in every channel.
* Never display posts: Posts from this contact will never be displayed in any channel.
Predefined Channels
---
* 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 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.
* What's Hot: Posts with a more than average number of interactions.
* Language: Posts in your language.
* Followers: Posts from your followers that you don't follow.
* Sharers of sharers: Posts from accounts that are followed by accounts that you follow.
* Images: Posts with images.
* Audio: Posts with audio.
* Videos: Posts with videos.
User defined Channels
---
In the "Channels" settings you can create your own channels.
Each channel is defined by these values:
* Label: This value is mandatory and is used for the menu label.
* Description: A short description of the content. This can help to keep the overview, when you have got a lot of channels.
* 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.
* 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.
* 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.
Additional keywords for the full text search
---
Additionally to the search for content, there are additional keywords that can be used in the full text search:
* 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.
* group - Use "from:nickname" or "from:nickname@domain.tld" to search for group post of the given group.
* tag - Use "tag:tagname" to search for a specific tag.
* network - Use this to include or exclude some networks from your channel.
* network:apub - ActivityPub (Used by the systems in the Fediverse)
* network:dfrn - Legacy Friendica protocol. Nowayday Friendica mostly uses ActivityPub.
* network:dspr - The Diaspora protocol is mainly used by Diaspora itself. Some other systems support the protocol as well like Hubzilla, Socialhome or Ganggo.
* network:feed - RSS/Atom feeds
* network:mail - Mails that had been imported via IMAP.
* network:stat - The OStatus protocol is mainly used by old GNU Social installations.
* network:dscs - Posts that are received by the Discourse connector.
* network:tmbl - Posts that are received by the Tumblr connector.
* network:bsky - Posts that are received by the Bluesky connector.
* visibility - You have the choice between different visibilities. You can only see unlisted or private posts that you have the access for.
* visibility:public
* visibility:unlisted
* visibility:private
Remember that you can combine these kerywords.
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

@ -42,7 +42,7 @@ The listed emails need to be separated by a comma like this:
'admin_email' => 'mail1@example.com,mail2@example.com', 'admin_email' => 'mail1@example.com,mail2@example.com',
``` ```
<a name="dbupdate"> <a name="dbupdate"></a>
### The Database structure seems not to be updated. What can I do? ### The Database structure seems not to be updated. What can I do?
Please have a look at the Admin panel under [DB updates](/admin/dbsync/) and follow the link to *check database structure*. Please have a look at the Admin panel under [DB updates](/admin/dbsync/) and follow the link to *check database structure*.
@ -52,4 +52,4 @@ You can manually execute the structure update from the CLI in the base directory
bin/console dbstructure update bin/console dbstructure update
if there occur any errors, please contact the [support group](https://forum.friendi.ca/profile/helpers). if there occur any errors, please contact the [Friendica Support group](https://forum.friendi.ca/profile/helpers) or discuss in the [Friendica Admins group](https://forum.friendi.ca/profile/admins).

View file

@ -17,6 +17,7 @@ Friendica Documentation and Resources
* [Circles and Privacy](help/Circles-and-Privacy) * [Circles and Privacy](help/Circles-and-Privacy)
* [Tags and Mentions](help/Tags-and-Mentions) * [Tags and Mentions](help/Tags-and-Mentions)
* [Community Groups](help/Groups) * [Community Groups](help/Groups)
* [Channels](help/Channels)
* [Chats](help/Chats) * [Chats](help/Chats)
* Further information * Further information
* [Move your account](help/Move-Account) * [Move your account](help/Move-Account)

View file

@ -28,9 +28,9 @@ Due to the large variety of operating systems and PHP platforms in existence we
### Requirements ### Requirements
* 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.3+ (PHP8 is not fully supported yet) * PHP 7.3+
* 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 and OpenSSL extensions * Curl, GD, GMP, PDO, mbstrings, MySQLi, hash, xml, zip, IntlChar 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

@ -17,6 +17,7 @@ Database Tables
| [arrived-activity](help/database/db_arrived-activity) | Id of arrived activities | | [arrived-activity](help/database/db_arrived-activity) | Id of arrived activities |
| [attach](help/database/db_attach) | file attachments | | [attach](help/database/db_attach) | file attachments |
| [cache](help/database/db_cache) | Stores temporary data | | [cache](help/database/db_cache) | Stores temporary data |
| [channel](help/database/db_channel) | User defined Channels |
| [config](help/database/db_config) | main configuration storage | | [config](help/database/db_config) | main configuration storage |
| [contact](help/database/db_contact) | contact table | | [contact](help/database/db_contact) | contact table |
| [contact-relation](help/database/db_contact-relation) | Contact relations | | [contact-relation](help/database/db_contact-relation) | Contact relations |
@ -61,6 +62,7 @@ Database Tables
| [post-content](help/database/db_post-content) | Content for all posts | | [post-content](help/database/db_post-content) | Content for all posts |
| [post-delivery](help/database/db_post-delivery) | Delivery data for posts for the batch processing | | [post-delivery](help/database/db_post-delivery) | Delivery data for posts for the batch processing |
| [post-delivery-data](help/database/db_post-delivery-data) | Delivery data for items | | [post-delivery-data](help/database/db_post-delivery-data) | Delivery data for items |
| [post-engagement](help/database/db_post-engagement) | Engagement data per post |
| [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 |

View file

@ -0,0 +1,37 @@
Table channel
===========
User defined Channels
Fields
------
| Field | Description | Type | Null | Key | Default | Extra |
| ---------------- | ------------------------------------------------------------------------------------------------- | ------------------ | ---- | --- | ------- | -------------- |
| id | | int unsigned | NO | PRI | NULL | auto_increment |
| uid | User id | mediumint unsigned | NO | | NULL | |
| label | Channel label | varchar(64) | NO | | NULL | |
| description | Channel description | varchar(64) | YES | | NULL | |
| circle | Circle or channel that this channel is based on | int | YES | | NULL | |
| access-key | Access key | varchar(1) | YES | | NULL | |
| include-tags | Comma separated list of tags that will be included in the channel | varchar(255) | YES | | NULL | |
| exclude-tags | Comma separated list of tags that aren't allowed in the channel | varchar(255) | YES | | NULL | |
| full-text-search | Full text search pattern, see https://mariadb.com/kb/en/full-text-index-overview/#in-boolean-mode | varchar(255) | YES | | NULL | |
| media-type | Filtered media types | smallint unsigned | YES | | NULL | |
Indexes
------------
| Name | Fields |
| ------- | ------ |
| PRIMARY | id |
| uid | uid |
Foreign Keys
------------
| Field | Target Table | Target Field |
|-------|--------------|--------------|
| uid | [user](help/database/db_user) | uid |
Return to [database documentation](help/database)

View file

@ -6,13 +6,17 @@ Contact relations
Fields Fields
------ ------
| Field | Description | Type | Null | Key | Default | Extra | | Field | Description | Type | Null | Key | Default | Extra |
| ---------------- | --------------------------------------------------- | ------------ | ---- | --- | ------------------- | ----- | | --------------------- | -------------------------------------------------------- | ----------------- | ---- | --- | ------------------- | ----- |
| cid | contact the related contact had interacted with | int unsigned | NO | PRI | 0 | | | cid | contact the related contact had interacted with | int unsigned | NO | PRI | 0 | |
| relation-cid | related contact who had interacted with the contact | int unsigned | NO | PRI | 0 | | | relation-cid | related contact who had interacted with the contact | int unsigned | NO | PRI | 0 | |
| last-interaction | Date of the last interaction | datetime | NO | | 0001-01-01 00:00:00 | | | last-interaction | Date of the last interaction by relation-cid on cid | datetime | NO | | 0001-01-01 00:00:00 | |
| follow-updated | Date of the last update of the contact relationship | datetime | NO | | 0001-01-01 00:00:00 | | | follow-updated | Date of the last update of the contact relationship | datetime | NO | | 0001-01-01 00:00:00 | |
| follows | | boolean | NO | | 0 | | | follows | if true, relation-cid follows cid | boolean | NO | | 0 | |
| score | score for interactions of cid on relation-cid | smallint unsigned | YES | | NULL | |
| relation-score | score for interactions of relation-cid on cid | smallint unsigned | YES | | NULL | |
| thread-score | score for interactions of cid on threads of relation-cid | smallint unsigned | YES | | NULL | |
| relation-thread-score | score for interactions of relation-cid on threads of cid | smallint unsigned | YES | | NULL | |
Indexes Indexes
------------ ------------

View file

@ -0,0 +1,40 @@
Table post-engagement
===========
Engagement data per post
Fields
------
| 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 | |
| owner-id | Item owner | int unsigned | 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 | |
| language | Language information about this post | varbinary(128) | YES | | NULL | |
| searchtext | Simplified text for the full text search | mediumtext | YES | | NULL | |
| created | | datetime | YES | | NULL | |
| restricted | If true, this post is either unlisted or not from a federated network | boolean | NO | | 0 | |
| comments | Number of comments | mediumint unsigned | YES | | NULL | |
| activities | Number of activities (like, dislike, ...) | mediumint unsigned | YES | | NULL | |
Indexes
------------
| Name | Fields |
| ---------- | -------------------- |
| PRIMARY | uri-id |
| owner-id | owner-id |
| created | created |
| searchtext | FULLTEXT, searchtext |
Foreign Keys
------------
| Field | Target Table | Target Field |
|-------|--------------|--------------|
| uri-id | [item-uri](help/database/db_item-uri) | id |
| owner-id | [contact](help/database/db_contact) | id |
Return to [database documentation](help/database)

View file

@ -49,6 +49,7 @@ Indexes
| psid | psid | | psid | psid |
| post-user-id | post-user-id | | post-user-id | post-user-id |
| commented | commented | | commented | commented |
| received | received |
| uid_received | uid, received | | uid_received | uid, received |
| uid_wall_received | uid, wall, received | | uid_wall_received | uid, wall, received |
| uid_commented | uid, commented | | uid_commented | uid, commented |

View file

@ -6,24 +6,24 @@ Table report
Fields 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 |
| uid | Reporting user | mediumint unsigned | YES | | NULL | | | uid | Reporting user | mediumint unsigned | YES | | NULL | |
| reporter-id | Reporting contact | int unsigned | YES | | NULL | | | reporter-id | Reporting contact | int unsigned | YES | | NULL | |
| cid | Reported contact | int unsigned | NO | | NULL | | | cid | Reported contact | int unsigned | NO | | NULL | |
| gsid | Reported contact server | int unsigned | YES | | NULL | | | gsid | Reported contact server | int unsigned | YES | | NULL | |
| comment | Report | text | YES | | NULL | | | comment | Report | text | YES | | NULL | |
| category-id | Report category, one of Entity\Report::CATEGORY_* | int unsigned | NO | | 1 | | | category-id | Report category, one of Entity Report::CATEGORY_* | int unsigned | NO | | 1 | |
| forward | Forward the report to the remote server | boolean | YES | | NULL | | | forward | Forward the report to the remote server | boolean | YES | | NULL | |
| public-remarks | Remarks shared with the reporter | text | YES | | NULL | | | public-remarks | Remarks shared with the reporter | text | YES | | NULL | |
| private-remarks | Remarks shared with the moderation team | text | YES | | NULL | | | private-remarks | Remarks shared with the moderation team | text | YES | | NULL | |
| last-editor-uid | Last editor user | mediumint unsigned | YES | | NULL | | | last-editor-uid | Last editor user | mediumint unsigned | YES | | NULL | |
| assigned-uid | Assigned moderator user | mediumint unsigned | YES | | NULL | | | assigned-uid | Assigned moderator user | mediumint unsigned | YES | | NULL | |
| status | Status of the report, one of Entity\Report::STATUS_* | tinyint unsigned | NO | | NULL | | | status | Status of the report, one of Entity Report::STATUS_* | tinyint unsigned | NO | | NULL | |
| resolution | Resolution of the report, one of Entity\Report::RESOLUTION_* | tinyint unsigned | YES | | NULL | | | resolution | Resolution of the report, one of Entity Report::RESOLUTION_* | tinyint unsigned | YES | | NULL | |
| created | | datetime(6) | NO | | 0001-01-01 00:00:00 | | | created | | datetime(6) | NO | | 0001-01-01 00:00:00.000000 | |
| edited | Last time the report has been edited | datetime(6) | YES | | NULL | | | edited | Last time the report has been edited | datetime(6) | YES | | NULL | |
Indexes Indexes
------------ ------------

View file

@ -16,6 +16,7 @@ Fields
| 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 | |
| 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 | |
| pending | | boolean | YES | | NULL | | | pending | | boolean | YES | | NULL | |
| rel | The kind of the relation between the user and the contact | tinyint unsigned | YES | | NULL | | | rel | The kind of the relation between the user and the contact | tinyint unsigned | YES | | NULL | |
| info | | mediumtext | YES | | NULL | | | info | | mediumtext | YES | | NULL | |

View file

@ -8,8 +8,8 @@ Fields
| Field | Description | Type | Null | Key | Default | Extra | | Field | Description | Type | Null | Key | Default | Extra |
| ------- | ---------------------------------------- | ------------------ | ---- | --- | ------- | ----- | | ------- | ---------------------------------------- | ------------------ | ---- | --- | ------- | ----- |
| uid | Owner User id | mediumint unsigned | NO | | 0 | | | uid | Owner User id | mediumint unsigned | NO | PRI | 0 | |
| gsid | Gserver id | int unsigned | NO | | 0 | | | gsid | Gserver id | int unsigned | NO | PRI | 0 | |
| ignored | server accounts are ignored for the user | boolean | NO | | 0 | | | ignored | server accounts are ignored for the user | boolean | NO | | 0 | |
Indexes Indexes

View file

@ -103,6 +103,15 @@ Derzeitige Hooks
$b ist das Item-Array einer Information, die in der Datenbank und im Item gespeichert ist. $b ist das Item-Array einer Information, die in der Datenbank und im Item gespeichert ist.
{Bitte beachte: der Seiteninhalt ist bbcode - nicht HTML) {Bitte beachte: der Seiteninhalt ist bbcode - nicht HTML)
**'detect_languages'**
Wird nach der Sprachenerkennung aufgerufen.
Dieser Hook kann dafür verwendet werden, alternative Erkennungsfunktionen einzubinden.
`$data` ist ein Array:
'text' => Der analysierte Text.
'detected' => (Eingabe/Ausgabe) Das Array mit den erkannten Sprachen. Der Sprachcode ist der Array-Schlüssel, der Array-Wert ist der dezimale Wert für die Wahrscheinlichkeit.
'uri-id' => Die Uri-Id des Beitrags
'author-id' => Die Contact-id des Autors.
**'addon_settings'** - wird aufgerufen, wenn die HTML-Ausgabe der Addon-Einstellungsseite generiert wird. **'addon_settings'** - wird aufgerufen, wenn die HTML-Ausgabe der Addon-Einstellungsseite generiert wird.
$b ist die HTML-Ausgabe (String) der Addon-Einstellungsseite vor dem finalen "</form>"-Tag. $b ist die HTML-Ausgabe (String) der Addon-Einstellungsseite vor dem finalen "</form>"-Tag.
@ -316,6 +325,7 @@ Eine komplette Liste aller Hook-Callbacks mit den zugehörigen Dateien (am 01-Ap
### src/Model/Item.php ### src/Model/Item.php
Hook::callAll('detect_languages', $item);
Hook::callAll('post_local', $item); Hook::callAll('post_local', $item);
Hook::callAll('post_remote', $item); Hook::callAll('post_remote', $item);
Hook::callAll('post_local_end', $posted_item); Hook::callAll('post_local_end', $posted_item);

View file

@ -45,7 +45,7 @@ Die aufgelisteten Adressen werden wie folgt durch Kommas voneinander getrennt:
'admin_email' => 'mail1@example.com,mail2@example.com', 'admin_email' => 'mail1@example.com,mail2@example.com',
``` ```
<a name="dbupdate"> <a name="dbupdate"></a>
### Die Datenbank Struktur schein nicht aktuell zu sein. Was kann ich tun? ### Die Datenbank Struktur schein nicht aktuell zu sein. Was kann ich tun?
Rufe bitte im Admin Panel den Punkt [DB Updates](/admin/dbsync/) auf und folge dem Link *Datenbank Struktur überprüfen*. Rufe bitte im Admin Panel den Punkt [DB Updates](/admin/dbsync/) auf und folge dem Link *Datenbank Struktur überprüfen*.
@ -56,4 +56,4 @@ Starte dazu bitte vom Grundverzeichnis deiner Friendica Instanz folgendes Komman
bin/console dbstructure update bin/console dbstructure update
sollten bei der Ausführung Fehler auftreten, kontaktiere bitte die [Support Gruppe](https://forum.friendi.ca/profile/helpers). sollten bei der Ausführung Fehler auftreten, kontaktiere bitte die [Friendia Support](https://forum.friendi.ca/profile/helpers) Gruppe oder die [Friendica Admins](https://forum.friendi.ca/profile/admins) Gruppe.

View file

@ -17,6 +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)
* [Chats](help/Chats) * [Chats](help/Chats)
* Weiterführende Informationen * Weiterführende Informationen
* [Account umziehen](help/Move-Account) * [Account umziehen](help/Move-Account)

View file

@ -25,9 +25,9 @@ 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.3+ (PHP 8 wird noch nicht komplett unterstützt) * PHP 7.3+
* 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, MySQLi, xml, zip und OpenSSL-Erweiterung * Curl, GD, GMP, PDO, mbstrings, MySQLi, hash, xml, zip, IntlChar and 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

@ -34,7 +34,7 @@ function lostpass_post(App $a)
DI::baseUrl()->redirect(); DI::baseUrl()->redirect();
} }
$condition = ['(`email` = ? OR `nickname` = ?) AND `verified` = 1 AND `blocked` = 0 AND `account_removed` = 0 AND `account_expired` = 0', $loginame, $loginame]; $condition = ['(`email` = ? OR `nickname` = ?) AND `verified` AND NOT `blocked` AND NOT `account_removed` AND NOT `account_expired`', $loginame, $loginame];
$user = DBA::selectFirst('user', ['uid', 'username', 'nickname', 'email', 'language'], $condition); $user = DBA::selectFirst('user', ['uid', 'username', 'nickname', 'email', 'language'], $condition);
if (!DBA::isResult($user)) { if (!DBA::isResult($user)) {
DI::sysmsg()->addNotice(DI::l10n()->t('No valid account found.')); DI::sysmsg()->addNotice(DI::l10n()->t('No valid account found.'));

View file

@ -64,7 +64,7 @@ class App
{ {
const PLATFORM = 'Friendica'; const PLATFORM = 'Friendica';
const CODENAME = 'Giant Rhubarb'; const CODENAME = 'Giant Rhubarb';
const VERSION = '2023.09-dev'; const VERSION = '2023.09-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
@ -565,6 +565,9 @@ 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'] ?? '');
$this->logger->debug('Request received', ['address' => $_SERVER['REMOTE_ADDR'] ?? '', 'request' => $requeststring, 'referer' => $_SERVER['HTTP_REFERER'] ?? '', 'user-agent' => $_SERVER['HTTP_USER_AGENT'] ?? '']);
$this->profiler->set($start_time, 'start'); $this->profiler->set($start_time, 'start');
$this->profiler->set(microtime(true), 'classinit'); $this->profiler->set(microtime(true), 'classinit');
@ -712,8 +715,10 @@ class App
$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->baseURL, $this->args, $this->mode, $response, $this->l10n, $this->profiler, $this->config, $pconfig, $nav, $this->session->getLocalUserId());
} }
$page->exit($response); $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'] ?? '']);
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'] ?? '']);
$httpException->rawContent($e); $httpException->rawContent($e);
} }
$page->logRuntime($this->config, 'runFrontend'); $page->logRuntime($this->config, 'runFrontend');

View file

@ -401,36 +401,6 @@ class Page implements ArrayAccess
$this->footerScripts[] = trim($url, '/'); $this->footerScripts[] = trim($url, '/');
} }
/**
* Directly exit with the current response (include setting all headers)
*
* @param ResponseInterface $response
*/
public function exit(ResponseInterface $response)
{
header(sprintf("HTTP/%s %s %s",
$response->getProtocolVersion(),
$response->getStatusCode(),
$response->getReasonPhrase())
);
foreach ($response->getHeaders() as $key => $header) {
if (is_array($header)) {
$header_str = implode(',', $header);
} else {
$header_str = $header;
}
if (empty($key)) {
header($header_str);
} else {
header("$key: $header_str");
}
}
echo $response->getBody();
}
/** /**
* Executes the creation of the current page and prints it to the screen * Executes the creation of the current page and prints it to the screen
* *
@ -526,7 +496,9 @@ class Page implements ArrayAccess
} }
if ($_GET["mode"] == "raw") { if ($_GET["mode"] == "raw") {
System::httpExit(substr($target->saveHTML(), 6, -8), Response::TYPE_HTML); $response->withBody(Utils::streamFor($target->saveHTML()));
System::echoResponse($response);
System::exit();
} }
} }

View file

@ -129,6 +129,24 @@ class BaseCollection extends \ArrayIterator
return new static(array_reverse($this->getArrayCopy()), $this->getTotalCount()); return new static(array_reverse($this->getArrayCopy()), $this->getTotalCount());
} }
/**
* Split the collection in smaller collections no bigger than the provided length
*
* @param int $length
* @return static[]
*/
public function chunk(int $length): array
{
if ($length < 1) {
throw new \RangeException('BaseCollection->chunk(): Size parameter expected to be greater than 0');
}
return array_map(function ($array) {
return new static($array);
}, array_chunk($this->getArrayCopy(), $length));
}
/** /**
* @inheritDoc * @inheritDoc
* *

View file

@ -27,11 +27,13 @@ use Friendica\Capabilities\ICanCreateResponses;
use Friendica\Core\Hook; use Friendica\Core\Hook;
use Friendica\Core\L10n; use Friendica\Core\L10n;
use Friendica\Core\Logger; use Friendica\Core\Logger;
use Friendica\Core\System;
use Friendica\Model\User; use Friendica\Model\User;
use Friendica\Module\Response; use Friendica\Module\Response;
use Friendica\Module\Special\HTTPException as ModuleHTTPException; use Friendica\Module\Special\HTTPException as ModuleHTTPException;
use Friendica\Network\HTTPException; use Friendica\Network\HTTPException;
use Friendica\Util\Profiler; use Friendica\Util\Profiler;
use Friendica\Util\XML;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
@ -106,8 +108,7 @@ abstract class BaseModule implements ICanHandleRequests
*/ */
protected function rawContent(array $request = []) protected function rawContent(array $request = [])
{ {
// echo ''; // $this->httpExit(...);
// exit;
} }
/** /**
@ -234,7 +235,8 @@ abstract class BaseModule implements ICanHandleRequests
$timestamp = microtime(true); $timestamp = microtime(true);
// "rawContent" is especially meant for technical endpoints. // "rawContent" is especially meant for technical endpoints.
// This endpoint doesn't need any theme initialization or other comparable stuff. // This endpoint doesn't need any theme initialization or
// templating and is expected to exit on its own if it is set.
$this->rawContent($request); $this->rawContent($request);
try { try {
@ -456,4 +458,76 @@ abstract class BaseModule implements ICanHandleRequests
return $tabs; return $tabs;
} }
/**
* This function adds the content and a content-type HTTP header to the output.
* After finishing the process is getting killed.
*
* @param string $content
* @param string $type
* @param string|null $content_type
* @return void
* @throws HTTPException\InternalServerErrorException
*/
public function httpExit(string $content, string $type = Response::TYPE_HTML, ?string $content_type = null)
{
$this->response->setType($type, $content_type);
$this->response->addContent($content);
System::echoResponse($this->response->generate());
System::exit();
}
/**
* Send HTTP status header and exit.
*
* @param integer $httpCode HTTP status result value
* @param string $message Error message. Optional.
* @param mixed $content Response body. Optional.
* @throws \Exception
*/
public function httpError(int $httpCode, string $message = '', $content = '')
{
if ($httpCode >= 400) {
$this->logger->debug('Exit with error', ['code' => $httpCode, 'message' => $message, 'callstack' => System::callstack(20), 'method' => $this->args->getMethod(), 'agent' => $this->server['HTTP_USER_AGENT'] ?? '']);
}
$this->response->setStatus($httpCode, $message);
$this->httpExit($content);
}
/**
* Display the response using JSON to encode the content
*
* @param mixed $content
* @param string $content_type
* @param int $options A combination of json_encode() binary flags
* @return void
* @throws HTTPException\InternalServerErrorException
* @see json_encode()
*/
public function jsonExit($content, string $content_type = 'application/json', int $options = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)
{
$this->httpExit(json_encode($content, $options), ICanCreateResponses::TYPE_JSON, $content_type);
}
/**
* Display a non-200 HTTP code response using JSON to encode the content and exit
*
* @param int $httpCode
* @param mixed $content
* @param string $content_type
* @return void
* @throws HTTPException\InternalServerErrorException
*/
public function jsonError(int $httpCode, $content, string $content_type = 'application/json')
{
if ($httpCode >= 400) {
$this->logger->debug('Exit with error', ['code' => $httpCode, 'content_type' => $content_type, 'callstack' => System::callstack(20), 'method' => $this->args->getMethod(), 'agent' => $this->server['HTTP_USER_AGENT'] ?? '']);
}
$this->response->setStatus($httpCode);
$this->jsonExit($content, $content_type);
}
} }

View file

@ -70,7 +70,7 @@ interface ICanCreateResponses
* *
* @throws InternalServerErrorException * @throws InternalServerErrorException
*/ */
public function setType(string $type, ?string $content_type = null): void; public function setType(string $type = ICanCreateResponses::TYPE_HTML, ?string $content_type = null): void;
/** /**
* Sets the status and the reason for the response * Sets the status and the reason for the response

View file

@ -221,6 +221,7 @@ class ContactSelector
'mastodon' => 'mastodon', 'peertube' => 'peertube', 'pixelfed' => 'pixelfed', 'mastodon' => 'mastodon', 'peertube' => 'peertube', 'pixelfed' => 'pixelfed',
'pleroma' => 'pleroma', 'red' => 'hubzilla', 'redmatrix' => 'hubzilla', 'pleroma' => 'pleroma', 'red' => 'hubzilla', 'redmatrix' => 'hubzilla',
'socialhome' => 'social-home', 'wordpress' => 'wordpress', 'lemmy' => 'users', 'socialhome' => 'social-home', 'wordpress' => 'wordpress', 'lemmy' => 'users',
'plume' => 'plume', 'funkwhale' => 'funkwhale', 'nextcloud' => 'nextcloud', 'drupal' => 'drupal',
'firefish' => 'fire', 'calckey' => 'calculator', 'kbin' => 'check']; 'firefish' => 'fire', 'calckey' => 'calculator', 'kbin' => 'check'];
$search = array_keys($nets); $search = array_keys($nets);

View file

@ -57,6 +57,7 @@ use Psr\Log\LoggerInterface;
class Conversation class Conversation
{ {
const MODE_CHANNEL = 'channel';
const MODE_COMMUNITY = 'community'; const MODE_COMMUNITY = 'community';
const MODE_CONTACTS = 'contacts'; const MODE_CONTACTS = 'contacts';
const MODE_CONTACT_POSTS = 'contact-posts'; const MODE_CONTACT_POSTS = 'contact-posts';
@ -494,7 +495,9 @@ class Conversation
. (!empty($_GET['cmin']) ? '&cmin=' . rawurlencode($_GET['cmin']) : '') . (!empty($_GET['cmin']) ? '&cmin=' . rawurlencode($_GET['cmin']) : '')
. (!empty($_GET['cmax']) ? '&cmax=' . rawurlencode($_GET['cmax']) : '') . (!empty($_GET['cmax']) ? '&cmax=' . rawurlencode($_GET['cmax']) : '')
. (!empty($_GET['file']) ? '&file=' . rawurlencode($_GET['file']) : '') . (!empty($_GET['file']) ? '&file=' . rawurlencode($_GET['file']) : '')
. (!empty($_GET['channel']) ? '&channel=' . rawurlencode($_GET['channel']) : '')
. (!empty($_GET['no_sharer']) ? '&no_sharer=' . rawurlencode($_GET['no_sharer']) : '')
. (!empty($_GET['accounttype']) ? '&accounttype=' . rawurlencode($_GET['accounttype']) : '')
. "'; </script>\r\n"; . "'; </script>\r\n";
} }
} elseif ($mode === self::MODE_PROFILE) { } elseif ($mode === self::MODE_PROFILE) {
@ -530,6 +533,17 @@ class Conversation
. "<script> var profile_uid = " . ($this->session->getLocalUserId() ?: 0) . ";" . "<script> var profile_uid = " . ($this->session->getLocalUserId() ?: 0) . ";"
. "</script>"; . "</script>";
} }
} elseif ($mode === self::MODE_CHANNEL) {
$items = $this->addChildren($items, true, $order, $uid, $mode, $ignoredGsids);
if (!$update) {
$live_update_div = '<div id="live-channel"></div>' . "\r\n"
. "<script> var profile_uid = -1; var netargs = '" . substr($this->args->getCommand(), 8)
. '?f='
. (!empty($_GET['no_sharer']) ? '&no_sharer=' . rawurlencode($_GET['no_sharer']) : '')
. (!empty($_GET['accounttype']) ? '&accounttype=' . rawurlencode($_GET['accounttype']) : '')
. "'; </script>\r\n";
}
} elseif ($mode === self::MODE_COMMUNITY) { } elseif ($mode === self::MODE_COMMUNITY) {
$items = $this->addChildren($items, true, $order, $uid, $mode, $ignoredGsids); $items = $this->addChildren($items, true, $order, $uid, $mode, $ignoredGsids);
@ -621,7 +635,7 @@ class Conversation
unset($conv_responses['dislike']); unset($conv_responses['dislike']);
} }
if (in_array($mode, [self::MODE_COMMUNITY, self::MODE_CONTACTS, self::MODE_PROFILE])) { if (in_array($mode, [self::MODE_CHANNEL, self::MODE_COMMUNITY, self::MODE_CONTACTS, self::MODE_PROFILE])) {
$writable = true; $writable = true;
} else { } else {
$writable = $items[0]['writable'] || ($items[0]['uid'] == 0) && in_array($items[0]['network'], Protocol::FEDERATED); $writable = $items[0]['writable'] || ($items[0]['uid'] == 0) && in_array($items[0]['network'], Protocol::FEDERATED);
@ -918,7 +932,8 @@ class Conversation
continue; continue;
} }
if (in_array($row['author-gsid'], $ignoredGsids) if (
in_array($row['author-gsid'], $ignoredGsids)
|| in_array($row['owner-gsid'], $ignoredGsids) || in_array($row['owner-gsid'], $ignoredGsids)
|| in_array($row['causer-gsid'], $ignoredGsids) || in_array($row['causer-gsid'], $ignoredGsids)
) { ) {
@ -1009,7 +1024,7 @@ class Conversation
$items[$key]['user-collapsed-owner'] = !$always_display && in_array($row['owner-id'], $collapses); $items[$key]['user-collapsed-owner'] = !$always_display && in_array($row['owner-id'], $collapses);
if ( if (
in_array($mode, [self::MODE_COMMUNITY, self::MODE_NETWORK]) && in_array($mode, [self::MODE_CHANNEL, self::MODE_COMMUNITY, self::MODE_NETWORK]) &&
(in_array($row['author-id'], $blocks) || in_array($row['owner-id'], $blocks) || in_array($row['author-id'], $ignores) || in_array($row['owner-id'], $ignores)) (in_array($row['author-id'], $blocks) || in_array($row['owner-id'], $blocks) || in_array($row['author-id'], $ignores) || in_array($row['owner-id'], $ignores))
) { ) {
unset($items[$key]); unset($items[$key]);

View file

@ -0,0 +1,28 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, 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\Content\Conversation\Collection;
use Friendica\BaseCollection;
class Timelines extends BaseCollection
{
}

View file

@ -0,0 +1,26 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, 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\Content\Conversation\Collection;
class UserDefinedChannels extends Timelines
{
}

View file

@ -0,0 +1,34 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, 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\Content\Conversation\Entity;
class Channel extends Timeline
{
const WHATSHOT = 'whatshot';
const FORYOU = 'foryou';
const FOLLOWERS = 'followers';
const SHARERSOFSHARERS = 'sharersofsharers';
const IMAGE = 'image';
const VIDEO = 'video';
const AUDIO = 'audio';
const LANGUAGE = 'language';
}

View file

@ -0,0 +1,28 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, 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\Content\Conversation\Entity;
final class Community extends Timeline
{
const LOCAL = 'local';
const GLOBAL = 'global';
}

View file

@ -0,0 +1,31 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, 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\Content\Conversation\Entity;
final class Network extends Timeline
{
const STAR = 'star';
const MENTION = 'mention';
const RECEIVED = 'received';
const COMMENTED = 'commented';
const CREATED = 'created';
}

View file

@ -0,0 +1,76 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, 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\Content\Conversation\Entity;
/**
* @property-read string $code Channel code
* @property-read string $label Channel label
* @property-read string $description Channel description
* @property-read string $accessKey Access key
* @property-read string $path Path
* @property-read int $uid User of the channel
* @property-read string $includeTags The tags to include in the channel
* @property-read string $excludeTags The tags to exclude in the channel
* @property-read string $fullTextSearch full text search pattern
* @property-read int $mediaType Media types that are included in the channel
* @property-read int $circle Circle or timeline this channel is based on
*/
class Timeline extends \Friendica\BaseEntity
{
/** @var string */
protected $code;
/** @var string */
protected $label;
/** @var string */
protected $description;
/** @var string */
protected $accessKey;
/** @var string */
protected $path;
/** @var int */
protected $uid;
/** @var int */
protected $circle;
/** @var string */
protected $includeTags;
/** @var string */
protected $excludeTags;
/** @var string */
protected $fullTextSearch;
/** @var int */
protected $mediaType;
public function __construct(string $code = null, string $label = null, string $description = null, string $accessKey = null, string $path = null, int $uid = null, string $includeTags = null, string $excludeTags = null, string $fullTextSearch = null, int $mediaType = null, int $circle = null)
{
$this->code = $code;
$this->label = $label;
$this->description = $description;
$this->accessKey = $accessKey;
$this->path = $path;
$this->uid = $uid;
$this->includeTags = $includeTags;
$this->excludeTags = $excludeTags;
$this->fullTextSearch = $fullTextSearch;
$this->mediaType = $mediaType;
$this->circle = $circle;
}
}

View file

@ -0,0 +1,26 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, 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\Content\Conversation\Entity;
class UserDefinedChannel extends Channel
{
}

View file

@ -0,0 +1,59 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, 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\Content\Conversation\Factory;
use Friendica\Content\Conversation\Collection\Timelines;
use Friendica\Content\Conversation\Entity\Channel as ChannelEntity;
use Friendica\Model\User;
final class Channel extends Timeline
{
/**
* List of available channels
*
* @param integer $uid
* @return Timelines
*/
public function getTimelines(int $uid): Timelines
{
$language = User::getLanguageCode($uid);
$languages = $this->l10n->getAvailableLanguages(true);
$tabs = [
new ChannelEntity(ChannelEntity::FORYOU, $this->l10n->t('For you'), $this->l10n->t('Posts from contacts you interact with and who interact with you'), 'y'),
new ChannelEntity(ChannelEntity::WHATSHOT, $this->l10n->t('What\'s Hot'), $this->l10n->t('Posts with a lot of interactions'), 'h'),
new ChannelEntity(ChannelEntity::LANGUAGE, $languages[$language], $this->l10n->t('Posts in %s', $languages[$language]), 'g'),
new ChannelEntity(ChannelEntity::FOLLOWERS, $this->l10n->t('Followers'), $this->l10n->t('Posts from your followers that you don\'t follow'), 'f'),
new ChannelEntity(ChannelEntity::SHARERSOFSHARERS, $this->l10n->t('Sharers of sharers'), $this->l10n->t('Posts from accounts that are followed by accounts that you follow'), 'r'),
new ChannelEntity(ChannelEntity::IMAGE, $this->l10n->t('Images'), $this->l10n->t('Posts with images'), 'i'),
new ChannelEntity(ChannelEntity::AUDIO, $this->l10n->t('Audio'), $this->l10n->t('Posts with audio'), 'd'),
new ChannelEntity(ChannelEntity::VIDEO, $this->l10n->t('Videos'), $this->l10n->t('Posts with videos'), 'v'),
];
return new Timelines($tabs);
}
public function isTimeline(string $selectedTab): bool
{
return in_array($selectedTab, [ChannelEntity::WHATSHOT, ChannelEntity::FORYOU, ChannelEntity::FOLLOWERS, ChannelEntity::SHARERSOFSHARERS, ChannelEntity::IMAGE, ChannelEntity::VIDEO, ChannelEntity::AUDIO, ChannelEntity::LANGUAGE]);
}
}

View file

@ -0,0 +1,56 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, 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\Content\Conversation\Factory;
use Friendica\Content\Conversation\Collection\Timelines;
use Friendica\Content\Conversation\Entity\Community as CommunityEntity;
use Friendica\Module\Conversation\Community as CommunityModule;
final class Community extends Timeline
{
/**
* List of available communities
*
* @param boolean $authenticated
* @return Timelines
*/
public function getTimelines(bool $authenticated): Timelines
{
$page_style = $this->config->get('system', 'community_page_style');
$tabs = [];
if (($authenticated || in_array($page_style, [CommunityModule::LOCAL_AND_GLOBAL, CommunityModule::LOCAL])) && empty($this->config->get('system', 'singleuser'))) {
$tabs[] = new CommunityEntity(CommunityEntity::LOCAL, $this->l10n->t('Local Community'), $this->l10n->t('Posts from local users on this server'), 'l');
}
if ($authenticated || in_array($page_style, [CommunityModule::LOCAL_AND_GLOBAL, CommunityModule::GLOBAL])) {
$tabs[] = new CommunityEntity(CommunityEntity::GLOBAL, $this->l10n->t('Global Community'), $this->l10n->t('Posts from users of the whole federated network'), 'g');
}
return new Timelines($tabs);
}
public function isTimeline(string $selectedTab): bool
{
return in_array($selectedTab, [CommunityEntity::LOCAL, CommunityEntity::GLOBAL]);
}
}

View file

@ -0,0 +1,51 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, 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\Content\Conversation\Factory;
use Friendica\Content\Conversation\Collection\Timelines;
use Friendica\Content\Conversation\Entity\Network as NetworkEntity;
final class Network extends Timeline
{
/**
* List of available network timelines
*
* @param string $command
* @return Timelines
*/
public function getTimelines(string $command): Timelines
{
$tabs = [
new NetworkEntity(NetworkEntity::COMMENTED, $this->l10n->t('Latest Activity'), $this->l10n->t('Sort by latest activity'), 'e', $command . '?' . http_build_query(['order' => 'commented'])),
new NetworkEntity(NetworkEntity::RECEIVED, $this->l10n->t('Latest Posts'), $this->l10n->t('Sort by post received date'), 't', $command . '?' . http_build_query(['order' => 'received'])),
new NetworkEntity(NetworkEntity::CREATED, $this->l10n->t('Latest Creation'), $this->l10n->t('Sort by post creation date'), 'q', $command . '?' . http_build_query(['order' => 'created'])),
new NetworkEntity(NetworkEntity::MENTION, $this->l10n->t('Personal'), $this->l10n->t('Posts that mention or involve you'), 'r', $command . '?' . http_build_query(['mention' => true])),
new NetworkEntity(NetworkEntity::STAR, $this->l10n->t('Starred'), $this->l10n->t('Favourite Posts'), 'm', $command . '?' . http_build_query(['star' => true])),
];
return new Timelines($tabs);
}
public function isTimeline(string $selectedTab): bool
{
return in_array($selectedTab, [NetworkEntity::COMMENTED, NetworkEntity::RECEIVED, NetworkEntity::CREATED, NetworkEntity::MENTION, NetworkEntity::STAR]);
}
}

View file

@ -0,0 +1,48 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, 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\Content\Conversation\Factory;
use Friendica\Capabilities\ICanCreateFromTableRow;
use Friendica\Content\Conversation\Entity\Timeline as TimelineEntity;
use Friendica\Content\Conversation\Repository\UserDefinedChannel;
use Friendica\Core\Config\Capability\IManageConfigValues;
use Friendica\Core\L10n;
use Psr\Log\LoggerInterface;
class Timeline extends \Friendica\BaseFactory
{
/** @var L10n */
protected $l10n;
/** @var IManageConfigValues The config */
protected $config;
/** @var UserDefinedChannel */
protected $channelRepository;
public function __construct(UserDefinedChannel $channel, L10n $l10n, LoggerInterface $logger, IManageConfigValues $config)
{
parent::__construct($logger);
$this->channelRepository = $channel;
$this->l10n = $l10n;
$this->config = $config;
}
}

View file

@ -0,0 +1,51 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, 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\Content\Conversation\Factory;
use Friendica\Capabilities\ICanCreateFromTableRow;
use Friendica\Content\Conversation\Collection\Timelines;
use Friendica\Content\Conversation\Entity;
final class UserDefinedChannel extends Timeline implements ICanCreateFromTableRow
{
public function isTimeline(string $selectedTab, int $uid): bool
{
return is_numeric($selectedTab) && $uid && $this->channelRepository->existsById($selectedTab, $uid);
}
public function createFromTableRow(array $row): Entity\UserDefinedChannel
{
return new Entity\UserDefinedChannel(
$row['id'] ?? null,
$row['label'],
$row['description'] ?? null,
$row['access-key'] ?? null,
null,
$row['uid'],
$row['include-tags'] ?? null,
$row['exclude-tags'] ?? null,
$row['full-text-search'] ?? null,
$row['media-type'] ?? null,
$row['circle'] ?? null,
);
}
}

View file

@ -0,0 +1,133 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, 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\Content\Conversation\Repository;
use Friendica\BaseCollection;
use Friendica\Content\Conversation\Collection\UserDefinedChannels;
use Friendica\Content\Conversation\Entity;
use Friendica\Content\Conversation\Factory;
use Friendica\Database\Database;
use Psr\Log\LoggerInterface;
class UserDefinedChannel extends \Friendica\BaseRepository
{
protected static $table_name = 'channel';
public function __construct(Database $database, LoggerInterface $logger, Factory\UserDefinedChannel $factory)
{
parent::__construct($database, $logger, $factory);
}
/**
* @param array $condition
* @param array $params
* @return UserDefinedChannels
* @throws \Exception
*/
protected function _select(array $condition, array $params = []): BaseCollection
{
$rows = $this->db->selectToArray(static::$table_name, [], $condition, $params);
$Entities = new UserDefinedChannels();
foreach ($rows as $fields) {
$Entities[] = $this->factory->createFromTableRow($fields);
}
return $Entities;
}
/**
* Fetch a single user channel
*
* @param int $id The id of the user defined channel
* @param int $uid The user that this channel belongs to. (Not part of the primary key)
* @return Entity\UserDefinedChannel
* @throws \Friendica\Network\HTTPException\NotFoundException
*/
public function selectById(int $id, int $uid): Entity\UserDefinedChannel
{
return $this->_selectOne(['id' => $id, 'uid' => $uid]);
}
/**
* Checks if the provided channel id exists for this user
*
* @param integer $id
* @param integer $uid
* @return boolean
*/
public function existsById(int $id, int $uid): bool
{
return $this->exists(['id' => $id, 'uid' => $uid]);
}
/**
* Delete the given channel
*
* @param integer $id
* @param integer $uid
* @return boolean
*/
public function deleteById(int $id, int $uid): bool
{
return $this->db->delete('channel', ['id' => $id, 'uid' => $uid]);
}
/**
* Fetch all user channels
*
* @param integer $uid
* @return UserDefinedChannels
* @throws \Exception
*/
public function selectByUid(int $uid): UserDefinedChannels
{
return $this->_select(['uid' => $uid]);
}
public function save(Entity\UserDefinedChannel $Channel): Entity\UserDefinedChannel
{
$fields = [
'label' => $Channel->label,
'description' => $Channel->description,
'access-key' => $Channel->accessKey,
'uid' => $Channel->uid,
'circle' => $Channel->circle,
'include-tags' => $Channel->includeTags,
'exclude-tags' => $Channel->excludeTags,
'full-text-search' => $Channel->fullTextSearch,
'media-type' => $Channel->mediaType,
];
if ($Channel->code) {
$this->db->update(self::$table_name, $fields, ['uid' => $Channel->uid, 'id' => $Channel->code]);
} else {
$this->db->insert(self::$table_name, $fields, Database::INSERT_IGNORE);
$newChannelId = $this->db->lastInsertId();
$Channel = $this->selectById($newChannelId, $Channel->uid);
}
return $Channel;
}
}

154
src/Content/Image.php Normal file
View file

@ -0,0 +1,154 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, 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\Content;
use Friendica\Content\Image\Collection\MasonryImageRow;
use Friendica\Content\Image\Entity\MasonryImage;
use Friendica\Content\Post\Collection\PostMedias;
use Friendica\Core\Renderer;
class Image
{
public static function getBodyAttachHtml(PostMedias $PostMediaImages): string
{
$media = '';
if ($PostMediaImages->haveDimensions()) {
if (count($PostMediaImages) > 1) {
$media = self::getHorizontalMasonryHtml($PostMediaImages);
} elseif (count($PostMediaImages) == 1) {
$media = Renderer::replaceMacros(Renderer::getMarkupTemplate('content/image/single_with_height_allocation.tpl'), [
'$image' => $PostMediaImages[0],
'$allocated_height' => $PostMediaImages[0]->getAllocatedHeight(),
'$allocated_max_width' => ($PostMediaImages[0]->previewWidth ?? $PostMediaImages[0]->width) . 'px',
]);
}
} else {
if (count($PostMediaImages) > 1) {
$media = self::getImageGridHtml($PostMediaImages);
} elseif (count($PostMediaImages) == 1) {
$media = Renderer::replaceMacros(Renderer::getMarkupTemplate('content/image/single.tpl'), [
'$image' => $PostMediaImages[0],
]);
}
}
return $media;
}
/**
* @param PostMedias $images
* @return string
* @throws \Friendica\Network\HTTPException\ServiceUnavailableException
*/
private static function getImageGridHtml(PostMedias $images): string
{
// Image for first column (fc) and second column (sc)
$images_fc = [];
$images_sc = [];
for ($i = 0; $i < count($images); $i++) {
($i % 2 == 0) ? ($images_fc[] = $images[$i]) : ($images_sc[] = $images[$i]);
}
return Renderer::replaceMacros(Renderer::getMarkupTemplate('content/image/grid.tpl'), [
'columns' => [
'fc' => $images_fc,
'sc' => $images_sc,
],
]);
}
/**
* Creates a horizontally masoned gallery with a fixed maximum number of pictures per row.
*
* For each row, we calculate how much of the total width each picture will take depending on their aspect ratio
* and how much relative height it needs to accomodate all pictures next to each other with their height normalized.
*
* @param array $images
* @return string
* @throws \Friendica\Network\HTTPException\ServiceUnavailableException
*/
private static function getHorizontalMasonryHtml(PostMedias $images): string
{
static $column_size = 2;
$rows = array_map(
function (PostMedias $PostMediaImages) {
if ($singleImageInRow = count($PostMediaImages) == 1) {
$PostMediaImages[] = $PostMediaImages[0];
}
$widths = [];
$heights = [];
foreach ($PostMediaImages as $PostMediaImage) {
if ($PostMediaImage->width && $PostMediaImage->height) {
$widths[] = $PostMediaImage->width;
$heights[] = $PostMediaImage->height;
} else {
$widths[] = $PostMediaImage->previewWidth;
$heights[] = $PostMediaImage->previewHeight;
}
}
$maxHeight = max($heights);
// Corrected width preserving aspect ratio when all images on a row are the same height
$correctedWidths = [];
foreach ($widths as $i => $width) {
$correctedWidths[] = $width * $maxHeight / $heights[$i];
}
$totalWidth = array_sum($correctedWidths);
$row_images2 = [];
if ($singleImageInRow) {
unset($PostMediaImages[1]);
}
foreach ($PostMediaImages as $i => $PostMediaImage) {
$row_images2[] = new MasonryImage(
$PostMediaImage->uriId,
$PostMediaImage->url,
$PostMediaImage->preview,
$PostMediaImage->description ?? '',
100 * $correctedWidths[$i] / $totalWidth,
100 * $maxHeight / $correctedWidths[$i]
);
}
// This magic value will stay constant for each image of any given row and is ultimately
// used to determine the height of the row container relative to the available width.
$commonHeightRatio = 100 * $correctedWidths[0] / $totalWidth / ($widths[0] / $heights[0]);
return new MasonryImageRow($row_images2, count($row_images2), $commonHeightRatio);
},
$images->chunk($column_size)
);
return Renderer::replaceMacros(Renderer::getMarkupTemplate('content/image/horizontal_masonry.tpl'), [
'$rows' => $rows,
'$column_size' => $column_size,
]);
}
}

View file

@ -0,0 +1,57 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, 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\Content\Image\Collection;
use Friendica\Content\Image\Entity;
use Friendica\BaseCollection;
use Friendica\Content\Image\Entity\MasonryImage;
class MasonryImageRow extends BaseCollection
{
/** @var ?float */
protected $heightRatio;
/**
* @param MasonryImage[] $entities
* @param int|null $totalCount
* @param float|null $heightRatio
*/
public function __construct(array $entities = [], int $totalCount = null, float $heightRatio = null)
{
parent::__construct($entities, $totalCount);
$this->heightRatio = $heightRatio;
}
/**
* @return Entity\MasonryImage
*/
public function current(): Entity\MasonryImage
{
return parent::current();
}
public function getHeightRatio(): ?float
{
return $this->heightRatio;
}
}

View file

@ -0,0 +1,60 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, 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\Content\Image\Entity;
use Friendica\BaseEntity;
use Psr\Http\Message\UriInterface;
/**
* @property-read int $uriId
* @property-read UriInterface $url
* @property-read ?UriInterface $preview
* @property-read string $description
* @property-read float $heightRatio
* @property-read float $widthRatio
* @see \Friendica\Content\Image::getHorizontalMasonryHtml()
*/
class MasonryImage extends BaseEntity
{
/** @var int */
protected $uriId;
/** @var UriInterface */
protected $url;
/** @var ?UriInterface */
protected $preview;
/** @var string */
protected $description;
/** @var float Ratio of the width of the image relative to the total width of the images on the row */
protected $widthRatio;
/** @var float Ratio of the height of the image relative to its width for height allocation */
protected $heightRatio;
public function __construct(int $uriId, UriInterface $url, ?UriInterface $preview, string $description, float $widthRatio, float $heightRatio)
{
$this->url = $url;
$this->uriId = $uriId;
$this->preview = $preview;
$this->description = $description;
$this->widthRatio = $widthRatio;
$this->heightRatio = $heightRatio;
}
}

View file

@ -297,7 +297,7 @@ class Item
if ($this->activity->match($item['verb'], Activity::TAG)) { if ($this->activity->match($item['verb'], Activity::TAG)) {
$fields = [ $fields = [
'author-id', 'author-link', 'author-name', 'author-network', 'author-id', 'author-link', 'author-name', 'author-network', 'author-link', 'author-alias',
'verb', 'object-type', 'resource-id', 'body', 'plink' 'verb', 'object-type', 'resource-id', 'body', 'plink'
]; ];
$obj = Post::selectFirst($fields, ['uri' => $item['parent-uri']]); $obj = Post::selectFirst($fields, ['uri' => $item['parent-uri']]);
@ -638,7 +638,7 @@ class Item
$body = $item['body']; $body = $item['body'];
} }
if (empty($item['quote-uri-id'])) { if (empty($item['quote-uri-id']) || ($item['quote-uri-id'] == $item['uri-id'])) {
return $body; return $body;
} }
@ -729,7 +729,7 @@ class Item
*/ */
public function getSharedPost(array $item, array $fields = []): array public function getSharedPost(array $item, array $fields = []): array
{ {
if (!empty($item['quote-uri-id'])) { if (!empty($item['quote-uri-id']) && ($item['quote-uri-id'] != $item['uri-id'])) {
$shared = Post::selectFirst($fields, ['uri-id' => $item['quote-uri-id'], 'uid' => [0, $item['uid'] ?? 0]]); $shared = Post::selectFirst($fields, ['uri-id' => $item['quote-uri-id'], 'uid' => [0, $item['uid'] ?? 0]]);
if (is_array($shared)) { if (is_array($shared)) {
return [ return [
@ -770,7 +770,7 @@ class Item
return $attributes; return $attributes;
} }
if (!empty($item['quote-uri-id'])) { if (!empty($item['quote-uri-id']) && ($item['quote-uri-id'] != $item['uri-id'])) {
$shared = Post::selectFirst(['author-name', 'author-link', 'author-avatar', 'plink', 'created', 'guid', 'uri', 'body'], ['uri-id' => $item['quote-uri-id']]); $shared = Post::selectFirst(['author-name', 'author-link', 'author-avatar', 'plink', 'created', 'guid', 'uri', 'body'], ['uri-id' => $item['quote-uri-id']]);
if (!empty($shared)) { if (!empty($shared)) {
return [ return [

View file

@ -40,20 +40,21 @@ use Friendica\Network\HTTPException;
class Nav class Nav
{ {
private static $selected = [ private static $selected = [
'global' => null, 'global' => null,
'community' => null, 'community' => null,
'network' => null, 'channel' => null,
'home' => null, 'network' => null,
'profiles' => null, 'home' => null,
'profiles' => null,
'introductions' => null, 'introductions' => null,
'notifications' => null, 'notifications' => null,
'messages' => null, 'messages' => null,
'directory' => null, 'directory' => null,
'settings' => null, 'settings' => null,
'contacts' => null, 'contacts' => null,
'delegation'=> null, 'delegation' => null,
'calendar' => null, 'calendar' => null,
'register' => null 'register' => null
]; ];
/** /**
@ -199,6 +200,7 @@ class Nav
'moderation' => null, 'moderation' => null,
'apps' => null, 'apps' => null,
'community' => null, 'community' => null,
'channel' => null,
'home' => null, 'home' => null,
'calendar' => null, 'calendar' => null,
'login' => null, 'login' => null,

View file

@ -0,0 +1,57 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, 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\Content\Post\Collection;
use Friendica\BaseCollection;
use Friendica\Content\Post\Entity;
class PostMedias extends BaseCollection
{
/**
* @param Entity\PostMedia[] $entities
* @param int|null $totalCount
*/
public function __construct(array $entities = [], int $totalCount = null)
{
parent::__construct($entities, $totalCount);
}
/**
* @return Entity\PostMedia
*/
public function current(): Entity\PostMedia
{
return parent::current();
}
/**
* Determine whether all the collection's item have at least one set of dimensions provided
*
* @return bool
*/
public function haveDimensions(): bool
{
return array_reduce($this->getArrayCopy(), function (bool $carry, Entity\PostMedia $item) {
return $carry && $item->hasDimensions();
}, true);
}
}

View file

@ -0,0 +1,300 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, 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\Content\Post\Entity;
use Friendica\BaseEntity;
use Friendica\Network\Entity\MimeType;
use Friendica\Util\Images;
use Friendica\Util\Proxy;
use Psr\Http\Message\UriInterface;
/**
* @property-read int $id
* @property-read int $uriId
* @property-read ?int $activityUriId
* @property-read UriInterface $url
* @property-read int $type
* @property-read MimeType $mimetype
* @property-read ?int $width
* @property-read ?int $height
* @property-read ?int $size
* @property-read ?UriInterface $preview
* @property-read ?int $previewWidth
* @property-read ?int $previewHeight
* @property-read ?string $description
* @property-read ?string $name
* @property-read ?UriInterface $authorUrl
* @property-read ?string $authorName
* @property-read ?UriInterface $authorImage
* @property-read ?UriInterface $publisherUrl
* @property-read ?string $publisherName
* @property-read ?UriInterface $publisherImage
* @property-read ?string $blurhash
*/
class PostMedia extends BaseEntity
{
const TYPE_UNKNOWN = 0;
const TYPE_IMAGE = 1;
const TYPE_VIDEO = 2;
const TYPE_AUDIO = 3;
const TYPE_TEXT = 4;
const TYPE_APPLICATION = 5;
const TYPE_TORRENT = 16;
const TYPE_HTML = 17;
const TYPE_XML = 18;
const TYPE_PLAIN = 19;
const TYPE_ACTIVITY = 20;
const TYPE_ACCOUNT = 21;
const TYPE_DOCUMENT = 128;
/** @var int */
protected $id;
/** @var int */
protected $uriId;
/** @var UriInterface */
protected $url;
/** @var int One of TYPE_* */
protected $type;
/** @var MimeType */
protected $mimetype;
/** @var ?int */
protected $activityUriId;
/** @var ?int In pixels */
protected $width;
/** @var ?int In pixels */
protected $height;
/** @var ?int In bytes */
protected $size;
/** @var ?UriInterface Preview URL */
protected $preview;
/** @var ?int In pixels */
protected $previewWidth;
/** @var ?int In pixels */
protected $previewHeight;
/** @var ?string Alternative text like for images */
protected $description;
/** @var ?string */
protected $name;
/** @var ?UriInterface */
protected $authorUrl;
/** @var ?string */
protected $authorName;
/** @var ?UriInterface Image URL */
protected $authorImage;
/** @var ?UriInterface */
protected $publisherUrl;
/** @var ?string */
protected $publisherName;
/** @var ?UriInterface Image URL */
protected $publisherImage;
/** @var ?string Blurhash string representation for images
* @see https://github.com/woltapp/blurhash
* @see https://blurha.sh/
*/
protected $blurhash;
public function __construct(
int $uriId,
UriInterface $url,
int $type,
MimeType $mimetype,
?int $activityUriId,
?int $width = null,
?int $height = null,
?int $size = null,
?UriInterface $preview = null,
?int $previewWidth = null,
?int $previewHeight = null,
?string $description = null,
?string $name = null,
?UriInterface $authorUrl = null,
?string $authorName = null,
?UriInterface $authorImage = null,
?UriInterface $publisherUrl = null,
?string $publisherName = null,
?UriInterface $publisherImage = null,
?string $blurhash = null,
int $id = null
)
{
$this->uriId = $uriId;
$this->url = $url;
$this->type = $type;
$this->mimetype = $mimetype;
$this->activityUriId = $activityUriId;
$this->width = $width;
$this->height = $height;
$this->size = $size;
$this->preview = $preview;
$this->previewWidth = $previewWidth;
$this->previewHeight = $previewHeight;
$this->description = $description;
$this->name = $name;
$this->authorUrl = $authorUrl;
$this->authorName = $authorName;
$this->authorImage = $authorImage;
$this->publisherUrl = $publisherUrl;
$this->publisherName = $publisherName;
$this->publisherImage = $publisherImage;
$this->blurhash = $blurhash;
$this->id = $id;
}
/**
* Get media link for given media id
*
* @param string $size One of the Proxy::SIZE_* constants
* @return string media link
*/
public function getPhotoPath(string $size = ''): string
{
return '/photo/media/' .
(Proxy::getPixelsFromSize($size) ? Proxy::getPixelsFromSize($size) . '/' : '') .
$this->id;
}
/**
* Get preview path for given media id relative to the base URL
*
* @param string $size One of the Proxy::SIZE_* constants
* @return string preview link
*/
public function getPreviewPath(string $size = ''): string
{
return '/photo/preview/' .
(Proxy::getPixelsFromSize($size) ? Proxy::getPixelsFromSize($size) . '/' : '') .
$this->id;
}
/**
* Computes the allocated height value used in the content/image/single_with_height_allocation.tpl template
*
* Either base or preview dimensions need to be set at runtime.
*
* @return string
*/
public function getAllocatedHeight(): string
{
if (!$this->hasDimensions()) {
throw new \RangeException('Either width and height or previewWidth and previewHeight must be defined to use this method.');
}
if ($this->width && $this->height) {
$width = $this->width;
$height = $this->height;
} else {
$width = $this->previewWidth;
$height = $this->previewHeight;
}
return (100 * $height / $width) . '%';
}
/**
* Return a new PostMedia entity with a different preview URI and an optional proxy size name.
* The new entity preview's width and height are rescaled according to the provided size.
*
* @param \GuzzleHttp\Psr7\Uri $preview
* @param string $size
* @return $this
*/
public function withPreview(\GuzzleHttp\Psr7\Uri $preview, string $size = ''): self
{
if ($this->width || $this->height) {
$newWidth = $this->width;
$newHeight = $this->height;
} else {
$newWidth = $this->previewWidth;
$newHeight = $this->previewHeight;
}
if ($newWidth && $newHeight && $size) {
$dimensionts = Images::getScalingDimensions($newWidth, $newHeight, Proxy::getPixelsFromSize($size));
$newWidth = $dimensionts['width'];
$newHeight = $dimensionts['height'];
}
return new static(
$this->uriId,
$this->url,
$this->type,
$this->mimetype,
$this->activityUriId,
$this->width,
$this->height,
$this->size,
$preview,
$newWidth,
$newHeight,
$this->description,
$this->name,
$this->authorUrl,
$this->authorName,
$this->authorImage,
$this->publisherUrl,
$this->publisherName,
$this->publisherImage,
$this->blurhash,
$this->id,
);
}
public function withUrl(\GuzzleHttp\Psr7\Uri $url): self
{
return new static(
$this->uriId,
$url,
$this->type,
$this->mimetype,
$this->activityUriId,
$this->width,
$this->height,
$this->size,
$this->preview,
$this->previewWidth,
$this->previewHeight,
$this->description,
$this->name,
$this->authorUrl,
$this->authorName,
$this->authorImage,
$this->publisherUrl,
$this->publisherName,
$this->publisherImage,
$this->blurhash,
$this->id,
);
}
/**
* Checks the media has at least one full set of dimensions, needed for the height allocation feature
*
* @return bool
*/
public function hasDimensions(): bool
{
return $this->width && $this->height || $this->previewWidth && $this->previewHeight;
}
}

View file

@ -0,0 +1,117 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, 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\Content\Post\Factory;
use Friendica\BaseFactory;
use Friendica\Capabilities\ICanCreateFromTableRow;
use Friendica\Content\Post\Entity;
use Friendica\Network;
use GuzzleHttp\Psr7\Uri;
use Psr\Log\LoggerInterface;
use stdClass;
class PostMedia extends BaseFactory implements ICanCreateFromTableRow
{
/** @var Network\Factory\MimeType */
private $mimeTypeFactory;
public function __construct(Network\Factory\MimeType $mimeTypeFactory, LoggerInterface $logger)
{
parent::__construct($logger);
$this->mimeTypeFactory = $mimeTypeFactory;
}
/**
* @inheritDoc
*/
public function createFromTableRow(array $row)
{
return new Entity\PostMedia(
$row['uri-id'],
$row['url'] ? new Uri($row['url']) : null,
$row['type'],
$this->mimeTypeFactory->createFromContentType($row['mimetype']),
$row['media-uri-id'],
$row['width'],
$row['height'],
$row['size'],
$row['preview'] ? new Uri($row['preview']) : null,
$row['preview-width'],
$row['preview-height'],
$row['description'],
$row['name'],
$row['author-url'] ? new Uri($row['author-url']) : null,
$row['author-name'],
$row['author-image'] ? new Uri($row['author-image']) : null,
$row['publisher-url'] ? new Uri($row['publisher-url']) : null,
$row['publisher-name'],
$row['publisher-image'] ? new Uri($row['publisher-image']) : null,
$row['blurhash'],
$row['id']
);
}
public function createFromBlueskyImageEmbed(int $uriId, stdClass $image): Entity\PostMedia
{
return new Entity\PostMedia(
$uriId,
new Uri($image->fullsize),
Entity\PostMedia::TYPE_IMAGE,
new Network\Entity\MimeType('unkn', 'unkn'),
null,
null,
null,
null,
new Uri($image->thumb),
null,
null,
$image->alt,
);
}
public function createFromBlueskyExternalEmbed(int $uriId, stdClass $external): Entity\PostMedia
{
return new Entity\PostMedia(
$uriId,
new Uri($external->uri),
Entity\PostMedia::TYPE_HTML,
new Network\Entity\MimeType('text', 'html'),
null,
null,
null,
null,
null,
null,
null,
$external->description,
$external->title
);
}
public function createFromAttachment(int $uriId, array $attachment)
{
$attachment['uri-id'] = $uriId;
return $this->createFromTableRow($attachment);
}
}

View file

@ -0,0 +1,204 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, 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\Content\Post\Repository;
use Friendica\BaseCollection;
use Friendica\BaseRepository;
use Friendica\Content\Post\Collection;
use Friendica\Content\Post\Entity;
use Friendica\Content\Post\Factory;
use Friendica\Database\Database;
use Friendica\Util\Strings;
use Psr\Log\LoggerInterface;
class PostMedia extends BaseRepository
{
protected static $table_name = 'post-media';
public function __construct(Database $database, LoggerInterface $logger, Factory\PostMedia $factory)
{
parent::__construct($database, $logger, $factory);
}
protected function _select(array $condition, array $params = []): BaseCollection
{
$rows = $this->db->selectToArray(static::$table_name, [], $condition, $params);
$Entities = new Collection\PostMedias();
foreach ($rows as $fields) {
$Entities[] = $this->factory->createFromTableRow($fields);
}
return $Entities;
}
public function selectOneById(int $postMediaId): Entity\PostMedia
{
return $this->_selectOne(['id' => $postMediaId]);
}
public function selectByUriId(int $uriId): Collection\PostMedias
{
return $this->_select(['uri-id' => $uriId]);
}
public function save(Entity\PostMedia $PostMedia): Entity\PostMedia
{
$fields = [
'uri-id' => $PostMedia->uriId,
'url' => $PostMedia->url->__toString(),
'type' => $PostMedia->type,
'mimetype' => $PostMedia->mimetype->__toString(),
'height' => $PostMedia->height,
'width' => $PostMedia->width,
'size' => $PostMedia->size,
'preview' => $PostMedia->preview ? $PostMedia->preview->__toString() : null,
'preview-height' => $PostMedia->previewHeight,
'preview-width' => $PostMedia->previewWidth,
'description' => $PostMedia->description,
'name' => $PostMedia->name,
'author-url' => $PostMedia->authorUrl ? $PostMedia->authorUrl->__toString() : null,
'author-name' => $PostMedia->authorName,
'author-image' => $PostMedia->authorImage ? $PostMedia->authorImage->__toString() : null,
'publisher-url' => $PostMedia->publisherUrl ? $PostMedia->publisherUrl->__toString() : null,
'publisher-name' => $PostMedia->publisherName,
'publisher-image' => $PostMedia->publisherImage ? $PostMedia->publisherImage->__toString() : null,
'media-uri-id' => $PostMedia->activityUriId,
'blurhash' => $PostMedia->blurhash,
];
if ($PostMedia->id) {
$this->db->update(self::$table_name, $fields, ['id' => $PostMedia->id]);
} else {
$this->db->insert(self::$table_name, $fields, Database::INSERT_IGNORE);
$newPostMediaId = $this->db->lastInsertId();
$PostMedia = $this->selectOneById($newPostMediaId);
}
return $PostMedia;
}
/**
* Split the attachment media in the three segments "visual", "link" and "additional"
*
* @param int $uri_id URI id
* @param array $links list of links that shouldn't be added
* @param bool $has_media
* @return Collection\PostMedias[] Three collections in "visual", "link" and "additional" keys
*/
public function splitAttachments(int $uri_id, array $links = [], bool $has_media = true): array
{
$attachments = [
'visual' => new Collection\PostMedias(),
'link' => new Collection\PostMedias(),
'additional' => new Collection\PostMedias(),
];
if (!$has_media) {
return $attachments;
}
$PostMedias = $this->selectByUriId($uri_id);
if (!count($PostMedias)) {
return $attachments;
}
$heights = [];
$selected = '';
$previews = [];
foreach ($PostMedias as $PostMedia) {
foreach ($links as $link) {
if (Strings::compareLink($link, $PostMedia->url)) {
continue 2;
}
}
// Avoid adding separate media entries for previews
foreach ($previews as $preview) {
if (Strings::compareLink($preview, $PostMedia->url)) {
continue 2;
}
}
// Currently these two types are ignored here.
// Posts are added differently and contacts are not displayed as attachments.
if (in_array($PostMedia->type, [Entity\PostMedia::TYPE_ACCOUNT, Entity\PostMedia::TYPE_ACTIVITY])) {
continue;
}
if (!empty($PostMedia->preview)) {
$previews[] = $PostMedia->preview;
}
//$PostMedia->filetype = $filetype;
//$PostMedia->subtype = $subtype;
if ($PostMedia->type == Entity\PostMedia::TYPE_HTML || ($PostMedia->mimetype->type == 'text' && $PostMedia->mimetype->subtype == 'html')) {
$attachments['link'][] = $PostMedia;
continue;
}
if (
in_array($PostMedia->type, [Entity\PostMedia::TYPE_AUDIO, Entity\PostMedia::TYPE_IMAGE]) ||
in_array($PostMedia->mimetype->type, ['audio', 'image'])
) {
$attachments['visual'][] = $PostMedia;
} elseif (($PostMedia->type == Entity\PostMedia::TYPE_VIDEO) || ($PostMedia->mimetype->type == 'video')) {
if (!empty($PostMedia->height)) {
// Peertube videos are delivered in many different resolutions. We pick a moderate one.
// Since only Peertube provides a "height" parameter, this wouldn't be executed
// when someone for example on Mastodon was sharing multiple videos in a single post.
$heights[$PostMedia->height] = (string)$PostMedia->url;
$video[(string) $PostMedia->url] = $PostMedia;
} else {
$attachments['visual'][] = $PostMedia;
}
} else {
$attachments['additional'][] = $PostMedia;
}
}
if (!empty($heights)) {
ksort($heights);
foreach ($heights as $height => $url) {
if (empty($selected) || $height <= 480) {
$selected = $url;
}
}
if (!empty($selected)) {
$attachments['visual'][] = $video[$selected];
unset($video[$selected]);
foreach ($video as $element) {
$attachments['additional'][] = $element;
}
}
}
return $attachments;
}
}

View file

@ -230,18 +230,73 @@ class BBCode
{ {
DI::profiler()->startRecording('rendering'); DI::profiler()->startRecording('rendering');
// Remove pictures in advance to avoid unneeded proxy calls // Remove pictures in advance to avoid unneeded proxy calls
$text = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", ' ', $text);
$text = preg_replace("/\[img\=(.*?)\](.*?)\[\/img\]/ism", ' $2 ', $text); $text = preg_replace("/\[img\=(.*?)\](.*?)\[\/img\]/ism", ' $2 ', $text);
$text = preg_replace("/\[img.*?\[\/img\]/ism", ' ', $text); $text = preg_replace("/\[img.*?\[\/img\]/ism", ' ', $text);
// Remove attachment // Remove attachment
$text = self::replaceAttachment($text); $text = self::replaceAttachment($text);
$naked_text = HTML::toPlaintext(self::convert($text, false, BBCode::EXTERNAL, true), 0, !$keep_urls); $naked_text = HTML::toPlaintext(self::convert($text, false, self::EXTERNAL, true), 0, !$keep_urls);
DI::profiler()->stopRecording(); DI::profiler()->stopRecording();
return $naked_text; return $naked_text;
} }
/**
* Converts text into a format that can be used for the channel search and the language detection.
*
* @param string $text
* @param integer $uri_id
* @return string
*/
public static function toSearchText(string $text, int $uri_id): string
{
// Removes attachments
$text = self::removeAttachment($text);
// Add images because of possible alt texts
if (!empty($uri_id)) {
$text = Post\Media::addAttachmentsToBody($uri_id, $text, [Post\Media::IMAGE]);
}
if (empty($text)) {
return '';
}
// Remove links without a link description
$text = preg_replace("~\[url\=.*\]https?:.*\[\/url\]~", ' ', $text);
// Remove pictures
$text = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", ' ', $text);
// Replace picture with the alt description
$text = preg_replace("/\[img\=.*?\](.*?)\[\/img\]/ism", ' $1 ', $text);
// Remove the other pictures
$text = preg_replace("/\[img.*?\[\/img\]/ism", ' ', $text);
// Removes mentions, remove links from hashtags
$text = preg_replace('/[@!]\[url\=.*?\].*?\[\/url\]/ism', ' ', $text);
$text = preg_replace('/[#]\[url\=.*?\](.*?)\[\/url\]/ism', ' #$1 ', $text);
$text = preg_replace('/[@!#]?\[url.*?\[\/url\]/ism', ' ', $text);
$text = preg_replace("/\[url=[^\[\]]*\](.*)\[\/url\]/Usi", ' $1 ', $text);
// Convert it to plain text
$text = self::toPlaintext($text, false);
// Remove possibly remaining links
$text = preg_replace(Strings::autoLinkRegEx(), '', $text);
// Remove all unneeded white space
do {
$oldtext = $text;
$text = str_replace([' ', "\n", "\r", '"', '_'], ' ', $text);
} while ($oldtext != $text);
return trim($text);
}
private static function proxyUrl(string $image, int $simplehtml = self::INTERNAL, int $uriid = 0, string $size = ''): string private static function proxyUrl(string $image, int $simplehtml = self::INTERNAL, int $uriid = 0, string $size = ''): string
{ {
// Only send proxied pictures to API and for internal display // Only send proxied pictures to API and for internal display
@ -931,7 +986,7 @@ class BBCode
$network = $contact['network'] ?? Protocol::PHANTOM; $network = $contact['network'] ?? Protocol::PHANTOM;
$tpl = Renderer::getMarkupTemplate('shared_content.tpl'); $tpl = Renderer::getMarkupTemplate('shared_content.tpl');
$text .= BBCode::SHARED_ANCHOR . Renderer::replaceMacros($tpl, [ $text .= self::SHARED_ANCHOR . Renderer::replaceMacros($tpl, [
'$profile' => $attributes['profile'], '$profile' => $attributes['profile'],
'$avatar' => $attributes['avatar'], '$avatar' => $attributes['avatar'],
'$author' => $attributes['author'], '$author' => $attributes['author'],
@ -1112,6 +1167,7 @@ class BBCode
public static function removeLinks(string $bbcode): string public static function removeLinks(string $bbcode): string
{ {
DI::profiler()->startRecording('rendering'); DI::profiler()->startRecording('rendering');
$bbcode = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", ' ', $bbcode);
$bbcode = preg_replace("/\[img\=(.*?)\](.*?)\[\/img\]/ism", ' $1 ', $bbcode); $bbcode = preg_replace("/\[img\=(.*?)\](.*?)\[\/img\]/ism", ' $1 ', $bbcode);
$bbcode = preg_replace("/\[img.*?\[\/img\]/ism", ' ', $bbcode); $bbcode = preg_replace("/\[img.*?\[\/img\]/ism", ' ', $bbcode);
@ -1992,7 +2048,7 @@ class BBCode
{ {
DI::profiler()->startRecording('rendering'); DI::profiler()->startRecording('rendering');
$text = BBCode::performWithEscapedTags($text, ['code', 'noparse', 'nobb', 'pre'], function ($text) { $text = self::performWithEscapedTags($text, ['code', 'noparse', 'nobb', 'pre'], function ($text) {
$text = preg_replace("/[\s|\n]*\[abstract\].*?\[\/abstract\][\s|\n]*/ism", ' ', $text); $text = preg_replace("/[\s|\n]*\[abstract\].*?\[\/abstract\][\s|\n]*/ism", ' ', $text);
$text = preg_replace("/[\s|\n]*\[abstract=.*?\].*?\[\/abstract][\s|\n]*/ism", ' ', $text); $text = preg_replace("/[\s|\n]*\[abstract=.*?\].*?\[\/abstract][\s|\n]*/ism", ' ', $text);
return $text; return $text;
@ -2014,7 +2070,7 @@ class BBCode
DI::profiler()->startRecording('rendering'); DI::profiler()->startRecording('rendering');
$addon = strtolower($addon); $addon = strtolower($addon);
$abstract = BBCode::performWithEscapedTags($text, ['code', 'noparse', 'nobb', 'pre'], function ($text) use ($addon) { $abstract = self::performWithEscapedTags($text, ['code', 'noparse', 'nobb', 'pre'], function ($text) use ($addon) {
if ($addon && preg_match('#\[abstract=' . preg_quote($addon, '#') . '](.*?)\[/abstract]#ism', $text, $matches)) { if ($addon && preg_match('#\[abstract=' . preg_quote($addon, '#') . '](.*?)\[/abstract]#ism', $text, $matches)) {
return $matches[1]; return $matches[1];
} }

View file

@ -324,7 +324,7 @@ class Plaintext
$post['text'] = Post\Media::removeFromBody($post['text']); $post['text'] = Post\Media::removeFromBody($post['text']);
$images = Post\Media::getByURIId($item['uri-id'], [Post\Media::IMAGE]); $images = Post\Media::getByURIId($item['uri-id'], [Post\Media::IMAGE]);
if (!empty($item['quote-uri-id'])) { if (!empty($item['quote-uri-id']) && ($item['quote-uri-id'] != $item['uri-id'])) {
$images = array_merge($images, Post\Media::getByURIId($item['quote-uri-id'], [Post\Media::IMAGE])); $images = array_merge($images, Post\Media::getByURIId($item['quote-uri-id'], [Post\Media::IMAGE]));
} }
foreach ($images as $image) { foreach ($images as $image) {
@ -355,7 +355,7 @@ class Plaintext
// Look for audio or video links // Look for audio or video links
$media = Post\Media::getByURIId($item['uri-id'], [Post\Media::AUDIO, Post\Media::VIDEO]); $media = Post\Media::getByURIId($item['uri-id'], [Post\Media::AUDIO, Post\Media::VIDEO]);
if (!empty($item['quote-uri-id'])) { if (!empty($item['quote-uri-id']) && ($item['quote-uri-id'] != $item['uri-id'])) {
$media = array_merge($media, Post\Media::getByURIId($item['quote-uri-id'], [Post\Media::AUDIO, Post\Media::VIDEO])); $media = array_merge($media, Post\Media::getByURIId($item['quote-uri-id'], [Post\Media::AUDIO, Post\Media::VIDEO]));
} }

View file

@ -547,4 +547,53 @@ class Widget
$accounttype $accounttype
); );
} }
/**
* Get a list of all channels
*
* @param string $base
* @param string $channelname
* @param integer $uid
* @return string
*/
public static function channels(string $base, string $channelname, int $uid): string
{
$channels = [];
$enabled = DI::pConfig()->get($uid, 'system', 'enabled_timelines', []);
foreach (DI::NetworkFactory()->getTimelines('') as $channel) {
if (empty($enabled) || in_array($channel->code, $enabled)) {
$channels[] = ['ref' => $channel->code, 'name' => $channel->label];
}
}
foreach (DI::ChannelFactory()->getTimelines($uid) as $channel) {
if (empty($enabled) || in_array($channel->code, $enabled)) {
$channels[] = ['ref' => $channel->code, 'name' => $channel->label];
}
}
foreach (DI::userDefinedChannel()->selectByUid($uid) as $channel) {
if (empty($enabled) || in_array($channel->code, $enabled)) {
$channels[] = ['ref' => $channel->code, 'name' => $channel->label];
}
}
foreach (DI::CommunityFactory()->getTimelines(true) as $community) {
if (empty($enabled) || in_array($community->code, $enabled)) {
$channels[] = ['ref' => $community->code, 'name' => $community->label];
}
}
return self::filter(
'channel',
DI::l10n()->t('Channels'),
'',
'',
$base,
$channels,
$channelname
);
}
} }

View file

@ -68,6 +68,9 @@ class VCard
$follow_link = ''; $follow_link = '';
$unfollow_link = ''; $unfollow_link = '';
$wallmessage_link = ''; $wallmessage_link = '';
$mention_label = '';
$mention_link = '';
$showgroup_link = '';
$photo = Contact::getPhoto($contact); $photo = Contact::getPhoto($contact);
@ -99,6 +102,15 @@ class VCard
if (in_array($rel, [Contact::FOLLOWER, Contact::FRIEND]) && Contact::canReceivePrivateMessages($contact)) { if (in_array($rel, [Contact::FOLLOWER, Contact::FRIEND]) && Contact::canReceivePrivateMessages($contact)) {
$wallmessage_link = 'message/new/' . $id; $wallmessage_link = 'message/new/' . $id;
} }
if ($contact['contact-type'] == Contact::TYPE_COMMUNITY) {
$mention_label = DI::l10n()->t('Post to group');
$mention_link = 'compose/0?body=!' . $contact['addr'];
$showgroup_link = 'network/group/' . $id;
} else {
$mention_label = DI::l10n()->t('Mention');
$mention_link = 'compose/0?body=@' . $contact['addr'];
}
} }
return Renderer::replaceMacros(Renderer::getMarkupTemplate('widget/vcard.tpl'), [ return Renderer::replaceMacros(Renderer::getMarkupTemplate('widget/vcard.tpl'), [
@ -119,6 +131,10 @@ class VCard
'$unfollow_link' => $unfollow_link, '$unfollow_link' => $unfollow_link,
'$wallmessage' => DI::l10n()->t('Message'), '$wallmessage' => DI::l10n()->t('Message'),
'$wallmessage_link' => $wallmessage_link, '$wallmessage_link' => $wallmessage_link,
'$mention' => $mention_label,
'$mention_link' => $mention_link,
'$showgroup' => DI::l10n()->t('View group'),
'$showgroup_link' => $showgroup_link,
]); ]);
} }
} }

View file

@ -384,12 +384,10 @@ class Installer
$help = ''; $help = '';
$status = true; $status = true;
if (function_exists('apache_get_modules')) { if (function_exists('apache_get_modules') && !in_array('mod_rewrite', apache_get_modules())) {
if (!in_array('mod_rewrite', apache_get_modules())) { $help = DI::l10n()->t('Error: Apache webserver mod-rewrite module is required but not installed.');
$help = DI::l10n()->t('Error: Apache webserver mod-rewrite module is required but not installed.'); $status = false;
$status = false; $returnVal = false;
$returnVal = false;
}
} }
$this->addCheck(DI::l10n()->t('Apache mod_rewrite module'), $status, true, $help); $this->addCheck(DI::l10n()->t('Apache mod_rewrite module'), $status, true, $help);
@ -399,15 +397,25 @@ class Installer
$status = false; $status = false;
$help = DI::l10n()->t('Error: PDO or MySQLi PHP module required but not installed.'); $help = DI::l10n()->t('Error: PDO or MySQLi PHP module required but not installed.');
$returnVal = false; $returnVal = false;
} else { } elseif (!function_exists('mysqli_connect') && class_exists('pdo') && !in_array('mysql', \PDO::getAvailableDrivers())) {
if (!function_exists('mysqli_connect') && class_exists('pdo') && !in_array('mysql', \PDO::getAvailableDrivers())) { $status = false;
$status = false; $help = DI::l10n()->t('Error: The MySQL driver for PDO is not installed.');
$help = DI::l10n()->t('Error: The MySQL driver for PDO is not installed.'); $returnVal = false;
$returnVal = false;
}
} }
$this->addCheck(DI::l10n()->t('PDO or MySQLi PHP module'), $status, true, $help); $this->addCheck(DI::l10n()->t('PDO or MySQLi PHP module'), $status, true, $help);
// Uncomment when IntlChar is installed in the check pipeline.
/*
$help = '';
$status = true;
if (!class_exists('IntlChar')) {
$status = false;
$help = DI::l10n()->t('Error: The IntlChar module is not installed.');
$returnVal = false;
}
$this->addCheck(DI::l10n()->t('IntlChar PHP module'), $status, true, $help);
*/
// check for XML DOM Documents being able to be generated // check for XML DOM Documents being able to be generated
$help = ''; $help = '';
$status = true; $status = true;

View file

@ -378,7 +378,7 @@ class L10n
* *
* @return array * @return array
*/ */
public static function getAvailableLanguages(): array public function getAvailableLanguages(bool $additional = false): array
{ {
$langs = []; $langs = [];
$strings_file_paths = glob('view/lang/*/strings.php'); $strings_file_paths = glob('view/lang/*/strings.php');
@ -392,10 +392,109 @@ class L10n
$path_array = explode('/', $strings_file_path); $path_array = explode('/', $strings_file_path);
$langs[$path_array[2]] = self::LANG_NAMES[$path_array[2]] ?? $path_array[2]; $langs[$path_array[2]] = self::LANG_NAMES[$path_array[2]] ?? $path_array[2];
} }
if ($additional) {
// See https://github.com/friendica/friendica/issues/10511
// Persian is manually added to language detection until a persian translation is provided for the interface, at
// which point it will be automatically available through `getAvailableLanguages()` and this should be removed.
// Additionally some more languages are added to that list that are used in the Fediverse.
$additional_langs = [
'af' => 'Afrikaans',
'az-Latn' => 'azərbaycan dili',
'bs-Latn' => 'bosanski jezik',
'be' => 'беларуская мова',
'bn' => 'বাংলা',
'cy' => 'Cymraeg',
'el-monoton' => 'ελληνικά',
'eu' => 'euskara, euskera',
'fa' => 'فارسی',
'ga' => 'Gaeilge',
'gl' => 'galego',
'he' => 'עברית',
'hi' => 'हिन्दी, हिंदी',
'hr' => 'hrvatski jezik',
'hy' => 'Հայերեն',
'id' => 'Bahasa Indonesia',
'jv' => 'basa Jawa',
'ka' => 'ქართული',
'ko' => '한국어, 조선어',
'lt' => 'lietuvių kalba',
'lv' => 'latviešu valoda',
'ms-Latn' => 'bahasa Melayu, بهاس ملايو‎',
'sr-Cyrl' => 'српски језик',
'sk' => 'slovenčina, slovenský jazyk',
'sl' => 'slovenski jezik, slovenščina',
'sq' => 'Shqip',
'sw' => 'Kiswahili',
'ta' => 'தமிழ்',
'th' => 'ไทย',
'tl' => 'Wikang Tagalog, ᜏᜒᜃᜅ᜔ ᜆᜄᜎᜓᜄ᜔',
'tr' => 'Türkçe',
'pt-PT' => 'português',
'uk' => 'українська мова',
'uz' => 'Oʻzbek, Ўзбек, أۇزبېك‎',
'vi' => 'Việt Nam',
'zh-hant' => '繁體',
];
$langs = array_merge($additional_langs, $langs);
ksort($langs);
}
} }
return $langs; return $langs;
} }
/**
* The language detection routine uses some slightly different language codes.
* This function changes the language array accordingly.
*
* @param array $languages
* @return array
*/
public function convertForLanguageDetection(array $languages): array
{
foreach ($languages as $key => $language) {
$newkey = $this->convertCodeForLanguageDetection($key);
if ($newkey != $key) {
if (!isset($languages[$newkey])) {
$languages[$newkey] = $language;
}
unset($languages[$key]);
}
}
ksort($languages);
return $languages;
}
/**
* The language detection routine uses some slightly different language codes.
* This function changes the language codes accordingly.
*
* @param string $language
* @return string
*/
public function convertCodeForLanguageDetection(string $language): string
{
switch ($language) {
case 'da-dk':
return 'da';
case 'en-us':
case 'en-gb':
return 'en';
case 'fi-fi':
return 'fi';
case 'nb-no':
return 'nb';
case 'pt-br':
return 'pt-BR';
case 'zh-cn':
return 'zh-Hans';
default:
return $language;
}
}
/** /**
* Translate days and months names. * Translate days and months names.
* *

View file

@ -23,6 +23,7 @@ namespace Friendica\Core\Logger\Util;
use Friendica\App\Request; use Friendica\App\Request;
use Friendica\Core\Logger\Capability\IHaveCallIntrospections; use Friendica\Core\Logger\Capability\IHaveCallIntrospections;
use Friendica\Core\System;
/** /**
* Get Introspection information about the current call * Get Introspection information about the current call
@ -86,6 +87,7 @@ class Introspection implements IHaveCallIntrospections
'line' => $trace[$i - 1]['line'] ?? null, 'line' => $trace[$i - 1]['line'] ?? null,
'function' => $trace[$i]['function'] ?? null, 'function' => $trace[$i]['function'] ?? null,
'request-id' => $this->requestId, 'request-id' => $this->requestId,
'stack' => System::callstack(10, 4),
]; ];
} }

View file

@ -28,10 +28,12 @@ use Friendica\DI;
use Friendica\Model\User; use Friendica\Model\User;
use Friendica\Module\Response; use Friendica\Module\Response;
use Friendica\Network\HTTPException\FoundException; use Friendica\Network\HTTPException\FoundException;
use Friendica\Network\HTTPException\InternalServerErrorException;
use Friendica\Network\HTTPException\MovedPermanentlyException; use Friendica\Network\HTTPException\MovedPermanentlyException;
use Friendica\Network\HTTPException\TemporaryRedirectException; use Friendica\Network\HTTPException\TemporaryRedirectException;
use Friendica\Util\BasePath; use Friendica\Util\BasePath;
use Friendica\Util\XML; use Friendica\Util\XML;
use Psr\Http\Message\ResponseInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
/** /**
@ -274,32 +276,59 @@ class System
return implode(', ', $callstack2); return implode(', ', $callstack2);
} }
/**
* Display current response, including setting all headers
*
* @param ResponseInterface $response
*/
public static function echoResponse(ResponseInterface $response)
{
header(sprintf("HTTP/%s %s %s",
$response->getProtocolVersion(),
$response->getStatusCode(),
$response->getReasonPhrase())
);
foreach ($response->getHeaders() as $key => $header) {
if (is_array($header)) {
$header_str = implode(',', $header);
} else {
$header_str = $header;
}
if (is_int($key)) {
header($header_str);
} else {
header("$key: $header_str");
}
}
echo $response->getBody();
}
/** /**
* Generic XML return * Generic XML return
* Outputs a basic dfrn XML status structure to STDOUT, with a <status> variable * Outputs a basic dfrn XML status structure to STDOUT, with a <status> variable
* of $st and an optional text <message> of $message and terminates the current process. * of $st and an optional text <message> of $message and terminates the current process.
* *
* @param $st * @param mixed $status
* @param string $message * @param string $message
* @throws \Exception * @throws \Exception
* @deprecated since 2023.09 Use BaseModule->httpExit() instead
*/ */
public static function xmlExit($st, $message = '') public static function xmlExit($status, string $message = '')
{ {
$result = ['status' => $st]; $result = ['status' => $status];
if ($message != '') { if ($message != '') {
$result['message'] = $message; $result['message'] = $message;
} }
if ($st) { if ($status) {
Logger::notice('xml_status returning non_zero: ' . $st . " message=" . $message); Logger::notice('xml_status returning non_zero: ' . $status . " message=" . $message);
} }
DI::apiResponse()->setType(Response::TYPE_XML); self::httpExit(XML::fromArray(['result' => $result]), Response::TYPE_XML);
DI::apiResponse()->addContent(XML::fromArray(['result' => $result]));
DI::page()->exit(DI::apiResponse()->generate());
self::exit();
} }
/** /**
@ -309,6 +338,7 @@ class System
* @param string $message Error message. Optional. * @param string $message Error message. Optional.
* @param string $content Response body. Optional. * @param string $content Response body. Optional.
* @throws \Exception * @throws \Exception
* @deprecated since 2023.09 Use BaseModule->httpError instead
*/ */
public static function httpError($httpCode, $message = '', $content = '') public static function httpError($httpCode, $message = '', $content = '')
{ {
@ -316,29 +346,33 @@ class System
Logger::debug('Exit with error', ['code' => $httpCode, 'message' => $message, 'callstack' => System::callstack(20), 'method' => DI::args()->getMethod(), 'agent' => $_SERVER['HTTP_USER_AGENT'] ?? '']); Logger::debug('Exit with error', ['code' => $httpCode, 'message' => $message, 'callstack' => System::callstack(20), 'method' => DI::args()->getMethod(), 'agent' => $_SERVER['HTTP_USER_AGENT'] ?? '']);
} }
DI::apiResponse()->setStatus($httpCode, $message); DI::apiResponse()->setStatus($httpCode, $message);
DI::apiResponse()->addContent($content);
DI::page()->exit(DI::apiResponse()->generate());
self::exit(); self::httpExit($content);
} }
/** /**
* This function adds the content and a content-type HTTP header to the output. * This function adds the content and a content-type HTTP header to the output.
* After finishing the process is getting killed. * After finishing the process is getting killed.
* *
* @param string $content * @param string $content
* @param string $type * @param string $type
* @param string|null $content_type * @param string|null $content_type
* @return void * @return void
* @throws InternalServerErrorException
* @deprecated since 2023.09 Use BaseModule->httpExit() instead
*/ */
public static function httpExit(string $content, string $type = Response::TYPE_HTML, ?string $content_type = null) { public static function httpExit(string $content, string $type = Response::TYPE_HTML, ?string $content_type = null)
{
DI::apiResponse()->setType($type, $content_type); DI::apiResponse()->setType($type, $content_type);
DI::apiResponse()->addContent($content); DI::apiResponse()->addContent($content);
DI::page()->exit(DI::apiResponse()->generate()); self::echoResponse(DI::apiResponse()->generate());
self::exit(); self::exit();
} }
/**
* @deprecated since 2023.09 Use BaseModule->jsonError instead
*/
public static function jsonError($httpCode, $content, $content_type = 'application/json') public static function jsonError($httpCode, $content, $content_type = 'application/json')
{ {
if ($httpCode >= 400) { if ($httpCode >= 400) {
@ -358,14 +392,12 @@ class System
* @param mixed $content The input content * @param mixed $content The input content
* @param string $content_type Type of the input (Default: 'application/json') * @param string $content_type Type of the input (Default: 'application/json')
* @param integer $options JSON options * @param integer $options JSON options
* @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws InternalServerErrorException
* @deprecated since 2023.09 Use BaseModule->jsonExit instead
*/ */
public static function jsonExit($content, $content_type = 'application/json', int $options = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) { public static function jsonExit($content, string $content_type = 'application/json', int $options = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)
DI::apiResponse()->setType(Response::TYPE_JSON, $content_type); {
DI::apiResponse()->addContent(json_encode($content, $options)); self::httpExit(json_encode($content, $options), Response::TYPE_JSON, $content_type);
DI::page()->exit(DI::apiResponse()->generate());
self::exit();
} }
/** /**

View file

@ -601,7 +601,7 @@ class Worker
$rest = round(max(0, $up_duration - (self::$db_duration + self::$lock_duration)), 2); $rest = round(max(0, $up_duration - (self::$db_duration + self::$lock_duration)), 2);
$exec = round($duration, 2); $exec = round($duration, 2);
Logger::info('Performance:', ['state' => self::$state, 'count' => $dbcount, 'stat' => $dbstat, 'write' => $dbwrite, 'lock' => $dblock, 'total' => $dbtotal, 'rest' => $rest, 'exec' => $exec]); Logger::info('Performance:', ['function' => $funcname, 'state' => self::$state, 'count' => $dbcount, 'stat' => $dbstat, 'write' => $dbwrite, 'lock' => $dblock, 'total' => $dbtotal, 'rest' => $rest, 'exec' => $exec]);
self::coolDown(); self::coolDown();
@ -622,7 +622,7 @@ class Worker
Logger::info('Longer than 2 minutes.', ['priority' => $queue['priority'], 'id' => $queue['id'], 'duration' => round($duration/60, 3)]); Logger::info('Longer than 2 minutes.', ['priority' => $queue['priority'], 'id' => $queue['id'], 'duration' => round($duration/60, 3)]);
} }
Logger::info('Process done.', ['priority' => $queue['priority'], 'id' => $queue['id'], 'duration' => round($duration, 3)]); Logger::info('Process done.', ['function' => $funcname, 'priority' => $queue['priority'], 'retrial' => $queue['retrial'], 'id' => $queue['id'], 'duration' => round($duration, 3)]);
DI::profiler()->saveLog(DI::logger(), 'ID ' . $queue['id'] . ': ' . $funcname); DI::profiler()->saveLog(DI::logger(), 'ID ' . $queue['id'] . ': ' . $funcname);
} }

View file

@ -547,6 +547,43 @@ abstract class DI
return self::$dice->create(Contact\FriendSuggest\Factory\FriendSuggest::class); return self::$dice->create(Contact\FriendSuggest\Factory\FriendSuggest::class);
} }
/**
* @return Content\Conversation\Factory\Timeline
*/
public static function TimelineFactory()
{
return self::$dice->create(Content\Conversation\Factory\Timeline::class);
}
/**
* @return Content\Conversation\Factory\Community
*/
public static function CommunityFactory()
{
return self::$dice->create(Content\Conversation\Factory\Community::class);
}
/**
* @return Content\Conversation\Factory\Channel
*/
public static function ChannelFactory()
{
return self::$dice->create(Content\Conversation\Factory\Channel::class);
}
public static function userDefinedChannel(): Content\Conversation\Repository\UserDefinedChannel
{
return self::$dice->create(Content\Conversation\Repository\UserDefinedChannel::class);
}
/**
* @return Content\Conversation\Factory\Network
*/
public static function NetworkFactory()
{
return self::$dice->create(Content\Conversation\Factory\Network::class);
}
/** /**
* @return Contact\Introduction\Repository\Introduction * @return Contact\Introduction\Repository\Introduction
*/ */
@ -723,4 +760,9 @@ abstract class DI
{ {
return self::$dice->create(Util\Emailer::class); return self::$dice->create(Util\Emailer::class);
} }
public static function postMediaRepository(): Content\Post\Repository\PostMedia
{
return self::$dice->create(Content\Post\Repository\PostMedia::class);
}
} }

View file

@ -42,6 +42,11 @@ class DBA
*/ */
const NULL_DATETIME = '0001-01-01 00:00:00'; const NULL_DATETIME = '0001-01-01 00:00:00';
/**
* Lowest possible datetime(6) value
*/
const NULL_DATETIME6 = '0001-01-01 00:00:00.000000';
public static function connect(): bool public static function connect(): bool
{ {
return DI::dba()->connect(); return DI::dba()->connect();

View file

@ -57,7 +57,7 @@ class Error extends BaseFactory
$errorObj = new \Friendica\Object\Api\Mastodon\Error($error, $error_description); $errorObj = new \Friendica\Object\Api\Mastodon\Error($error, $error_description);
$this->logError(404, $error); $this->logError(404, $error);
System::jsonError(404, $errorObj->toArray()); $this->jsonError(404, $errorObj->toArray());
} }
public function UnprocessableEntity(string $error = '') public function UnprocessableEntity(string $error = '')
@ -67,7 +67,7 @@ class Error extends BaseFactory
$errorObj = new \Friendica\Object\Api\Mastodon\Error($error, $error_description); $errorObj = new \Friendica\Object\Api\Mastodon\Error($error, $error_description);
$this->logError(422, $error); $this->logError(422, $error);
System::jsonError(422, $errorObj->toArray()); $this->jsonError(422, $errorObj->toArray());
} }
public function Unauthorized(string $error = '', string $error_description = '') public function Unauthorized(string $error = '', string $error_description = '')
@ -76,7 +76,7 @@ class Error extends BaseFactory
$errorObj = new \Friendica\Object\Api\Mastodon\Error($error, $error_description); $errorObj = new \Friendica\Object\Api\Mastodon\Error($error, $error_description);
$this->logError(401, $error); $this->logError(401, $error);
System::jsonError(401, $errorObj->toArray()); $this->jsonError(401, $errorObj->toArray());
} }
public function Forbidden(string $error = '') public function Forbidden(string $error = '')
@ -86,7 +86,7 @@ class Error extends BaseFactory
$errorObj = new \Friendica\Object\Api\Mastodon\Error($error, $error_description); $errorObj = new \Friendica\Object\Api\Mastodon\Error($error, $error_description);
$this->logError(403, $error); $this->logError(403, $error);
System::jsonError(403, $errorObj->toArray()); $this->jsonError(403, $errorObj->toArray());
} }
public function InternalError(string $error = '') public function InternalError(string $error = '')
@ -96,6 +96,6 @@ class Error extends BaseFactory
$errorObj = new \Friendica\Object\Api\Mastodon\Error($error, $error_description); $errorObj = new \Friendica\Object\Api\Mastodon\Error($error, $error_description);
$this->logError(500, $error); $this->logError(500, $error);
System::jsonError(500, $errorObj->toArray()); $this->jsonError(500, $errorObj->toArray());
} }
} }

View file

@ -157,7 +157,10 @@ class APContact
$apcontact = []; $apcontact = [];
$webfinger = empty(parse_url($url, PHP_URL_SCHEME)); // Mastodon profile short-form URL https://domain.tld/@user doesn't return AP data when queried
// with HTTPSignature::fetchRaw, but returns the correct data when provided to WebFinger
// @see https://github.com/friendica/friendica/issues/13359
$webfinger = empty(parse_url($url, PHP_URL_SCHEME)) || strpos($url, '@') !== false;
if ($webfinger) { if ($webfinger) {
$apcontact = self::fetchWebfingerData($url); $apcontact = self::fetchWebfingerData($url);
if (empty($apcontact['url'])) { if (empty($apcontact['url'])) {
@ -197,7 +200,7 @@ class APContact
$curlResult = HTTPSignature::fetchRaw($url); $curlResult = HTTPSignature::fetchRaw($url);
$failed = empty($curlResult) || empty($curlResult->getBody()) || $failed = empty($curlResult) || empty($curlResult->getBody()) ||
(!$curlResult->isSuccess() && ($curlResult->getReturnCode() != 410)); (!$curlResult->isSuccess() && ($curlResult->getReturnCode() != 410));
if (!$failed) { if (!$failed) {
$data = json_decode($curlResult->getBody(), true); $data = json_decode($curlResult->getBody(), true);
$failed = empty($data) || !is_array($data); $failed = empty($data) || !is_array($data);

View file

@ -169,16 +169,17 @@ class Circle
* *
* Count unread items of each circle of the local user * Count unread items of each circle of the local user
* *
* @param int $uid
* @return array * @return array
* 'id' => circle id * 'id' => circle id
* 'name' => circle name * 'name' => circle name
* 'count' => counted unseen circle items * 'count' => counted unseen circle items
* @throws \Exception * @throws \Exception
*/ */
public static function countUnseen() public static function countUnseen(int $uid)
{ {
$stmt = DBA::p("SELECT `circle`.`id`, `circle`.`name`, $stmt = DBA::p("SELECT `circle`.`id`, `circle`.`name`,
(SELECT COUNT(*) FROM `post-user-view` (SELECT COUNT(*) FROM `post-user`
WHERE `uid` = ? WHERE `uid` = ?
AND `unseen` AND `unseen`
AND `contact-id` IN AND `contact-id` IN
@ -188,8 +189,8 @@ class Circle
) AS `count` ) AS `count`
FROM `group` AS `circle` FROM `group` AS `circle`
WHERE `circle`.`uid` = ?;", WHERE `circle`.`uid` = ?;",
DI::userSession()->getLocalUserId(), $uid,
DI::userSession()->getLocalUserId() $uid
); );
return DBA::toArray($stmt); return DBA::toArray($stmt);

View file

@ -222,6 +222,11 @@ class Contact
Contact\User::insertForContactArray($contact); Contact\User::insertForContactArray($contact);
if ((empty($contact['baseurl']) || empty($contact['gsid'])) && Probe::isProbable($contact['network'])) {
Logger::debug('Update missing baseurl', ['id' => $contact['id'], 'url' => $contact['url'], 'callstack' => System::callstack(4, 0, true)]);
UpdateContact::add(['priority' => Worker::PRIORITY_MEDIUM, 'dont_fork' => true], $contact['id']);
}
return $contact['id']; return $contact['id'];
} }
@ -528,6 +533,17 @@ class Contact
return self::isSharing($cid, $uid, $strict); return self::isSharing($cid, $uid, $strict);
} }
/**
* Checks if the provided public contact id has got followers on this system
*
* @param integer $cid
* @return boolean
*/
public static function hasFollowers(int $cid): bool
{
return DBA::exists('account-user-view', ["`pid` = ? AND `uid` != ? AND `rel` IN (?, ?)", $cid, 0, self::SHARING, self::FRIEND]);
}
/** /**
* Get the basepath for a given contact link * Get the basepath for a given contact link
* *
@ -734,7 +750,7 @@ class Contact
$user = DBA::selectFirst( $user = DBA::selectFirst(
'user', 'user',
['uid', 'username', 'nickname', 'pubkey', 'prvkey'], ['uid', 'username', 'nickname', 'pubkey', 'prvkey'],
['uid' => $uid, 'account_expired' => false] ['uid' => $uid, 'verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => false]
); );
if (!DBA::isResult($user)) { if (!DBA::isResult($user)) {
return false; return false;
@ -806,7 +822,7 @@ class Contact
} }
$fields = ['uid', 'username', 'nickname', 'page-flags', 'account-type', 'prvkey', 'pubkey']; $fields = ['uid', 'username', 'nickname', 'page-flags', 'account-type', 'prvkey', 'pubkey'];
$user = DBA::selectFirst('user', $fields, ['uid' => $uid, 'account_expired' => false]); $user = DBA::selectFirst('user', $fields, ['uid' => $uid, 'verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => false]);
if (!DBA::isResult($user)) { if (!DBA::isResult($user)) {
return false; return false;
} }
@ -1161,6 +1177,7 @@ class Contact
} }
$pm_url = ''; $pm_url = '';
$mention_url = '';
$status_link = ''; $status_link = '';
$photos_link = ''; $photos_link = '';
@ -1182,7 +1199,18 @@ class Contact
} }
$contact_url = 'contact/' . $contact['id']; $contact_url = 'contact/' . $contact['id'];
$posts_link = 'contact/' . $contact['id'] . '/conversations';
if ($contact['contact-type'] == Contact::TYPE_COMMUNITY) {
$mention_label = DI::l10n()->t('Post to group');
$mention_url = 'compose/0?body=!' . $contact['addr'];
$network_label = DI::l10n()->t('View group');
$network_url = 'network/group/' . $contact['id'];
} else {
$mention_label = DI::l10n()->t('Mention');
$mention_url = 'compose/0?body=@' . $contact['addr'];
$network_label = DI::l10n()->t('Network Posts');
$network_url = 'contact/' . $contact['id'] . '/conversations';
}
$follow_link = ''; $follow_link = '';
$unfollow_link = ''; $unfollow_link = '';
@ -1198,24 +1226,28 @@ class Contact
* Menu array: * Menu array:
* "name" => [ "Label", "link", (bool)Should the link opened in a new tab? ] * "name" => [ "Label", "link", (bool)Should the link opened in a new tab? ]
*/ */
if (empty($contact['uid'])) { if (empty($contact['uid'])) {
$menu = [ $menu = [
'profile' => [DI::l10n()->t('View Profile'), $profile_link, true], 'profile' => [DI::l10n()->t('View Profile'), $profile_link, true],
'network' => [DI::l10n()->t('Network Posts'), $posts_link, false], 'network' => [$network_label, $network_url, false],
'edit' => [DI::l10n()->t('View Contact'), $contact_url, false], 'edit' => [DI::l10n()->t('View Contact'), $contact_url, false],
'follow' => [DI::l10n()->t('Connect/Follow'), $follow_link, true], 'follow' => [DI::l10n()->t('Connect/Follow'), $follow_link, true],
'unfollow' => [DI::l10n()->t('Unfollow'), $unfollow_link, true], 'unfollow' => [DI::l10n()->t('Unfollow'), $unfollow_link, true],
'mention' => [$mention_label, $mention_url, false],
]; ];
} else { } else {
$menu = [ $menu = [
'status' => [DI::l10n()->t('View Status'), $status_link, true], 'status' => [DI::l10n()->t('View Status'), $status_link, true],
'profile' => [DI::l10n()->t('View Profile'), $profile_link, true], 'profile' => [DI::l10n()->t('View Profile'), $profile_link, true],
'photos' => [DI::l10n()->t('View Photos'), $photos_link, true], 'photos' => [DI::l10n()->t('View Photos'), $photos_link, true],
'network' => [DI::l10n()->t('Network Posts'), $posts_link, false], 'network' => [$network_label, $network_url, false],
'edit' => [DI::l10n()->t('View Contact'), $contact_url, false], 'edit' => [DI::l10n()->t('View Contact'), $contact_url, false],
'pm' => [DI::l10n()->t('Send PM'), $pm_url, false], 'pm' => [DI::l10n()->t('Send PM'), $pm_url, false],
'follow' => [DI::l10n()->t('Connect/Follow'), $follow_link, true], 'follow' => [DI::l10n()->t('Connect/Follow'), $follow_link, true],
'unfollow' => [DI::l10n()->t('Unfollow'), $unfollow_link, true], 'unfollow' => [DI::l10n()->t('Unfollow'), $unfollow_link, true],
'mention' => [$mention_label, $mention_url, false],
]; ];
if (!empty($contact['pending'])) { if (!empty($contact['pending'])) {
@ -1372,6 +1404,7 @@ class Contact
$fields = [ $fields = [
'uid' => $uid, 'uid' => $uid,
'url' => $data['url'], 'url' => $data['url'],
'baseurl' => $data['baseurl'] ?? '',
'nurl' => Strings::normaliseLink($data['url']), 'nurl' => Strings::normaliseLink($data['url']),
'network' => $data['network'], 'network' => $data['network'],
'created' => DateTimeFormat::utcNow(), 'created' => DateTimeFormat::utcNow(),
@ -3181,7 +3214,7 @@ class Contact
return false; return false;
} }
$fields = ['id', 'url', 'name', 'nick', 'avatar', 'photo', 'network', 'blocked']; $fields = ['id', 'url', 'name', 'nick', 'avatar', 'photo', 'network', 'blocked', 'baseurl'];
$pub_contact = DBA::selectFirst('contact', $fields, ['id' => $datarray['author-id']]); $pub_contact = DBA::selectFirst('contact', $fields, ['id' => $datarray['author-id']]);
if (!DBA::isResult($pub_contact)) { if (!DBA::isResult($pub_contact)) {
// Should never happen // Should never happen
@ -3252,6 +3285,7 @@ class Contact
'created' => DateTimeFormat::utcNow(), 'created' => DateTimeFormat::utcNow(),
'url' => $url, 'url' => $url,
'nurl' => Strings::normaliseLink($url), 'nurl' => Strings::normaliseLink($url),
'baseurl' => $pub_contact['baseurl'] ?? '',
'name' => $name, 'name' => $name,
'nick' => $nick, 'nick' => $nick,
'network' => $network, 'network' => $network,

View file

@ -31,6 +31,8 @@ use Friendica\Model\APContact;
use Friendica\Model\Contact; use Friendica\Model\Contact;
use Friendica\Model\Profile; use Friendica\Model\Profile;
use Friendica\Model\User; use Friendica\Model\User;
use Friendica\Model\Verb;
use Friendica\Protocol\Activity;
use Friendica\Protocol\ActivityPub; use Friendica\Protocol\ActivityPub;
use Friendica\Util\DateTimeFormat; use Friendica\Util\DateTimeFormat;
use Friendica\Util\Strings; use Friendica\Util\Strings;
@ -246,6 +248,10 @@ class Relation
{ {
$contact_discovery = DI::config()->get('system', 'contact_discovery'); $contact_discovery = DI::config()->get('system', 'contact_discovery');
if (Contact::isLocal($url)) {
return true;
}
if ($contact_discovery == self::DISCOVERY_NONE) { if ($contact_discovery == self::DISCOVERY_NONE) {
return false; return false;
} }
@ -770,4 +776,79 @@ class Relation
['limit' => [$offset, $count], 'order' => [$shuffle ? 'RAND()' : 'name']] ['limit' => [$offset, $count], 'order' => [$shuffle ? 'RAND()' : 'name']]
); );
} }
/**
* Calculate the interaction scores for the given user
*
* @param integer $uid
* @return void
*/
public static function calculateInteractionScore(int $uid)
{
$days = DI::config()->get('channel', 'interaction_score_days');
$contact_id = Contact::getPublicIdByUserId($uid);
Logger::debug('Calculation - start', ['uid' => $uid, 'cid' => $contact_id, 'days' => $days]);
$follow = Verb::getID(Activity::FOLLOW);
$view = Verb::getID(Activity::VIEW);
$read = Verb::getID(Activity::READ);
DBA::update('contact-relation', ['score' => 0, 'relation-score' => 0, 'thread-score' => 0, 'relation-thread-score' => 0], ['relation-cid' => $contact_id]);
$total = DBA::fetchFirst("SELECT count(*) AS `activity` FROM `post-user` INNER JOIN `post` ON `post`.`uri-id` = `post-user`.`thr-parent-id` WHERE `post-user`.`author-id` = ? AND `post-user`.`received` >= ? AND `post-user`.`uid` = ? AND `post`.`author-id` != ? AND NOT `post`.`vid` IN (?, ?, ?)",
$contact_id, DateTimeFormat::utc('now - ' . $days . ' day'), $uid, $contact_id, $follow, $view, $read);
Logger::debug('Calculate relation-score', ['uid' => $uid, 'total' => $total['activity']]);
$interactions = DBA::p("SELECT `post`.`author-id`, count(*) AS `activity`, EXISTS(SELECT `pid` FROM `account-user-view` WHERE `pid` = `post`.`author-id` AND `uid` = ? AND `rel` IN (?, ?)) AS `follows`
FROM `post-user` INNER JOIN `post` ON `post`.`uri-id` = `post-user`.`thr-parent-id` WHERE `post-user`.`author-id` = ? AND `post-user`.`received` >= ? AND `post-user`.`uid` = ? AND `post`.`author-id` != ? AND NOT `post`.`vid` IN (?, ?, ?) GROUP BY `post`.`author-id`",
$uid, Contact::SHARING, Contact::FRIEND, $contact_id, DateTimeFormat::utc('now - ' . $days . ' day'), $uid, $contact_id, $follow, $view, $read);
while ($interaction = DBA::fetch($interactions)) {
$score = min((int)(($interaction['activity'] / $total['activity']) * 65535), 65535);
DBA::update('contact-relation', ['relation-score' => $score, 'follows' => $interaction['follows']], ['relation-cid' => $contact_id, 'cid' => $interaction['author-id']]);
}
DBA::close($interactions);
$total = DBA::fetchFirst("SELECT count(*) AS `activity` FROM `post-user` INNER JOIN `post` ON `post`.`uri-id` = `post-user`.`parent-uri-id` WHERE `post-user`.`author-id` = ? AND `post-user`.`received` >= ? AND `post-user`.`uid` = ? AND `post`.`author-id` != ? AND NOT `post`.`vid` IN (?, ?, ?)",
$contact_id, DateTimeFormat::utc('now - ' . $days . ' day'), $uid, $contact_id, $follow, $view, $read);
Logger::debug('Calculate relation-thread-score', ['uid' => $uid, 'total' => $total['activity']]);
$interactions = DBA::p("SELECT `post`.`author-id`, count(*) AS `activity`, EXISTS(SELECT `pid` FROM `account-user-view` WHERE `pid` = `post`.`author-id` AND `uid` = ? AND `rel` IN (?, ?)) AS `follows`
FROM `post-user` INNER JOIN `post` ON `post`.`uri-id` = `post-user`.`parent-uri-id` WHERE `post-user`.`author-id` = ? AND `post-user`.`received` >= ? AND `post-user`.`uid` = ? AND `post`.`author-id` != ? AND NOT `post`.`vid` IN (?, ?, ?) GROUP BY `post`.`author-id`",
$uid, Contact::SHARING, Contact::FRIEND, $contact_id, DateTimeFormat::utc('now - ' . $days . ' day'), $uid, $contact_id, $follow, $view, $read);
while ($interaction = DBA::fetch($interactions)) {
$score = min((int)(($interaction['activity'] / $total['activity']) * 65535), 65535);
DBA::update('contact-relation', ['relation-thread-score' => $score, 'follows' => !empty($interaction['follows'])], ['relation-cid' => $contact_id, 'cid' => $interaction['author-id']]);
}
DBA::close($interactions);
$total = DBA::fetchFirst("SELECT count(*) AS `activity` FROM `post-user` INNER JOIN `post` ON `post-user`.`uri-id` = `post`.`thr-parent-id` WHERE `post-user`.`author-id` = ? AND `post-user`.`received` >= ? AND `post-user`.`uid` = ? AND `post`.`author-id` != ? AND NOT `post`.`vid` IN (?, ?, ?)",
$contact_id, DateTimeFormat::utc('now - ' . $days . ' day'), $uid, $contact_id, $follow, $view, $read);
Logger::debug('Calculate score', ['uid' => $uid, 'total' => $total['activity']]);
$interactions = DBA::p("SELECT `post`.`author-id`, count(*) AS `activity` FROM `post-user` INNER JOIN `post` ON `post-user`.`uri-id` = `post`.`thr-parent-id` WHERE `post-user`.`author-id` = ? AND `post-user`.`received` >= ? AND `post-user`.`uid` = ? AND `post`.`author-id` != ? AND NOT `post`.`vid` IN (?, ?, ?) GROUP BY `post`.`author-id`",
$contact_id, DateTimeFormat::utc('now - ' . $days . ' day'), $uid, $contact_id, $follow, $view, $read);
while ($interaction = DBA::fetch($interactions)) {
$score = min((int)(($interaction['activity'] / $total['activity']) * 65535), 65535);
DBA::update('contact-relation', ['score' => $score], ['relation-cid' => $contact_id, 'cid' => $interaction['author-id']]);
}
DBA::close($interactions);
$total = DBA::fetchFirst("SELECT count(*) AS `activity` FROM `post-user` INNER JOIN `post` ON `post-user`.`uri-id` = `post`.`parent-uri-id` WHERE `post-user`.`author-id` = ? AND `post-user`.`received` >= ? AND `post-user`.`uid` = ? AND `post`.`author-id` != ? AND NOT `post`.`vid` IN (?, ?, ?)",
$contact_id, DateTimeFormat::utc('now - ' . $days . ' day'), $uid, $contact_id, $follow, $view, $read);
Logger::debug('Calculate thread-score', ['uid' => $uid, 'total' => $total['activity']]);
$interactions = DBA::p("SELECT `post`.`author-id`, count(*) AS `activity` FROM `post-user` INNER JOIN `post` ON `post-user`.`uri-id` = `post`.`parent-uri-id` WHERE `post-user`.`author-id` = ? AND `post-user`.`received` >= ? AND `post-user`.`uid` = ? AND `post`.`author-id` != ? AND NOT `post`.`vid` IN (?, ?, ?) GROUP BY `post`.`author-id`",
$contact_id, DateTimeFormat::utc('now - ' . $days . ' day'), $uid, $contact_id, $follow, $view, $read);
while ($interaction = DBA::fetch($interactions)) {
$score = min((int)(($interaction['activity'] / $total['activity']) * 65535), 65535);
DBA::update('contact-relation', ['thread-score' => $score], ['relation-cid' => $contact_id, 'cid' => $interaction['author-id']]);
}
DBA::close($interactions);
Logger::debug('Calculation - end', ['uid' => $uid]);
}
} }

View file

@ -37,6 +37,10 @@ use PDOException;
*/ */
class User class User
{ {
const FREQUENCY_DEFAULT = 0;
const FREQUENCY_NEVER = 1;
const FREQUENCY_ALWAYS = 2;
const FREQUENCY_REDUCED = 3;
/** /**
* Insert a user-contact for a given contact array * Insert a user-contact for a given contact array
* *
@ -314,6 +318,53 @@ class User
return $collapsed; return $collapsed;
} }
/**
* Set the channel post frequency 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 $frequency Type of post frequency in channels
* @return void
* @throws \Exception
*/
public static function setChannelFrequency(int $cid, int $uid, int $frequency)
{
$cdata = Contact::getPublicAndUserContactID($cid, $uid);
if (empty($cdata)) {
return;
}
DBA::update('user-contact', ['channel-frequency' => $frequency], ['cid' => $cdata['public'], 'uid' => $uid], true);
}
/**
* Returns the channel frequency state for contact id and user id
*
* @param int $cid Either public contact id or user's contact id
* @param int $uid User ID
* @return int Type of post frequency in channels
* @throws HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function getChannelFrequency(int $cid, int $uid): int
{
$cdata = Contact::getPublicAndUserContactID($cid, $uid);
if (empty($cdata)) {
return false;
}
$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/Release that the user is blocked by the contact * Set/Release that the user is blocked by the contact
* *

View file

@ -22,6 +22,9 @@
namespace Friendica\Model; namespace Friendica\Model;
use Friendica\Contact\LocalRelationship\Entity\LocalRelationship; use Friendica\Contact\LocalRelationship\Entity\LocalRelationship;
use Friendica\Content\Image;
use Friendica\Content\Post\Collection\PostMedias;
use Friendica\Content\Post\Entity\PostMedia;
use Friendica\Content\Text\BBCode; use Friendica\Content\Text\BBCode;
use Friendica\Content\Text\HTML; use Friendica\Content\Text\HTML;
use Friendica\Core\Hook; use Friendica\Core\Hook;
@ -34,6 +37,7 @@ use Friendica\Database\DBA;
use Friendica\DI; use Friendica\DI;
use Friendica\Model\Post\Category; use Friendica\Model\Post\Category;
use Friendica\Network\HTTPException\InternalServerErrorException; use Friendica\Network\HTTPException\InternalServerErrorException;
use Friendica\Network\HTTPException\ServiceUnavailableException;
use Friendica\Protocol\Activity; use Friendica\Protocol\Activity;
use Friendica\Protocol\ActivityPub; use Friendica\Protocol\ActivityPub;
use Friendica\Protocol\Delivery; use Friendica\Protocol\Delivery;
@ -1078,7 +1082,10 @@ class Item
} }
if ($item['origin']) { if ($item['origin']) {
if (Photo::setPermissionFromBody($item['body'], $item['uid'], $item['contact-id'], $item['allow_cid'], $item['allow_gid'], $item['deny_cid'], $item['deny_gid'])) { if (
Photo::setPermissionFromBody($item['body'], $item['uid'], $item['contact-id'], $item['allow_cid'], $item['allow_gid'], $item['deny_cid'], $item['deny_gid'])
&& ($item['object-type'] != Activity\ObjectType::EVENT)
) {
$item['object-type'] = Activity\ObjectType::IMAGE; $item['object-type'] = Activity\ObjectType::IMAGE;
} }
@ -1181,6 +1188,11 @@ class Item
} }
} }
if (!empty($item['quote-uri-id']) && ($item['quote-uri-id'] == $item['uri-id'])) {
Logger::info('Quote-Uri-Id is identical to Uri-Id', ['uri-id' => $item['uri-id'], 'guid' => $item['guid']]);
unset($item['quote-uri-id']);
}
if (!empty($item['quote-uri-id'])) { if (!empty($item['quote-uri-id'])) {
$item['raw-body'] = BBCode::removeSharedData($item['raw-body']); $item['raw-body'] = BBCode::removeSharedData($item['raw-body']);
$item['body'] = BBCode::removeSharedData($item['body']); $item['body'] = BBCode::removeSharedData($item['body']);
@ -1200,8 +1212,6 @@ class Item
// Check for hashtags in the body and repair or add hashtag links // Check for hashtags in the body and repair or add hashtag links
$item['body'] = self::setHashtags($item['body']); $item['body'] = self::setHashtags($item['body']);
$item['language'] = self::getLanguage($item);
$notify_type = Delivery::POST; $notify_type = Delivery::POST;
// Filling item related side tables // Filling item related side tables
@ -1250,7 +1260,9 @@ class Item
} }
} }
Post::insert($item['uri-id'], $item); $item['language'] = self::getLanguage($item);
$inserted = Post::insert($item['uri-id'], $item);
if ($item['gravity'] == self::GRAVITY_PARENT) { if ($item['gravity'] == self::GRAVITY_PARENT) {
Post\Thread::insert($item['uri-id'], $item); Post\Thread::insert($item['uri-id'], $item);
@ -1405,6 +1417,10 @@ class Item
self::updateDisplayCache($posted_item['uri-id']); self::updateDisplayCache($posted_item['uri-id']);
} }
if ($inserted) {
Post\Engagement::storeFromItem($posted_item);
}
return $post_user_id; return $post_user_id;
} }
@ -1975,7 +1991,7 @@ class Item
return ''; return '';
} }
$languages = self::getLanguageArray(trim($item['title'] . "\n" . $item['body']), 3); $languages = self::getLanguageArray($item['title'] . ' ' . ($item['content-warning'] ?? '') . ' ' . $item['body'], 3, $item['uri-id'], $item['author-id']);
if (empty($languages)) { if (empty($languages)) {
return ''; return '';
} }
@ -1988,66 +2004,128 @@ class Item
* *
* @param string $body * @param string $body
* @param integer $count * @param integer $count
* @param integer $uri_id
* @param integer $author_id
* @return array * @return array
*/ */
public static function getLanguageArray(string $body, int $count): array public static function getLanguageArray(string $body, int $count, int $uri_id = 0, int $author_id = 0): array
{ {
// Convert attachments to links $searchtext = BBCode::toSearchText($body, $uri_id);
$naked_body = BBCode::removeAttachment($body);
if (empty($naked_body)) { if ((count(explode(' ', $searchtext)) < 10) && (mb_strlen($searchtext) < 30) && $author_id) {
$author = Contact::selectFirst(['about'], ['id' => $author_id]);
if (!empty($author['about'])) {
$about = BBCode::toSearchText($author['about'], 0);
Logger::debug('About field added', ['author' => $author_id, 'body' => $searchtext, 'about' => $about]);
$searchtext .= ' ' . $about;
}
}
if (empty($searchtext)) {
return []; return [];
} }
// Remove links and pictures $availableLanguages = DI::l10n()->getAvailableLanguages(true);
$naked_body = BBCode::removeLinks($naked_body); $availableLanguages = DI::l10n()->convertForLanguageDetection($availableLanguages);
// Convert the title and the body to plain text
$naked_body = BBCode::toPlaintext($naked_body);
// Remove possibly remaining links
$naked_body = preg_replace(Strings::autoLinkRegEx(), '', $naked_body);
if (empty($naked_body)) {
return [];
}
$naked_body = self::getDominantLanguage($naked_body);
$availableLanguages = DI::l10n()->getAvailableLanguages();
// See https://github.com/friendica/friendica/issues/10511
// Persian is manually added to language detection until a persian translation is provided for the interface, at
// which point it will be automatically available through `getAvailableLanguages()` and this should be removed.
$availableLanguages['fa'] = 'fa';
$ld = new Language(array_keys($availableLanguages)); $ld = new Language(array_keys($availableLanguages));
return $ld->detect($naked_body)->limit(0, $count)->close() ?: [];
$result = [];
foreach (self::splitByBlocks($searchtext) as $block) {
$languages = $ld->detect($block)->limit(0, $count)->close() ?: [];
$data = [
'text' => $block,
'detected' => $languages,
'uri-id' => $uri_id,
'author-id' => $author_id,
];
Hook::callAll('detect_languages', $data);
foreach ($data['detected'] as $language => $quality) {
$result[$language] = max($result[$language] ?? 0, $quality * (strlen($block) / strlen($searchtext)));
}
}
arsort($result);
$result = array_slice($result, 0, $count);
return $result;
} }
/** /**
* Check if latin or non latin are dominant in the body and only return the dominant one * Split a string into different unicode blocks
* Currently the text is split into the latin and the non latin part.
* *
* @param string $body * @param string $body
* @return string * @return array
*/ */
private static function getDominantLanguage(string $body): string private static function splitByBlocks(string $body): array
{ {
$latin = ''; if (!class_exists('IntlChar')) {
$non_latin = ''; return [$body];
}
$blocks = [];
$previous_block = 0;
for ($i = 0; $i < mb_strlen($body); $i++) { for ($i = 0; $i < mb_strlen($body); $i++) {
$character = mb_substr($body, $i, 1); $character = mb_substr($body, $i, 1);
$ord = mb_ord($character); $previous = ($i > 0) ? mb_substr($body, $i - 1, 1) : '';
$next = ($i < mb_strlen($body)) ? mb_substr($body, $i + 1, 1) : '';
// We add the most common characters to both strings. if (!\IntlChar::isalpha($character)) {
if (($ord <= 64) || ($ord >= 91 && $ord <= 96) || ($ord >= 123 && $ord <= 191) || in_array($ord, [215, 247]) || ($ord >= 697 && $ord <= 735) || ($ord > 65535)) { if (($previous != '') && (\IntlChar::isalpha($previous))) {
$latin .= $character; $previous_block = self::getBlockCode($previous);
$non_latin .= $character; }
} elseif ($ord < 768) {
$latin .= $character; $block = (($next != '') && \IntlChar::isalpha($next)) ? self::getBlockCode($next) : $previous_block;
$blocks[$block] = ($blocks[$block] ?? '') . $character;
} else { } else {
$non_latin .= $character; $block = self::getBlockCode($character);
$blocks[$block] = ($blocks[$block] ?? '') . $character;
} }
} }
return (mb_strlen($latin) > mb_strlen($non_latin)) ? $latin : $non_latin;
foreach (array_keys($blocks) as $key) {
$blocks[$key] = trim($blocks[$key]);
if (empty($blocks[$key])) {
unset($blocks[$key]);
}
}
return array_values($blocks);
}
/**
* returns the block code for the given character
*
* @param string $character
* @return integer 0 = no alpha character (blank, signs, emojis, ...), 1 = latin character, 2 = character in every other language
*/
private static function getBlockCode(string $character): int
{
if (!\IntlChar::isalpha($character)) {
return 0;
}
return self::isLatin($character) ? 1 : 2;
}
/**
* Checks if the given character is in one of the latin code blocks
*
* @param string $character
* @return boolean
*/
private static function isLatin(string $character): bool
{
return in_array(\IntlChar::getBlockCode($character), [
\IntlChar::BLOCK_CODE_BASIC_LATIN, \IntlChar::BLOCK_CODE_LATIN_1_SUPPLEMENT,
\IntlChar::BLOCK_CODE_LATIN_EXTENDED_A, \IntlChar::BLOCK_CODE_LATIN_EXTENDED_B,
\IntlChar::BLOCK_CODE_LATIN_EXTENDED_C, \IntlChar::BLOCK_CODE_LATIN_EXTENDED_D,
\IntlChar::BLOCK_CODE_LATIN_EXTENDED_E, \IntlChar::BLOCK_CODE_LATIN_EXTENDED_ADDITIONAL
]);
} }
public static function getLanguageMessage(array $item): string public static function getLanguageMessage(array $item): string
@ -2056,7 +2134,7 @@ class Item
$used_languages = ''; $used_languages = '';
foreach (json_decode($item['language'], true) as $language => $reliability) { foreach (json_decode($item['language'], true) as $language => $reliability) {
$used_languages .= $iso639->languageByCode1($language) . ' (' . $language . "): " . number_format($reliability, 5) . '\n'; $used_languages .= $iso639->nativeByCode1(substr($language, 0, 2)) . ' (' . $iso639->languageByCode1(substr($language, 0, 2)) . ' - ' . $language . "): " . number_format($reliability, 5) . '\n';
} }
$used_languages = DI::l10n()->t('Detected languages in this post:\n%s', $used_languages); $used_languages = DI::l10n()->t('Detected languages in this post:\n%s', $used_languages);
return $used_languages; return $used_languages;
@ -3121,7 +3199,7 @@ class Item
$item['body'] = BBCode::removeSharedData($item['body']); $item['body'] = BBCode::removeSharedData($item['body']);
} elseif (empty($shared_item['uri-id']) && empty($item['quote-uri-id']) && ($item['network'] != Protocol::DIASPORA)) { } elseif (empty($shared_item['uri-id']) && empty($item['quote-uri-id']) && ($item['network'] != Protocol::DIASPORA)) {
$media = Post\Media::getByURIId($item['uri-id'], [Post\Media::ACTIVITY]); $media = Post\Media::getByURIId($item['uri-id'], [Post\Media::ACTIVITY]);
if (!empty($media)) { if (!empty($media) && ($media[0]['media-uri-id'] != $item['uri-id'])) {
$shared_item = Post::selectFirst($fields, ['uri-id' => $media[0]['media-uri-id'], 'uid' => [$item['uid'], 0]]); $shared_item = Post::selectFirst($fields, ['uri-id' => $media[0]['media-uri-id'], 'uid' => [$item['uid'], 0]]);
if (empty($shared_item['uri-id'])) { if (empty($shared_item['uri-id'])) {
$shared_item = Post::selectFirst($fields, ['plink' => $media[0]['url'], 'uid' => [$item['uid'], 0]]); $shared_item = Post::selectFirst($fields, ['plink' => $media[0]['url'], 'uid' => [$item['uid'], 0]]);
@ -3156,15 +3234,15 @@ class Item
if (!empty($shared_item['uri-id'])) { if (!empty($shared_item['uri-id'])) {
$shared_uri_id = $shared_item['uri-id']; $shared_uri_id = $shared_item['uri-id'];
$shared_links[] = strtolower($shared_item['plink']); $shared_links[] = strtolower($shared_item['plink']);
$shared_attachments = Post\Media::splitAttachments($shared_uri_id, [], $shared_item['has-media']); $sharedSplitAttachments = DI::postMediaRepository()->splitAttachments($shared_uri_id, [], $shared_item['has-media']);
$shared_links = array_merge($shared_links, array_column($shared_attachments['visual'], 'url')); $shared_links = array_merge($shared_links, $sharedSplitAttachments['visual']->column('url'));
$shared_links = array_merge($shared_links, array_column($shared_attachments['link'], 'url')); $shared_links = array_merge($shared_links, $sharedSplitAttachments['link']->column('url'));
$shared_links = array_merge($shared_links, array_column($shared_attachments['additional'], 'url')); $shared_links = array_merge($shared_links, $sharedSplitAttachments['additional']->column('url'));
$item['body'] = self::replaceVisualAttachments($shared_attachments, $item['body']); $item['body'] = self::replaceVisualAttachments($sharedSplitAttachments['visual'], $item['body']);
} }
$attachments = Post\Media::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($attachments, $item['body'] ?? ''); $item['body'] = self::replaceVisualAttachments($itemSplitAttachments['visual'], $item['body'] ?? '');
self::putInCache($item); self::putInCache($item);
$item['body'] = $body; $item['body'] = $body;
@ -3189,7 +3267,7 @@ class Item
$filter_reasons[] = DI::l10n()->t('Content warning: %s', $item['content-warning']); $filter_reasons[] = DI::l10n()->t('Content warning: %s', $item['content-warning']);
} }
$item['attachments'] = $attachments; $item['attachments'] = $itemSplitAttachments;
$hook_data = [ $hook_data = [
'item' => $item, 'item' => $item,
@ -3218,11 +3296,11 @@ class Item
return $s; return $s;
} }
if (!empty($shared_attachments)) { if (!empty($sharedSplitAttachments)) {
$s = self::addGallery($s, $shared_attachments, $item['uri-id']); $s = self::addGallery($s, $sharedSplitAttachments['visual']);
$s = self::addVisualAttachments($shared_attachments, $shared_item, $s, true); $s = self::addVisualAttachments($sharedSplitAttachments['visual'], $shared_item, $s, true);
$s = self::addLinkAttachment($shared_uri_id ?: $item['uri-id'], $shared_attachments, $body, $s, true, $quote_shared_links); $s = self::addLinkAttachment($shared_uri_id ?: $item['uri-id'], $sharedSplitAttachments, $body, $s, true, $quote_shared_links);
$s = self::addNonVisualAttachments($shared_attachments, $item, $s, true); $s = self::addNonVisualAttachments($sharedSplitAttachments['additional'], $item, $s, true);
$body = BBCode::removeSharedData($body); $body = BBCode::removeSharedData($body);
} }
@ -3232,10 +3310,10 @@ class Item
$s = substr($s, 0, $pos); $s = substr($s, 0, $pos);
} }
$s = self::addGallery($s, $attachments, $item['uri-id']); $s = self::addGallery($s, $itemSplitAttachments['visual']);
$s = self::addVisualAttachments($attachments, $item, $s, false); $s = self::addVisualAttachments($itemSplitAttachments['visual'], $item, $s, false);
$s = self::addLinkAttachment($item['uri-id'], $attachments, $body, $s, false, $shared_links); $s = self::addLinkAttachment($item['uri-id'], $itemSplitAttachments, $body, $s, false, $shared_links);
$s = self::addNonVisualAttachments($attachments, $item, $s, false); $s = self::addNonVisualAttachments($itemSplitAttachments['additional'], $item, $s, false);
$s = self::addQuestions($item, $s); $s = self::addQuestions($item, $s);
// Map. // Map.
@ -3263,45 +3341,35 @@ class Item
return $hook_data['html']; return $hook_data['html'];
} }
/**
* @param array $images
* @return string
* @throws \Friendica\Network\HTTPException\ServiceUnavailableException
*/
private static function makeImageGrid(array $images): string
{
// Image for first column (fc) and second column (sc)
$images_fc = [];
$images_sc = [];
for ($i = 0; $i < count($images); $i++) {
($i % 2 == 0) ? ($images_fc[] = $images[$i]) : ($images_sc[] = $images[$i]);
}
return Renderer::replaceMacros(Renderer::getMarkupTemplate('content/image_grid.tpl'), [
'columns' => [
'fc' => $images_fc,
'sc' => $images_sc,
],
]);
}
/** /**
* Modify links to pictures to links for the "Fancybox" gallery * Modify links to pictures to links for the "Fancybox" gallery
* *
* @param string $s * @param string $s
* @param array $attachments * @param PostMedias $PostMedias
* @param integer $uri_id
* @return string * @return string
*/ */
private static function addGallery(string $s, array $attachments, int $uri_id): string private static function addGallery(string $s, PostMedias $PostMedias): string
{ {
foreach ($attachments['visual'] as $attachment) { foreach ($PostMedias as $PostMedia) {
if (empty($attachment['preview']) || ($attachment['type'] != Post\Media::IMAGE)) { if (!$PostMedia->preview || ($PostMedia->type !== Post\Media::IMAGE)) {
continue; continue;
} }
$s = str_replace('<a href="' . $attachment['url'] . '"', '<a data-fancybox="' . $uri_id . '" href="' . $attachment['url'] . '"', $s);
if ($PostMedia->hasDimensions()) {
$pattern = '#<a href="' . preg_quote($PostMedia->url) . '">(.*?)"></a>#';
$s = preg_replace_callback($pattern, function () use ($PostMedia) {
return Renderer::replaceMacros(Renderer::getMarkupTemplate('content/image/single_with_height_allocation.tpl'), [
'$image' => $PostMedia,
'$allocated_height' => $PostMedia->getAllocatedHeight(),
'$allocated_max_width' => ($PostMedia->previewWidth ?? $PostMedia->width) . 'px',
]);
}, $s);
} else {
$s = str_replace('<a href="' . $PostMedia->url . '"', '<a data-fancybox="uri-id-' . $PostMedia->uriId . '" href="' . $PostMedia->url . '"', $s);
}
} }
return $s; return $s;
} }
@ -3359,30 +3427,30 @@ class Item
/** /**
* Replace visual attachments in the body * Replace visual attachments in the body
* *
* @param array $attachments * @param PostMedias $PostMedias
* @param string $body * @param string $body
* @return string modified body * @return string modified body
*/ */
private static function replaceVisualAttachments(array $attachments, string $body): string private static function replaceVisualAttachments(PostMedias $PostMedias, string $body): string
{ {
DI::profiler()->startRecording('rendering'); DI::profiler()->startRecording('rendering');
foreach ($attachments['visual'] as $attachment) { foreach ($PostMedias as $PostMedia) {
if (!empty($attachment['preview'])) { if ($PostMedia->preview) {
if (Network::isLocalLink($attachment['preview'])) { if (DI::baseUrl()->isLocalUri($PostMedia->preview)) {
continue; continue;
} }
$proxy = Post\Media::getPreviewUrlForId($attachment['id'], Proxy::SIZE_LARGE); $proxy = DI::baseUrl() . $PostMedia->getPreviewPath(Proxy::SIZE_LARGE);
$search = ['[img=' . $attachment['preview'] . ']', ']' . $attachment['preview'] . '[/img]']; $search = ['[img=' . $PostMedia->preview . ']', ']' . $PostMedia->preview . '[/img]'];
$replace = ['[img=' . $proxy . ']', ']' . $proxy . '[/img]']; $replace = ['[img=' . $proxy . ']', ']' . $proxy . '[/img]'];
$body = str_replace($search, $replace, $body); $body = str_replace($search, $replace, $body);
} elseif ($attachment['filetype'] == 'image') { } elseif ($PostMedia->mimetype->type == 'image') {
if (Network::isLocalLink($attachment['url'])) { if (DI::baseUrl()->isLocalUri($PostMedia->url)) {
continue; continue;
} }
$proxy = Post\Media::getUrlForId($attachment['id']); $proxy = DI::baseUrl() . $PostMedia->getPreviewPath(Proxy::SIZE_LARGE);
$search = ['[img=' . $attachment['url'] . ']', ']' . $attachment['url'] . '[/img]']; $search = ['[img=' . $PostMedia->url . ']', ']' . $PostMedia->url . '[/img]'];
$replace = ['[img=' . $proxy . ']', ']' . $proxy . '[/img]']; $replace = ['[img=' . $proxy . ']', ']' . $proxy . '[/img]'];
$body = str_replace($search, $replace, $body); $body = str_replace($search, $replace, $body);
@ -3395,29 +3463,34 @@ class Item
/** /**
* Add visual attachments to the content * Add visual attachments to the content
* *
* @param array $attachments * @param PostMedias $PostMedias
* @param array $item * @param array $item
* @param string $content * @param string $content
* @param bool $shared
* @return string modified content * @return string modified content
* @throws ServiceUnavailableException
*/ */
private static function addVisualAttachments(array $attachments, array $item, string $content, bool $shared): string private static function addVisualAttachments(PostMedias $PostMedias, array $item, string $content, bool $shared): string
{ {
DI::profiler()->startRecording('rendering'); DI::profiler()->startRecording('rendering');
$leading = ''; $leading = '';
$trailing = ''; $trailing = '';
$images = []; $images = new PostMedias();
// @todo In the future we should make a single for the template engine with all media in it. This allows more flexibilty. // @todo In the future we should make a single for the template engine with all media in it. This allows more flexibilty.
foreach ($attachments['visual'] as $attachment) { foreach ($PostMedias as $PostMedia) {
if (self::containsLink($item['body'], $attachment['preview'] ?? $attachment['url'], $attachment['type'])) { if (self::containsLink($item['body'], $PostMedia->preview ?? $PostMedia->url, $PostMedia->type)) {
continue; continue;
} }
if ($attachment['filetype'] == 'image') { if ($PostMedia->mimetype->type == 'image') {
$preview_url = Post\Media::getPreviewUrlForId($attachment['id'], ($attachment['width'] > $attachment['height']) ? Proxy::SIZE_MEDIUM : Proxy::SIZE_LARGE); $preview_size = $PostMedia->width > $PostMedia->height ? Proxy::SIZE_MEDIUM : Proxy::SIZE_LARGE;
} elseif (!empty($attachment['preview'])) { $preview_url = DI::baseUrl() . $PostMedia->getPreviewPath($preview_size);
$preview_url = Post\Media::getPreviewUrlForId($attachment['id'], Proxy::SIZE_LARGE); } elseif ($PostMedia->preview) {
$preview_size = Proxy::SIZE_LARGE;
$preview_url = DI::baseUrl() . $PostMedia->getPreviewPath($preview_size);
} else { } else {
$preview_size = 0;
$preview_url = ''; $preview_url = '';
} }
@ -3425,15 +3498,15 @@ class Item
continue; continue;
} }
if (($attachment['filetype'] == 'video')) { if ($PostMedia->mimetype->type == 'video') {
/// @todo Move the template to /content as well /// @todo Move the template to /content as well
$media = Renderer::replaceMacros(Renderer::getMarkupTemplate('video_top.tpl'), [ $media = Renderer::replaceMacros(Renderer::getMarkupTemplate('video_top.tpl'), [
'$video' => [ '$video' => [
'id' => $attachment['id'], 'id' => $PostMedia->id,
'src' => $attachment['url'], 'src' => (string)$PostMedia->url,
'name' => $attachment['name'] ?: $attachment['url'], 'name' => $PostMedia->name ?: $PostMedia->url,
'preview' => $preview_url, 'preview' => $preview_url,
'mime' => $attachment['mimetype'], 'mime' => (string)$PostMedia->mimetype,
], ],
]); ]);
if (($item['post-type'] ?? null) == Item::PT_VIDEO) { if (($item['post-type'] ?? null) == Item::PT_VIDEO) {
@ -3441,13 +3514,13 @@ class Item
} else { } else {
$trailing .= $media; $trailing .= $media;
} }
} elseif ($attachment['filetype'] == 'audio') { } elseif ($PostMedia->mimetype->type == 'audio') {
$media = Renderer::replaceMacros(Renderer::getMarkupTemplate('content/audio.tpl'), [ $media = Renderer::replaceMacros(Renderer::getMarkupTemplate('content/audio.tpl'), [
'$audio' => [ '$audio' => [
'id' => $attachment['id'], 'id' => $PostMedia->id,
'src' => $attachment['url'], 'src' => (string)$PostMedia->url,
'name' => $attachment['name'] ?: $attachment['url'], 'name' => $PostMedia->name ?: $PostMedia->url,
'mime' => $attachment['mimetype'], 'mime' => (string)$PostMedia->mimetype,
], ],
]); ]);
if (($item['post-type'] ?? null) == Item::PT_AUDIO) { if (($item['post-type'] ?? null) == Item::PT_AUDIO) {
@ -3455,23 +3528,17 @@ class Item
} else { } else {
$trailing .= $media; $trailing .= $media;
} }
} elseif ($attachment['filetype'] == 'image') { } elseif ($PostMedia->mimetype->type == 'image') {
$src_url = Post\Media::getUrlForId($attachment['id']); $src_url = DI::baseUrl() . $PostMedia->getPhotoPath();
if (self::containsLink($item['body'], $src_url)) { if (self::containsLink($item['body'], $src_url)) {
continue; continue;
} }
$images[] = ['src' => $src_url, 'preview' => $preview_url, 'attachment' => $attachment, 'uri_id' => $item['uri-id']];
$images[] = $PostMedia->withUrl(new Uri($src_url))->withPreview(new Uri($preview_url), $preview_size);
} }
} }
$media = ''; $media = Image::getBodyAttachHtml($images);
if (count($images) > 1) {
$media = self::makeImageGrid($images);
} elseif (count($images) == 1) {
$media = Renderer::replaceMacros(Renderer::getMarkupTemplate('content/image.tpl'), [
'$image' => $images[0],
]);
}
// On Diaspora posts the attached pictures are leading // On Diaspora posts the attached pictures are leading
if ($item['network'] == Protocol::DIASPORA) { if ($item['network'] == Protocol::DIASPORA) {
@ -3500,59 +3567,62 @@ class Item
/** /**
* Add link attachment to the content * Add link attachment to the content
* *
* @param array $attachments * @param int $uriid
* @param string $body * @param PostMedias[] $attachments
* @param string $content * @param string $body
* @param bool $shared * @param string $content
* @param array $ignore_links A list of URLs to ignore * @param bool $shared
* @param array $ignore_links A list of URLs to ignore
* @return string modified content * @return string modified content
* @throws InternalServerErrorException
* @throws ServiceUnavailableException
*/ */
private static function addLinkAttachment(int $uriid, array $attachments, string $body, string $content, bool $shared, array $ignore_links): 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)
$types = array_column($attachments['visual'], 'type'); $types = $attachments['visual']->column('type');
$preview = !in_array(Post\Media::IMAGE, $types) && !in_array(Post\Media::VIDEO, $types); $preview = !in_array(PostMedia::TYPE_IMAGE, $types) && !in_array(PostMedia::TYPE_VIDEO, $types);
if (!empty($attachments['link'])) { /** @var ?PostMedia $attachment */
foreach ($attachments['link'] as $link) { $attachment = null;
$found = false; foreach ($attachments['link'] as $PostMedia) {
foreach ($ignore_links as $ignore_link) { $found = false;
if (Strings::compareLink($link['url'], $ignore_link)) { foreach ($ignore_links as $ignore_link) {
$found = true; if (Strings::compareLink($PostMedia->url, $ignore_link)) {
} $found = true;
}
// @todo Judge between the links to use the one with most information
if (!$found && (empty($attachment) || !empty($link['author-name']) ||
(empty($attachment['name']) && !empty($link['name'])) ||
(empty($attachment['description']) && !empty($link['description'])) ||
(empty($attachment['preview']) && !empty($link['preview'])))) {
$attachment = $link;
} }
} }
// @todo Judge between the links to use the one with most information
if (!$found && (empty($attachment) || $PostMedia->authorName ||
(!$attachment->name && $PostMedia->name) ||
(!$attachment->description && $PostMedia->description) ||
(!$attachment->preview && $PostMedia->preview))) {
$attachment = $PostMedia;
}
} }
if (!empty($attachment)) { if (!empty($attachment)) {
$data = [ $data = [
'after' => '', 'after' => '',
'author_name' => $attachment['author-name'] ?? '', 'author_name' => $attachment->authorName ?? '',
'author_url' => $attachment['author-url'] ?? '', 'author_url' => (string)($attachment->authorUrl ?? ''),
'description' => $attachment['description'] ?? '', 'description' => $attachment->description ?? '',
'image' => '', 'image' => '',
'preview' => '', 'preview' => '',
'provider_name' => $attachment['publisher-name'] ?? '', 'provider_name' => $attachment->publisherName ?? '',
'provider_url' => $attachment['publisher-url'] ?? '', 'provider_url' => (string)($attachment->publisherUrl ?? ''),
'text' => '', 'text' => '',
'title' => $attachment['name'] ?? '', 'title' => $attachment->name ?? '',
'type' => 'link', 'type' => 'link',
'url' => $attachment['url'] 'url' => (string)$attachment->url,
]; ];
if ($preview && !empty($attachment['preview'])) { if ($preview && $attachment->preview) {
if ($attachment['preview-width'] >= 500) { if ($attachment->previewWidth >= 500) {
$data['image'] = Post\Media::getPreviewUrlForId($attachment['id'], Proxy::SIZE_MEDIUM); $data['image'] = DI::baseUrl() . $attachment->getPreviewPath(Proxy::SIZE_MEDIUM);
} else { } else {
$data['preview'] = Post\Media::getPreviewUrlForId($attachment['id'], Proxy::SIZE_MEDIUM); $data['preview'] = DI::baseUrl() . $attachment->getPreviewPath(Proxy::SIZE_MEDIUM);
} }
} }
@ -3620,19 +3690,21 @@ class Item
} }
/** /**
* Add non visual attachments to the content * Add non-visual attachments to the content
* *
* @param array $attachments * @param PostMedias $PostMedias
* @param array $item * @param array $item
* @param string $content * @param string $content
* @return string modified content * @return string modified content
* @throws InternalServerErrorException
* @throws \ImagickException
*/ */
private static function addNonVisualAttachments(array $attachments, array $item, string $content): string private static function addNonVisualAttachments(PostMedias $PostMedias, array $item, string $content): string
{ {
DI::profiler()->startRecording('rendering'); DI::profiler()->startRecording('rendering');
$trailing = ''; $trailing = '';
foreach ($attachments['additional'] as $attachment) { foreach ($PostMedias as $PostMedia) {
if (strpos($item['body'], $attachment['url'])) { if (strpos($item['body'], $PostMedia->url)) {
continue; continue;
} }
@ -3643,16 +3715,16 @@ class Item
'url' => $item['author-link'], 'url' => $item['author-link'],
'alias' => $item['author-alias'] 'alias' => $item['author-alias']
]; ];
$the_url = Contact::magicLinkByContact($author, $attachment['url']); $the_url = Contact::magicLinkByContact($author, $PostMedia->url);
$title = Strings::escapeHtml(trim(($attachment['description'] ?? '') ?: $attachment['url'])); $title = Strings::escapeHtml(trim($PostMedia->description ?? '' ?: $PostMedia->url));
if (!empty($attachment['size'])) { if ($PostMedia->size) {
$title .= ' ' . $attachment['size'] . ' ' . DI::l10n()->t('bytes'); $title .= ' ' . $PostMedia->size . ' ' . DI::l10n()->t('bytes');
} }
/// @todo Use a template /// @todo Use a template
$icon = '<div class="attachtype icon s22 type-' . $attachment['filetype'] . ' subtype-' . $attachment['subtype'] . '"></div>'; $icon = '<div class="attachtype icon s22 type-' . $PostMedia->mimetype->type . ' subtype-' . $PostMedia->mimetype->subtype . '"></div>';
$trailing .= '<a href="' . strip_tags($the_url) . '" title="' . $title . '" class="attachlink" target="_blank" rel="noopener noreferrer" >' . $icon . '</a>'; $trailing .= '<a href="' . strip_tags($the_url) . '" title="' . $title . '" class="attachlink" target="_blank" rel="noopener noreferrer" >' . $icon . '</a>';
} }

View file

@ -36,10 +36,10 @@ class Post
* *
* @param integer $uri_id * @param integer $uri_id
* @param array $fields * @param array $fields
* @return int ID of inserted post * @return bool Success of the insert process
* @throws \Exception * @throws \Exception
*/ */
public static function insert(int $uri_id, array $data = []): int public static function insert(int $uri_id, array $data = []): bool
{ {
if (empty($uri_id)) { if (empty($uri_id)) {
throw new BadMethodCallException('Empty URI_id'); throw new BadMethodCallException('Empty URI_id');
@ -50,11 +50,7 @@ class Post
// Additionally assign the key fields // Additionally assign the key fields
$fields['uri-id'] = $uri_id; $fields['uri-id'] = $uri_id;
if (!DBA::insert('post', $fields, Database::INSERT_IGNORE)) { return DBA::insert('post', $fields, Database::INSERT_IGNORE);
return 0;
}
return DBA::lastInsertId();
} }
/** /**

View file

@ -0,0 +1,210 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, 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\Content\Text\BBCode;
use Friendica\Core\Logger;
use Friendica\Core\Protocol;
use Friendica\Database\Database;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Model\Contact;
use Friendica\Model\Item;
use Friendica\Model\Post;
use Friendica\Model\Tag;
use Friendica\Model\Verb;
use Friendica\Protocol\Activity;
use Friendica\Protocol\Relay;
use Friendica\Util\DateTimeFormat;
use Friendica\Util\Strings;
// Channel
class Engagement
{
/**
* Store engagement data from an item array
*
* @param array $item
* @return void
*/
public static function storeFromItem(array $item)
{
if (in_array($item['verb'], [Activity::FOLLOW, Activity::VIEW, Activity::READ])) {
Logger::debug('Technical activities are not stored', ['uri-id' => $item['uri-id'], 'parent-uri-id' => $item['parent-uri-id'], 'verb' => $item['verb']]);
return;
}
$parent = Post::selectFirst(['uri-id', 'created', 'author-id', 'owner-id', 'uid', 'private', 'contact-contact-type', 'language', 'network',
'title', 'content-warning', 'body', 'author-contact-type', 'author-nick', 'author-addr', 'owner-contact-type', 'owner-nick', 'owner-addr'],
['uri-id' => $item['parent-uri-id']]);
if ($parent['created'] < self::getCreationDateLimit(false)) {
Logger::debug('Post is too old', ['uri-id' => $item['uri-id'], 'parent-uri-id' => $item['parent-uri-id'], 'created' => $parent['created']]);
return;
}
$store = ($item['gravity'] != Item::GRAVITY_PARENT);
if (!$store) {
$store = Contact::hasFollowers($parent['owner-id']);
}
if (!$store) {
$tagList = Relay::getSubscribedTags();
foreach (array_column(Tag::getByURIId($item['parent-uri-id'], [Tag::HASHTAG]), 'name') as $tag) {
if (in_array($tag, $tagList)) {
$store = true;
break;
}
}
}
$mediatype = self::getMediaType($item['parent-uri-id']);
if (!$store) {
$mediatype = !empty($mediatype);
}
$engagement = [
'uri-id' => $item['parent-uri-id'],
'owner-id' => $parent['owner-id'],
'contact-type' => $parent['contact-contact-type'],
'media-type' => $mediatype,
'language' => $parent['language'],
'searchtext' => self::getSearchText($parent),
'created' => $parent['created'],
'restricted' => !in_array($item['network'], Protocol::FEDERATED) || ($parent['private'] != Item::PUBLIC),
'comments' => DBA::count('post', ['parent-uri-id' => $item['parent-uri-id'], 'gravity' => Item::GRAVITY_COMMENT]),
'activities' => DBA::count('post', [
"`parent-uri-id` = ? AND `gravity` = ? AND NOT `vid` IN (?, ?, ?)",
$item['parent-uri-id'], Item::GRAVITY_ACTIVITY,
Verb::getID(Activity::FOLLOW), Verb::getID(Activity::VIEW), Verb::getID(Activity::READ)
])
];
if (!$store && ($engagement['comments'] == 0) && ($engagement['activities'] == 0)) {
Logger::debug('No media, follower, subscribed tags, comments or activities. Engagement not stored', ['fields' => $engagement]);
return;
}
$ret = DBA::insert('post-engagement', $engagement, Database::INSERT_UPDATE);
Logger::debug('Engagement stored', ['fields' => $engagement, 'ret' => $ret]);
}
private static function getSearchText(array $item): string
{
$body = '[nosmile]network:' . $item['network'];
switch ($item['private']) {
case Item::PUBLIC:
$body .= ' visibility:public';
break;
case Item::UNLISTED:
$body .= ' visibility:unlisted';
break;
case Item::PRIVATE:
$body .= ' visibility:private';
break;
}
if ($item['author-contact-type'] == Contact::TYPE_COMMUNITY) {
$body .= ' group:' . $item['author-nick'] . ' group:' . $item['author-addr'];
} elseif (in_array($item['author-contact-type'], [Contact::TYPE_PERSON, Contact::TYPE_NEWS, Contact::TYPE_ORGANISATION])) {
$body .= ' from:' . $item['author-nick'] . ' from:' . $item['author-addr'];
}
if ($item['author-id'] != $item['owner-id']) {
if ($item['owner-contact-type'] == Contact::TYPE_COMMUNITY) {
$body .= ' group:' . $item['owner-nick'] . ' group:' . $item['owner-addr'];
} elseif (in_array($item['owner-contact-type'], [Contact::TYPE_PERSON, Contact::TYPE_NEWS, Contact::TYPE_ORGANISATION])) {
$body .= ' from:' . $item['owner-nick'] . ' from:' . $item['owner-addr'];
}
}
foreach (Tag::getByURIId($item['uri-id'], [Tag::MENTION, Tag::IMPLICIT_MENTION, Tag::EXCLUSIVE_MENTION, Tag::AUDIENCE]) as $tag) {
$contact = Contact::getByURL($tag['name'], false, ['nick', 'addr', 'contact-type']);
if (empty($contact)) {
continue;
}
if (($contact['contact-type'] == Contact::TYPE_COMMUNITY) && !strpos($body, 'group:' . $contact['addr'])) {
$body .= ' group:' . $contact['nick'] . ' group:' . $contact['addr'];
} elseif (in_array($contact['contact-type'], [Contact::TYPE_PERSON, Contact::TYPE_NEWS, Contact::TYPE_ORGANISATION])) {
$body .= ' to:' . $contact['nick'] . ' to:' . $contact['addr'];
}
}
foreach (Tag::getByURIId($item['uri-id'], [Tag::HASHTAG]) as $tag) {
$body .= ' tag:' . $tag['name'];
}
$body .= ' ' . $item['title'] . ' ' . $item['content-warning'] . ' ' . $item['body'];
return BBCode::toSearchText($body, $item['uri-id']);
}
private static function getMediaType(int $uri_id): int
{
$media = Post\Media::getByURIId($uri_id);
$type = 0;
foreach ($media as $entry) {
if ($entry['type'] == Post\Media::IMAGE) {
$type = $type | 1;
} elseif ($entry['type'] == Post\Media::VIDEO) {
$type = $type | 2;
} elseif ($entry['type'] == Post\Media::AUDIO) {
$type = $type | 4;
}
}
return $type;
}
/**
* Expire old engagement data
*
* @return void
*/
public static function expire()
{
$limit = self::getCreationDateLimit(true);
if (empty($limit)) {
Logger::notice('Expiration limit not reached');
return;
}
DBA::delete('post-engagement', ["`created` < ?", $limit]);
Logger::notice('Cleared expired engagements', ['limit' => $limit, 'rows' => DBA::affectedRows()]);
}
private static function getCreationDateLimit(bool $forDeletion): string
{
$posts = DI::config()->get('channel', 'engagement_post_limit');
if (!empty($posts)) {
$limit = DBA::selectToArray('post-engagement', ['created'], [], ['limit' => [$posts, 1], 'order' => ['created' => true]]);
if (!empty($limit)) {
return $limit[0]['created'];
} elseif ($forDeletion) {
return '';
}
}
return DateTimeFormat::utc('now - ' . DI::config()->get('channel', 'engagement_hours') . ' hour');
}
}

View file

@ -265,6 +265,11 @@ class Media
return $media; return $media;
} }
if ($item['uri-id'] == $media['uri-id']) {
Logger::info('Media-Uri-Id is identical to Uri-Id', ['uri-id' => $media['uri-id']]);
return $media;
}
if ( if (
!empty($item['plink']) && Strings::compareLink($item['plink'], $media['url']) && !empty($item['plink']) && Strings::compareLink($item['plink'], $media['url']) &&
parse_url($item['plink'], PHP_URL_HOST) != parse_url($item['uri'], PHP_URL_HOST) parse_url($item['plink'], PHP_URL_HOST) != parse_url($item['uri'], PHP_URL_HOST)
@ -869,113 +874,6 @@ class Media
return DBA::delete('post-media', ['id' => $id]); return DBA::delete('post-media', ['id' => $id]);
} }
/**
* Split the attachment media in the three segments "visual", "link" and "additional"
*
* @param int $uri_id URI id
* @param array $links list of links that shouldn't be added
* @param bool $has_media
* @return array attachments
*/
public static function splitAttachments(int $uri_id, array $links = [], bool $has_media = true): array
{
$attachments = ['visual' => [], 'link' => [], 'additional' => []];
if (!$has_media) {
return $attachments;
}
$media = self::getByURIId($uri_id);
if (empty($media)) {
return $attachments;
}
$heights = [];
$selected = '';
$previews = [];
foreach ($media as $medium) {
foreach ($links as $link) {
if (Strings::compareLink($link, $medium['url'])) {
continue 2;
}
}
// Avoid adding separate media entries for previews
foreach ($previews as $preview) {
if (Strings::compareLink($preview, $medium['url'])) {
continue 2;
}
}
// Currently these two types are ignored here.
// Posts are added differently and contacts are not displayed as attachments.
if (in_array($medium['type'], [self::ACCOUNT, self::ACTIVITY])) {
continue;
}
if (!empty($medium['preview'])) {
$previews[] = $medium['preview'];
}
$type = explode('/', explode(';', $medium['mimetype'] ?? '')[0]);
if (count($type) < 2) {
Logger::info('Unknown MimeType', ['type' => $type, 'media' => $medium]);
$filetype = 'unkn';
$subtype = 'unkn';
} else {
$filetype = strtolower($type[0]);
$subtype = strtolower($type[1]);
}
$medium['filetype'] = $filetype;
$medium['subtype'] = $subtype;
if ($medium['type'] == self::HTML || (($filetype == 'text') && ($subtype == 'html'))) {
$attachments['link'][] = $medium;
continue;
}
if (
in_array($medium['type'], [self::AUDIO, self::IMAGE]) ||
in_array($filetype, ['audio', 'image'])
) {
$attachments['visual'][] = $medium;
} elseif (($medium['type'] == self::VIDEO) || ($filetype == 'video')) {
if (!empty($medium['height'])) {
// Peertube videos are delivered in many different resolutions. We pick a moderate one.
// Since only Peertube provides a "height" parameter, this wouldn't be executed
// when someone for example on Mastodon was sharing multiple videos in a single post.
$heights[$medium['height']] = $medium['url'];
$video[$medium['url']] = $medium;
} else {
$attachments['visual'][] = $medium;
}
} else {
$attachments['additional'][] = $medium;
}
}
if (!empty($heights)) {
ksort($heights);
foreach ($heights as $height => $url) {
if (empty($selected) || $height <= 480) {
$selected = $url;
}
}
if (!empty($selected)) {
$attachments['visual'][] = $video[$selected];
unset($video[$selected]);
foreach ($video as $element) {
$attachments['additional'][] = $element;
}
}
}
return $attachments;
}
/** /**
* Add media attachments to the body * Add media attachments to the body
* *
@ -1114,25 +1012,9 @@ class Media
*/ */
public static function getPreviewUrlForId(int $id, string $size = ''): string public static function getPreviewUrlForId(int $id, string $size = ''): string
{ {
$url = DI::baseUrl() . '/photo/preview/'; return DI::baseUrl() . '/photo/preview/' .
switch ($size) { (Proxy::getPixelsFromSize($size) ? Proxy::getPixelsFromSize($size) . '/' : '') .
case Proxy::SIZE_MICRO: $id;
$url .= Proxy::PIXEL_MICRO . '/';
break;
case Proxy::SIZE_THUMB:
$url .= Proxy::PIXEL_THUMB . '/';
break;
case Proxy::SIZE_SMALL:
$url .= Proxy::PIXEL_SMALL . '/';
break;
case Proxy::SIZE_MEDIUM:
$url .= Proxy::PIXEL_MEDIUM . '/';
break;
case Proxy::SIZE_LARGE:
$url .= Proxy::PIXEL_LARGE . '/';
break;
}
return $url . $id;
} }
/** /**
@ -1144,24 +1026,8 @@ class Media
*/ */
public static function getUrlForId(int $id, string $size = ''): string public static function getUrlForId(int $id, string $size = ''): string
{ {
$url = DI::baseUrl() . '/photo/media/'; return DI::baseUrl() . '/photo/media/' .
switch ($size) { (Proxy::getPixelsFromSize($size) ? Proxy::getPixelsFromSize($size) . '/' : '') .
case Proxy::SIZE_MICRO: $id;
$url .= Proxy::PIXEL_MICRO . '/';
break;
case Proxy::SIZE_THUMB:
$url .= Proxy::PIXEL_THUMB . '/';
break;
case Proxy::SIZE_SMALL:
$url .= Proxy::PIXEL_SMALL . '/';
break;
case Proxy::SIZE_MEDIUM:
$url .= Proxy::PIXEL_MEDIUM . '/';
break;
case Proxy::SIZE_LARGE:
$url .= Proxy::PIXEL_LARGE . '/';
break;
}
return $url . $id;
} }
} }

View file

@ -602,7 +602,7 @@ class UserNotification
*/ */
private static function checkQuoted(array $item, array $contacts): bool private static function checkQuoted(array $item, array $contacts): bool
{ {
if (empty($item['quote-uri-id'])) { if (empty($item['quote-uri-id']) || ($item['quote-uri-id'] == $item['uri-id'])) {
return false; return false;
} }
$condition = ['uri-id' => $item['quote-uri-id'], 'uid' => $item['uid'], 'author-id' => $contacts, 'deleted' => false, 'gravity' => [item::GRAVITY_PARENT, Item::GRAVITY_COMMENT]]; $condition = ['uri-id' => $item['quote-uri-id'], 'uid' => $item['uid'], 'author-id' => $contacts, 'deleted' => false, 'gravity' => [item::GRAVITY_PARENT, Item::GRAVITY_COMMENT]];

View file

@ -453,6 +453,18 @@ class Profile
Logger::warning('Missing hidewall key in profile array', ['profile' => $profile, 'callstack' => System::callstack(10)]); Logger::warning('Missing hidewall key in profile array', ['profile' => $profile, 'callstack' => System::callstack(10)]);
} }
if ($profile['account-type'] == Contact::TYPE_COMMUNITY) {
$mention_label = DI::l10n()->t('Post to group');
$mention_url = 'compose/0?body=!' . $profile['addr'];
$network_label = DI::l10n()->t('View group');
$network_url = 'network/group/' . $profile['id'];
} else {
$mention_label = DI::l10n()->t('Mention');
$mention_url = 'compose/0?body=@' . $profile['addr'];
$network_label = DI::l10n()->t('Network Posts');
$network_url = 'contact/' . $profile['id'] . '/conversations';
}
$tpl = Renderer::getMarkupTemplate('profile/vcard.tpl'); $tpl = Renderer::getMarkupTemplate('profile/vcard.tpl');
$o .= Renderer::replaceMacros($tpl, [ $o .= Renderer::replaceMacros($tpl, [
'$profile' => $p, '$profile' => $p,
@ -476,6 +488,10 @@ class Profile
'$updated' => $updated, '$updated' => $updated,
'$diaspora' => $diaspora, '$diaspora' => $diaspora,
'$contact_block' => $contact_block, '$contact_block' => $contact_block,
'$mention_label' => $mention_label,
'$mention_url' => $mention_url,
'$network_label' => $network_label,
'$network_url' => $network_url,
]); ]);
$arr = ['profile' => &$profile, 'entry' => &$o]; $arr = ['profile' => &$profile, 'entry' => &$o];
@ -813,12 +829,14 @@ class Profile
/** /**
* Set the visitor cookies (see remote_user()) for signed HTTP requests * 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 * @return array Visitor contact array
* @throws InternalServerErrorException
*/ */
public static function addVisitorCookieForHTTPSigner(): array public static function addVisitorCookieForHTTPSigner(array $server): array
{ {
$requester = HTTPSignature::getSigner('', $_SERVER); $requester = HTTPSignature::getSigner('', $server);
if (empty($requester)) { if (empty($requester)) {
return []; return [];
} }
@ -940,7 +958,7 @@ 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 = ["NOT `blocked` AND NOT `account_removed` $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
@ -953,7 +971,7 @@ class Profile
$searchTerm, $searchTerm, $searchTerm, $searchTerm, $searchTerm, $searchTerm, $searchTerm, $searchTerm,
$searchTerm, $searchTerm, $searchTerm, $searchTerm]; $searchTerm, $searchTerm, $searchTerm, $searchTerm];
} else { } else {
$condition = ['blocked' => false, 'account_removed' => 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')) {
$condition['publish'] = true; $condition['publish'] = true;
} }

View file

@ -183,7 +183,7 @@ class User
$system['dob'] = '0000-00-00'; $system['dob'] = '0000-00-00';
// Ensure that the user contains data // Ensure that the user contains data
$user = DBA::selectFirst('user', ['prvkey', 'guid'], ['uid' => 0]); $user = DBA::selectFirst('user', ['prvkey', 'guid', 'language'], ['uid' => 0]);
if (empty($user['prvkey']) || empty($user['guid'])) { if (empty($user['prvkey']) || empty($user['guid'])) {
$fields = [ $fields = [
'username' => $system['name'], 'username' => $system['name'],
@ -203,7 +203,8 @@ class User
$system['guid'] = $fields['guid']; $system['guid'] = $fields['guid'];
} else { } else {
$system['guid'] = $user['guid']; $system['guid'] = $user['guid'];
$system['language'] = $user['language'];
} }
return $system; return $system;
@ -279,8 +280,7 @@ class User
// List of possible actor names // List of possible actor names
$possible_accounts = ['friendica', 'actor', 'system', 'internal']; $possible_accounts = ['friendica', 'actor', 'system', 'internal'];
foreach ($possible_accounts as $name) { foreach ($possible_accounts as $name) {
if (!DBA::exists('user', ['nickname' => $name, 'account_removed' => false, 'account_expired' => false]) && if (!DBA::exists('user', ['nickname' => $name]) && !DBA::exists('userd', ['username' => $name])) {
!DBA::exists('userd', ['username' => $name])) {
DI::config()->set('system', 'actor_name', $name); DI::config()->set('system', 'actor_name', $name);
return $name; return $name;
} }
@ -325,7 +325,7 @@ class User
public static function getByGuid(string $guid, array $fields = [], bool $active = true) public static function getByGuid(string $guid, array $fields = [], bool $active = true)
{ {
if ($active) { if ($active) {
$cond = ['guid' => $guid, 'account_expired' => false, 'account_removed' => false]; $cond = ['guid' => $guid, 'verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => false];
} else { } else {
$cond = ['guid' => $guid]; $cond = ['guid' => $guid];
} }
@ -532,6 +532,24 @@ class User
return $default_circle; return $default_circle;
} }
/**
* Fetch the language code from the given user. If the code is invalid, return the system language
*
* @param integer $uid User-Id
* @return string
*/
public static function getLanguageCode(int $uid): string
{
$owner = self::getOwnerDataById($uid);
$languages = DI::l10n()->getAvailableLanguages(true);
if (in_array($owner['language'], array_keys($languages))) {
$language = $owner['language'];
} else {
$language = DI::config()->get('system', 'language');
}
return $language;
}
/** /**
* Authenticate a user with a clear text password * Authenticate a user with a clear text password
* *
@ -683,7 +701,7 @@ class User
$fields = ['uid', 'nickname', 'password', 'legacy_password']; $fields = ['uid', 'nickname', 'password', 'legacy_password'];
$condition = [ $condition = [
"(`email` = ? OR `username` = ? OR `nickname` = ?) "(`email` = ? OR `username` = ? OR `nickname` = ?)
AND NOT `blocked` AND NOT `account_expired` AND NOT `account_removed` AND `verified`", AND `verified` AND NOT `blocked` AND NOT `account_removed` AND NOT `account_expired`",
$user_info, $user_info, $user_info $user_info, $user_info, $user_info
]; ];
$user = DBA::selectFirst('user', $fields, $condition); $user = DBA::selectFirst('user', $fields, $condition);
@ -719,7 +737,7 @@ class User
if ($user['last-activity'] != $current_day) { if ($user['last-activity'] != $current_day) {
User::update(['last-activity' => $current_day], $uid); User::update(['last-activity' => $current_day], $uid);
// Set the last activity for all identities of the user // Set the last activity for all identities of the user
DBA::update('user', ['last-activity' => $current_day], ['parent-uid' => $uid, 'account_removed' => false]); DBA::update('user', ['last-activity' => $current_day], ['parent-uid' => $uid, 'verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => false]);
} }
} }
@ -1684,7 +1702,7 @@ class User
$identities = []; $identities = [];
$user = DBA::selectFirst('user', ['uid', 'nickname', 'username', 'parent-uid'], ['uid' => $uid]); $user = DBA::selectFirst('user', ['uid', 'nickname', 'username', 'parent-uid'], ['uid' => $uid, 'verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => false]);
if (!DBA::isResult($user)) { if (!DBA::isResult($user)) {
return $identities; return $identities;
} }
@ -1701,7 +1719,7 @@ class User
$r = DBA::select( $r = DBA::select(
'user', 'user',
['uid', 'username', 'nickname'], ['uid', 'username', 'nickname'],
['parent-uid' => $user['uid'], 'account_removed' => false] ['parent-uid' => $user['uid'], 'verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => false]
); );
if (DBA::isResult($r)) { if (DBA::isResult($r)) {
$identities = array_merge($identities, DBA::toArray($r)); $identities = array_merge($identities, DBA::toArray($r));
@ -1711,7 +1729,7 @@ class User
$r = DBA::select( $r = DBA::select(
'user', 'user',
['uid', 'username', 'nickname'], ['uid', 'username', 'nickname'],
['uid' => $user['parent-uid'], 'account_removed' => false] ['uid' => $user['parent-uid'], 'verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => false]
); );
if (DBA::isResult($r)) { if (DBA::isResult($r)) {
$identities = DBA::toArray($r); $identities = DBA::toArray($r);
@ -1721,7 +1739,7 @@ class User
$r = DBA::select( $r = DBA::select(
'user', 'user',
['uid', 'username', 'nickname'], ['uid', 'username', 'nickname'],
['parent-uid' => $user['parent-uid'], 'account_removed' => false] ['parent-uid' => $user['parent-uid'], 'verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => false]
); );
if (DBA::isResult($r)) { if (DBA::isResult($r)) {
$identities = array_merge($identities, DBA::toArray($r)); $identities = array_merge($identities, DBA::toArray($r));
@ -1732,7 +1750,7 @@ class User
"SELECT `user`.`uid`, `user`.`username`, `user`.`nickname` "SELECT `user`.`uid`, `user`.`username`, `user`.`nickname`
FROM `manage` FROM `manage`
INNER JOIN `user` ON `manage`.`mid` = `user`.`uid` INNER JOIN `user` ON `manage`.`mid` = `user`.`uid`
WHERE `user`.`account_removed` = 0 AND `manage`.`uid` = ?", WHERE NOT `user`.`account_removed` AND `manage`.`uid` = ?",
$user['uid'] $user['uid']
); );
if (DBA::isResult($r)) { if (DBA::isResult($r)) {
@ -1754,7 +1772,7 @@ class User
return false; return false;
} }
$user = DBA::selectFirst('user', ['parent-uid'], ['uid' => $uid, 'account_removed' => false]); $user = DBA::selectFirst('user', ['parent-uid'], ['uid' => $uid, 'verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => false]);
if (!DBA::isResult($user)) { if (!DBA::isResult($user)) {
return false; return false;
} }
@ -1763,7 +1781,7 @@ class User
return true; return true;
} }
if (DBA::exists('user', ['parent-uid' => $uid, 'account_removed' => false])) { if (DBA::exists('user', ['parent-uid' => $uid, 'verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => false])) {
return true; return true;
} }

View file

@ -79,6 +79,6 @@ class AccountManagementControlDocument extends BaseModule
], ],
]; ];
System::jsonExit($output); $this->jsonExit($output);
} }
} }

View file

@ -46,6 +46,6 @@ class Featured extends BaseModule
$featured = ActivityPub\Transmitter::getFeatured($owner, $page); $featured = ActivityPub\Transmitter::getFeatured($owner, $page);
System::jsonExit($featured, 'application/activity+json'); $this->jsonExit($featured, 'application/activity+json');
} }
} }

View file

@ -49,6 +49,6 @@ class Followers extends BaseModule
$followers = ActivityPub\Transmitter::getContacts($owner, [Contact::FOLLOWER, Contact::FRIEND], 'followers', $page, (string)HTTPSignature::getSigner('', $_SERVER)); $followers = ActivityPub\Transmitter::getContacts($owner, [Contact::FOLLOWER, Contact::FRIEND], 'followers', $page, (string)HTTPSignature::getSigner('', $_SERVER));
System::jsonExit($followers, 'application/activity+json'); $this->jsonExit($followers, 'application/activity+json');
} }
} }

View file

@ -47,6 +47,6 @@ class Following extends BaseModule
$following = ActivityPub\Transmitter::getContacts($owner, [Contact::SHARING, Contact::FRIEND], 'following', $page); $following = ActivityPub\Transmitter::getContacts($owner, [Contact::SHARING, Contact::FRIEND], 'following', $page);
System::jsonExit($following, 'application/activity+json'); $this->jsonExit($following, 'application/activity+json');
} }
} }

View file

@ -66,7 +66,7 @@ class Inbox extends BaseApi
$inbox = ActivityPub\ClientToServer::getPublicInbox($uid, $page, $request['max_id'] ?? null); $inbox = ActivityPub\ClientToServer::getPublicInbox($uid, $page, $request['max_id'] ?? null);
} }
System::jsonExit($inbox, 'application/activity+json'); $this->jsonExit($inbox, 'application/activity+json');
} }
protected function post(array $request = []) protected function post(array $request = [])

View file

@ -130,6 +130,6 @@ class Objects extends BaseModule
// Relaxed CORS header for public items // Relaxed CORS header for public items
header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Origin: *');
System::jsonExit($data, 'application/activity+json'); $this->jsonExit($data, 'application/activity+json');
} }
} }

View file

@ -53,7 +53,7 @@ class Outbox extends BaseApi
$outbox = ActivityPub\ClientToServer::getOutbox($owner, $uid, $page, $request['max_id'] ?? null, HTTPSignature::getSigner('', $_SERVER)); $outbox = ActivityPub\ClientToServer::getOutbox($owner, $uid, $page, $request['max_id'] ?? null, HTTPSignature::getSigner('', $_SERVER));
System::jsonExit($outbox, 'application/activity+json'); $this->jsonExit($outbox, 'application/activity+json');
} }
protected function post(array $request = []) protected function post(array $request = [])
@ -79,6 +79,6 @@ class Outbox extends BaseApi
throw new \Friendica\Network\HTTPException\BadRequestException(); throw new \Friendica\Network\HTTPException\BadRequestException();
} }
System::jsonExit(ActivityPub\ClientToServer::processActivity($activity, $uid, self::getCurrentApplication() ?? [])); $this->jsonExit(ActivityPub\ClientToServer::processActivity($activity, $uid, self::getCurrentApplication() ?? []));
} }
} }

View file

@ -100,6 +100,6 @@ class Whoami extends BaseApi
]; ];
$data['generator'] = ActivityPub\Transmitter::getService(); $data['generator'] = ActivityPub\Transmitter::getService();
System::jsonExit($data, 'application/activity+json'); $this->jsonExit($data, 'application/activity+json');
} }
} }

View file

@ -504,7 +504,7 @@ class Site extends BaseAdmin
'$max_display_comments' => ['max_display_comments', DI::l10n()->t('Maximum numbers of comments per post on the display page'), DI::config()->get('system', 'max_display_comments'), DI::l10n()->t('How many comments should be shown on the single view for each post? Default value is 1000.')], '$max_display_comments' => ['max_display_comments', DI::l10n()->t('Maximum numbers of comments per post on the display page'), DI::config()->get('system', 'max_display_comments'), DI::l10n()->t('How many comments should be shown on the single view for each post? Default value is 1000.')],
'$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.')],
'$compute_circle_counts' => ['compute_circle_counts', DI::l10n()->t('Generate counts per contact circle when calculating network count'), DI::config()->get('system', 'compute_group_counts') ?? 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.')],
'$worker_queues' => ['worker_queues', DI::l10n()->t('Maximum number of parallel workers'), DI::config()->get('system', 'worker_queues'), DI::l10n()->t('On shared hosters set this to %d. On larger systems, values of %d are great. Default value is %d.', 5, 20, 10)], '$worker_queues' => ['worker_queues', DI::l10n()->t('Maximum number of parallel workers'), DI::config()->get('system', 'worker_queues'), DI::l10n()->t('On shared hosters set this to %d. On larger systems, values of %d are great. Default value is %d.', 5, 20, 10)],
'$worker_fastlane' => ['worker_fastlane', DI::l10n()->t('Enable fastlane'), DI::config()->get('system', 'worker_fastlane'), DI::l10n()->t('When enabed, the fastlane mechanism starts an additional worker if processes with higher priority are blocked by processes of lower priority.')], '$worker_fastlane' => ['worker_fastlane', DI::l10n()->t('Enable fastlane'), DI::config()->get('system', 'worker_fastlane'), DI::l10n()->t('When enabed, the fastlane mechanism starts an additional worker if processes with higher priority are blocked by processes of lower priority.')],

View file

@ -25,6 +25,7 @@ use Friendica\App\Arguments;
use Friendica\App\BaseURL; use Friendica\App\BaseURL;
use Friendica\Core\L10n; use Friendica\Core\L10n;
use Friendica\Module\Response; use Friendica\Module\Response;
use Friendica\Network\HTTPException;
use Friendica\Util\Arrays; use Friendica\Util\Arrays;
use Friendica\Util\DateTimeFormat; use Friendica\Util\DateTimeFormat;
use Friendica\Util\XML; use Friendica\Util\XML;
@ -46,14 +47,20 @@ class ApiResponse extends Response
protected $baseUrl; protected $baseUrl;
/** @var TwitterUser */ /** @var TwitterUser */
protected $twitterUser; protected $twitterUser;
/** @var array */
protected $server;
/** @var string */
protected $jsonpCallback;
public function __construct(L10n $l10n, Arguments $args, LoggerInterface $logger, BaseURL $baseUrl, TwitterUser $twitterUser) public function __construct(L10n $l10n, Arguments $args, LoggerInterface $logger, BaseURL $baseUrl, TwitterUser $twitterUser, array $server = [], string $jsonpCallback = '')
{ {
$this->l10n = $l10n; $this->l10n = $l10n;
$this->args = $args; $this->args = $args;
$this->logger = $logger; $this->logger = $logger;
$this->baseUrl = $baseUrl; $this->baseUrl = $baseUrl;
$this->twitterUser = $twitterUser; $this->twitterUser = $twitterUser;
$this->server = $server;
$this->jsonpCallback = $jsonpCallback;
} }
/** /**
@ -63,6 +70,7 @@ class ApiResponse extends Response
* @param string $root_element Name of the root element * @param string $root_element Name of the root element
* *
* @return string The XML data * @return string The XML data
* @throws \Exception
*/ */
public function createXML(array $data, string $root_element): string public function createXML(array $data, string $root_element): string
{ {
@ -109,6 +117,7 @@ class ApiResponse extends Response
* @param int $cid Contact ID of template * @param int $cid Contact ID of template
* *
* @return array * @return array
* @throws HTTPException\InternalServerErrorException
*/ */
private function addRSSValues(array $arr, int $cid): array private function addRSSValues(array $arr, int $cid): array
{ {
@ -141,6 +150,7 @@ class ApiResponse extends Response
* @param int $cid ID of the contact for RSS * @param int $cid ID of the contact for RSS
* *
* @return array|string (string|array) XML data or JSON data * @return array|string (string|array) XML data or JSON data
* @throws HTTPException\InternalServerErrorException
*/ */
public function formatData(string $root_element, string $type, array $data, int $cid = 0) public function formatData(string $root_element, string $type, array $data, int $cid = 0)
{ {
@ -180,7 +190,7 @@ class ApiResponse extends Response
} }
/** /**
* Exit with error code * Add formatted error message to response
* *
* @param int $code * @param int $code
* @param string $description * @param string $description
@ -188,6 +198,7 @@ class ApiResponse extends Response
* @param string|null $format * @param string|null $format
* *
* @return void * @return void
* @throws HTTPException\InternalServerErrorException
*/ */
public function error(int $code, string $description, string $message, string $format = null) public function error(int $code, string $description, string $message, string $format = null)
{ {
@ -197,21 +208,23 @@ class ApiResponse extends Response
'request' => $this->args->getQueryString() 'request' => $this->args->getQueryString()
]; ];
$this->setHeader(($_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.1') . ' ' . $code . ' ' . $description); $this->setHeader(($this->server['SERVER_PROTOCOL'] ?? 'HTTP/1.1') . ' ' . $code . ' ' . $description);
$this->exit('status', ['status' => $error], $format); $this->addFormattedContent('status', ['status' => $error], $format);
} }
/** /**
* Outputs formatted data according to the data type and then exits the execution. * Add formatted data according to the data type to the response.
* *
* @param string $root_element * @param string $root_element
* @param array $data An array with a single element containing the returned result * @param array $data An array with a single element containing the returned result
* @param string|null $format Output format (xml, json, rss, atom) * @param string|null $format Output format (xml, json, rss, atom)
* @param int $cid
* *
* @return void * @return void
* @throws HTTPException\InternalServerErrorException
*/ */
public function exit(string $root_element, array $data, string $format = null, int $cid = 0) public function addFormattedContent(string $root_element, array $data, string $format = null, int $cid = 0)
{ {
$format = $format ?? 'json'; $format = $format ?? 'json';
@ -226,8 +239,8 @@ class ApiResponse extends Response
$this->setType(static::TYPE_JSON); $this->setType(static::TYPE_JSON);
if (!empty($return)) { if (!empty($return)) {
$json = json_encode(end($return)); $json = json_encode(end($return));
if (!empty($_GET['callback'])) { if ($this->jsonpCallback) {
$json = $_GET['callback'] . '(' . $json . ')'; $json = $this->jsonpCallback . '(' . $json . ')';
} }
$return = $json; $return = $json;
} }
@ -246,15 +259,16 @@ class ApiResponse extends Response
} }
/** /**
* Wrapper around exit() for JSON only responses * Wrapper around addFormattedContent() for JSON only responses
* *
* @param array $data * @param array $data
* *
* @return void * @return void
* @throws HTTPException\InternalServerErrorException
*/ */
public function exitWithJson(array $data) public function addJsonContent(array $data)
{ {
$this->exit('content', ['content' => $data], static::TYPE_JSON); $this->addFormattedContent('content', ['content' => $data], static::TYPE_JSON);
} }
/** /**
@ -273,7 +287,7 @@ class ApiResponse extends Response
[ [
'method' => $method, 'method' => $method,
'path' => $path, 'path' => $path,
'agent' => $_SERVER['HTTP_USER_AGENT'] ?? '', 'agent' => $this->server['HTTP_USER_AGENT'] ?? '',
'request' => $request, 'request' => $request,
]); ]);
$error = $this->l10n->t('API endpoint %s %s is not implemented but might be in the future.', strtoupper($method), $path); $error = $this->l10n->t('API endpoint %s %s is not implemented but might be in the future.', strtoupper($method), $path);

View file

@ -60,7 +60,7 @@ class Activity extends BaseApi
if ($res) { if ($res) {
$status_info = DI::twitterStatus()->createFromUriId($request['id'], $uid)->toArray(); $status_info = DI::twitterStatus()->createFromUriId($request['id'], $uid)->toArray();
$this->response->exit('status', ['status' => $status_info], $this->parameters['extension'] ?? null); $this->response->addFormattedContent('status', ['status' => $status_info], $this->parameters['extension'] ?? null);
} else { } else {
$this->response->error(500, 'Error adding activity', '', $this->parameters['extension'] ?? null); $this->response->error(500, 'Error adding activity', '', $this->parameters['extension'] ?? null);
} }

View file

@ -82,6 +82,6 @@ class Create extends BaseApi
$result = ['success' => true, 'gid' => $gid, 'name' => $name, 'status' => $status, 'wrong users' => $errorusers]; $result = ['success' => true, 'gid' => $gid, 'name' => $name, 'status' => $status, 'wrong users' => $errorusers];
$this->response->exit('group_create', ['$result' => $result], $this->parameters['extension'] ?? null); $this->response->addFormattedContent('group_create', ['$result' => $result], $this->parameters['extension'] ?? null);
} }
} }

View file

@ -70,7 +70,7 @@ class Delete extends BaseApi
if ($ret) { if ($ret) {
// return success // return success
$success = ['success' => $ret, 'gid' => $request['gid'], 'name' => $request['name'], 'status' => 'deleted', 'wrong users' => []]; $success = ['success' => $ret, 'gid' => $request['gid'], 'name' => $request['name'], 'status' => 'deleted', 'wrong users' => []];
$this->response->exit('group_delete', ['$result' => $success], $this->parameters['extension'] ?? null); $this->response->addFormattedContent('group_delete', ['$result' => $success], $this->parameters['extension'] ?? null);
} else { } else {
throw new BadRequestException('other API error'); throw new BadRequestException('other API error');
} }

View file

@ -75,6 +75,6 @@ class Show extends BaseApi
$grps[] = ['name' => $circle['name'], 'gid' => $circle['id'], $user_element => $users]; $grps[] = ['name' => $circle['name'], 'gid' => $circle['id'], $user_element => $users];
} }
$this->response->exit('group_update', ['group' => $grps], $this->parameters['extension'] ?? null); $this->response->addFormattedContent('group_update', ['group' => $grps], $this->parameters['extension'] ?? null);
} }
} }

View file

@ -84,6 +84,6 @@ class Update extends BaseApi
// return success message incl. missing users in array // return success message incl. missing users in array
$status = ($erroraddinguser ? 'missing user' : 'ok'); $status = ($erroraddinguser ? 'missing user' : 'ok');
$success = ['success' => true, 'gid' => $gid, 'name' => $name, 'status' => $status, 'wrong users' => $errorusers]; $success = ['success' => true, 'gid' => $gid, 'name' => $name, 'status' => $status, 'wrong users' => $errorusers];
$this->response->exit('group_update', ['$result' => $success], $this->parameters['extension'] ?? null); $this->response->addFormattedContent('group_update', ['$result' => $success], $this->parameters['extension'] ?? null);
} }
} }

View file

@ -64,7 +64,7 @@ class Search extends BaseApi
// error if no searchstring specified // error if no searchstring specified
if ($request['searchstring'] == '') { if ($request['searchstring'] == '') {
$answer = ['result' => 'error', 'message' => 'searchstring not specified']; $answer = ['result' => 'error', 'message' => 'searchstring not specified'];
$this->response->exit('direct_message_search', ['$result' => $answer], $this->parameters['extension'] ?? null); $this->response->addFormattedContent('direct_message_search', ['$result' => $answer], $this->parameters['extension'] ?? null);
return; return;
} }
@ -82,6 +82,6 @@ class Search extends BaseApi
$success = ['success' => true, 'search_results' => $ret]; $success = ['success' => true, 'search_results' => $ret];
} }
$this->response->exit('direct_message_search', ['$result' => $success], $this->parameters['extension'] ?? null); $this->response->addFormattedContent('direct_message_search', ['$result' => $success], $this->parameters['extension'] ?? null);
} }
} }

View file

@ -42,14 +42,14 @@ class Setseen extends BaseApi
// return error if id is zero // return error if id is zero
if (empty($request['id'])) { if (empty($request['id'])) {
$answer = ['result' => 'error', 'message' => 'message id not specified']; $answer = ['result' => 'error', 'message' => 'message id not specified'];
$this->response->exit('direct_messages_setseen', ['$result' => $answer], $this->parameters['extension'] ?? null); $this->response->addFormattedContent('direct_messages_setseen', ['$result' => $answer], $this->parameters['extension'] ?? null);
return; return;
} }
// error message if specified id is not in database // error message if specified id is not in database
if (!DBA::exists('mail', ['id' => $request['id'], 'uid' => $uid])) { if (!DBA::exists('mail', ['id' => $request['id'], 'uid' => $uid])) {
$answer = ['result' => 'error', 'message' => 'message id not in database']; $answer = ['result' => 'error', 'message' => 'message id not in database'];
$this->response->exit('direct_messages_setseen', ['$result' => $answer], $this->parameters['extension'] ?? null); $this->response->addFormattedContent('direct_messages_setseen', ['$result' => $answer], $this->parameters['extension'] ?? null);
return; return;
} }
@ -60,6 +60,6 @@ class Setseen extends BaseApi
$answer = ['result' => 'error', 'message' => 'unknown error']; $answer = ['result' => 'error', 'message' => 'unknown error'];
} }
$this->response->exit('direct_messages_setseen', ['$result' => $answer], $this->parameters['extension'] ?? null); $this->response->addFormattedContent('direct_messages_setseen', ['$result' => $answer], $this->parameters['extension'] ?? null);
} }
} }

View file

@ -110,6 +110,6 @@ class Create extends BaseApi
$result = ['success' => true, 'event_id' => $event_id, 'event' => $event]; $result = ['success' => true, 'event_id' => $event_id, 'event' => $event];
$this->response->exit('event_create', ['$result' => $result], $this->parameters['extension'] ?? null); $this->response->addFormattedContent('event_create', ['$result' => $result], $this->parameters['extension'] ?? null);
} }
} }

View file

@ -59,6 +59,6 @@ class Delete extends BaseApi
Event::delete($eventid); Event::delete($eventid);
$success = ['id' => $eventid, 'status' => 'deleted']; $success = ['id' => $eventid, 'status' => 'deleted'];
$this->response->exit('event_delete', ['$result' => $success], $this->parameters['extension'] ?? null); $this->response->addFormattedContent('event_delete', ['$result' => $success], $this->parameters['extension'] ?? null);
} }
} }

View file

@ -69,6 +69,6 @@ class Index extends BaseApi
]; ];
} }
$this->response->exit('events', ['events' => $items], $this->parameters['extension'] ?? null); $this->response->addFormattedContent('events', ['events' => $items], $this->parameters['extension'] ?? null);
} }
} }

View file

@ -56,6 +56,6 @@ class Notification extends BaseApi
$result = false; $result = false;
} }
$this->response->exit('notes', ['note' => $result], $this->parameters['extension'] ?? null); $this->response->addFormattedContent('notes', ['note' => $result], $this->parameters['extension'] ?? null);
} }
} }

View file

@ -70,13 +70,13 @@ class Seen extends BaseApi
// we found the item, return it to the user // we found the item, return it to the user
$ret = [DI::twitterStatus()->createFromUriId($item['uri-id'], $item['uid'], $include_entities)->toArray()]; $ret = [DI::twitterStatus()->createFromUriId($item['uri-id'], $item['uid'], $include_entities)->toArray()];
$data = ['status' => $ret]; $data = ['status' => $ret];
$this->response->exit('statuses', $data, $this->parameters['extension'] ?? null); $this->response->addFormattedContent('statuses', $data, $this->parameters['extension'] ?? null);
return; return;
} }
// the item can't be found, but we set the notification as seen, so we count this as a success // the item can't be found, but we set the notification as seen, so we count this as a success
} }
$this->response->exit('statuses', ['result' => 'success'], $this->parameters['extension'] ?? null); $this->response->addFormattedContent('statuses', ['result' => 'success'], $this->parameters['extension'] ?? null);
} catch (NotFoundException $e) { } catch (NotFoundException $e) {
throw new BadRequestException('Invalid argument', $e); throw new BadRequestException('Invalid argument', $e);
} catch (Exception $e) { } catch (Exception $e) {

View file

@ -60,6 +60,6 @@ class Photo extends BaseApi
// prepare json/xml output with data from database for the requested photo // prepare json/xml output with data from database for the requested photo
$data = ['photo' => $this->friendicaPhoto->createFromId($photo_id, $scale, $uid, $type)]; $data = ['photo' => $this->friendicaPhoto->createFromId($photo_id, $scale, $uid, $type)];
$this->response->exit('statuses', $data, $this->parameters['extension'] ?? null, Contact::getPublicIdByUserId($uid)); $this->response->addFormattedContent('statuses', $data, $this->parameters['extension'] ?? null, Contact::getPublicIdByUserId($uid));
} }
} }

View file

@ -90,7 +90,7 @@ class Create extends BaseApi
if (!empty($photo)) { if (!empty($photo)) {
Photo::clearAlbumCache($uid); Photo::clearAlbumCache($uid);
$data = ['photo' => $this->friendicaPhoto->createFromId($photo['resource_id'], null, $uid, $type)]; $data = ['photo' => $this->friendicaPhoto->createFromId($photo['resource_id'], null, $uid, $type)];
$this->response->exit('photo_create', $data, $this->parameters['extension'] ?? null); $this->response->addFormattedContent('photo_create', $data, $this->parameters['extension'] ?? null);
} else { } else {
throw new HTTPException\InternalServerErrorException('unknown error - uploading photo failed, see Friendica log for more information'); throw new HTTPException\InternalServerErrorException('unknown error - uploading photo failed, see Friendica log for more information');
} }

View file

@ -62,7 +62,7 @@ class Delete extends BaseApi
Item::deleteForUser($condition, $uid); Item::deleteForUser($condition, $uid);
Photo::clearAlbumCache($uid); Photo::clearAlbumCache($uid);
$result = ['result' => 'deleted', 'message' => 'photo with id `' . $request['photo_id'] . '` has been deleted from server.']; $result = ['result' => 'deleted', 'message' => 'photo with id `' . $request['photo_id'] . '` has been deleted from server.'];
$this->response->exit('photo_delete', ['$result' => $result], $this->parameters['extension'] ?? null); $this->response->addFormattedContent('photo_delete', ['$result' => $result], $this->parameters['extension'] ?? null);
} else { } else {
throw new InternalServerErrorException("unknown error on deleting photo from database table"); throw new InternalServerErrorException("unknown error on deleting photo from database table");
} }

View file

@ -77,6 +77,6 @@ class Lists extends BaseApi
} }
} }
$this->response->exit('statuses', $data, $this->parameters['extension'] ?? null, Contact::getPublicIdByUserId($uid)); $this->response->addFormattedContent('statuses', $data, $this->parameters['extension'] ?? null, Contact::getPublicIdByUserId($uid));
} }
} }

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