From 6753eeab8b2f28c0736d0ba446e3b6f854aa1ec2 Mon Sep 17 00:00:00 2001 From: Marek Bachmann Date: Mon, 5 Dec 2022 03:27:51 +0100 Subject: [PATCH 01/21] First draft for using a image grid to display attached images. --- src/Model/Item.php | 37 ++++++++++++++++- view/templates/content/image_grid.tpl | 14 +++++++ view/templates/content/image_grid_column.tpl | 1 + view/theme/frio/css/image_grid.css | 43 ++++++++++++++++++++ 4 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 view/templates/content/image_grid.tpl create mode 100644 view/templates/content/image_grid_column.tpl create mode 100644 view/theme/frio/css/image_grid.css diff --git a/src/Model/Item.php b/src/Model/Item.php index 8026d567cb..1fff0a5869 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -2991,6 +2991,7 @@ class Item $a = DI::app(); Hook::callAll('prepare_body_init', $item); + // In order to provide theme developers more possibilities, event items // are treated differently. if ($item['object-type'] === Activity\ObjectType::EVENT && isset($item['event-id'])) { @@ -3049,6 +3050,7 @@ class Item $item['body'] = self::replaceVisualAttachments($attachments, $item['body'] ?? ''); $item['body'] = preg_replace("/\s*\[attachment .*?\].*?\[\/attachment\]\s*/ism", "\n", $item['body']); + self::putInCache($item); $item['body'] = $body; $s = $item["rendered-html"]; @@ -3082,7 +3084,10 @@ class Item 'filter_reasons' => $filter_reasons ]; Hook::callAll('prepare_body', $hook_data); - $s = $hook_data['html']; + // Remove old images + $hook_data['html'] = preg_replace('|(.*)|', '', $hook_data['html']); + $grid = self::make_image_grid($hook_data); + $s = $hook_data['html'] . $grid; unset($hook_data); if (!$attach) { @@ -3126,6 +3131,36 @@ class Item return $hook_data['html']; } + private function make_image_grid(array &$data) + { + $item = $data['item']; + if ($item['has-media']) { + $attachments = Post\Media::splitAttachments($item['uri-id'], [], $item['has-media'] ?? false); + if (count($attachments['visual']) > 1) { + $img_tags = array(); + foreach ($attachments['visual'] as $attachment) { + $src_url = Post\Media::getUrlForId($attachment['id']); + $preview_url = Post\Media::getPreviewUrlForId($attachment['id'], ($attachment['width'] > $attachment['height']) ? Proxy::SIZE_MEDIUM : Proxy::SIZE_LARGE); + $img_tag = array( + '$image' => [ + 'src' => $src_url, + 'preview' => $preview_url, + 'attachment' => $attachment, + ]); + $img_tags[] = $img_tag; + } + $img_grid = Renderer::replaceMacros(Renderer::getMarkupTemplate('content/image_grid.tpl'), [ + 'columns' => [ + 'fc' => $img_tags[0], + 'sc' => $img_tags[1], + ], + ]); + return $img_grid; + } + } + + } + /** * Check if the body contains a link * diff --git a/view/templates/content/image_grid.tpl b/view/templates/content/image_grid.tpl new file mode 100644 index 0000000000..7a06d33726 --- /dev/null +++ b/view/templates/content/image_grid.tpl @@ -0,0 +1,14 @@ + + +
+
+ {{foreach $columns.fc as $img}} + {{include file="content/image.tpl" image=$img}} + {{/foreach}} +
+
+ {{foreach $columns.sc as $img}} + {{include file="content/image.tpl" image=$img}} + {{/foreach}} +
+
\ No newline at end of file diff --git a/view/templates/content/image_grid_column.tpl b/view/templates/content/image_grid_column.tpl new file mode 100644 index 0000000000..6a58a4bda2 --- /dev/null +++ b/view/templates/content/image_grid_column.tpl @@ -0,0 +1 @@ +
{{$images}}
\ No newline at end of file diff --git a/view/theme/frio/css/image_grid.css b/view/theme/frio/css/image_grid.css new file mode 100644 index 0000000000..dd29dd201d --- /dev/null +++ b/view/theme/frio/css/image_grid.css @@ -0,0 +1,43 @@ +* { + box-sizing: border-box; +} + +.row { + display: -ms-flexbox; /* IE10 */ + display: flex; + -ms-flex-wrap: wrap; /* IE10 */ + flex-wrap: wrap; + padding: 0 4px; +} + +/* Create four equal columns that sits next to each other */ +.column { + -ms-flex: 50%; /* IE10 */ + flex: 50%; + max-width: 50%; + padding: 0 4px; +} + +.column img { + margin-top: 8px; + vertical-align: middle; + width: 100%; +} + +/*!* Responsive layout - makes a two column-layout instead of four columns *!*/ +/*@media screen and (max-width: 50px) {*/ +/* .column {*/ +/* -ms-flex: 50%;*/ +/* flex: 50%;*/ +/* max-width: 50%;*/ +/* }*/ +/*}*/ + +/* Responsive layout - makes the two columns stack on top of each other instead of next to each other */ +@media screen and (max-width: 150px) { + .column { + -ms-flex: 100%; + flex: 100%; + max-width: 100%; + } +} \ No newline at end of file From fbd4b5465606b3930489f91edcd3ef83026372a1 Mon Sep 17 00:00:00 2001 From: Marek Bachmann Date: Mon, 5 Dec 2022 04:23:34 +0100 Subject: [PATCH 02/21] Support more than 2 images --- src/Model/Item.php | 16 ++++++++++++---- view/templates/content/image_grid.tpl | 12 ++++++++---- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/Model/Item.php b/src/Model/Item.php index 1fff0a5869..152543b916 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -3137,7 +3137,9 @@ class Item if ($item['has-media']) { $attachments = Post\Media::splitAttachments($item['uri-id'], [], $item['has-media'] ?? false); if (count($attachments['visual']) > 1) { - $img_tags = array(); + $img_tags_fc = array(); + $img_tags_sc = array(); + $count = 0; foreach ($attachments['visual'] as $attachment) { $src_url = Post\Media::getUrlForId($attachment['id']); $preview_url = Post\Media::getPreviewUrlForId($attachment['id'], ($attachment['width'] > $attachment['height']) ? Proxy::SIZE_MEDIUM : Proxy::SIZE_LARGE); @@ -3147,12 +3149,18 @@ class Item 'preview' => $preview_url, 'attachment' => $attachment, ]); - $img_tags[] = $img_tag; + if ($count % 2 == 0) { + $img_tags_fc[] = $img_tag; + } else { + $img_tags_sc[] = $img_tag; + } + ++$count; } + $img_grid = Renderer::replaceMacros(Renderer::getMarkupTemplate('content/image_grid.tpl'), [ 'columns' => [ - 'fc' => $img_tags[0], - 'sc' => $img_tags[1], + 'fc' => $img_tags_fc, + 'sc' => $img_tags_sc, ], ]); return $img_grid; diff --git a/view/templates/content/image_grid.tpl b/view/templates/content/image_grid.tpl index 7a06d33726..1a8648d189 100644 --- a/view/templates/content/image_grid.tpl +++ b/view/templates/content/image_grid.tpl @@ -2,13 +2,17 @@
- {{foreach $columns.fc as $img}} - {{include file="content/image.tpl" image=$img}} + {{foreach $columns.fc as $fc}} + {{foreach $fc as $img}} + {{include file="content/image.tpl" image=$img}} + {{/foreach}} {{/foreach}}
- {{foreach $columns.sc as $img}} - {{include file="content/image.tpl" image=$img}} + {{foreach $columns.sc as $sc}} + {{foreach $sc as $img}} + {{include file="content/image.tpl" image=$img}} + {{/foreach}} {{/foreach}}
\ No newline at end of file From 733543505be2345b1a763b4924413569193773cc Mon Sep 17 00:00:00 2001 From: Marek Bachmann Date: Mon, 5 Dec 2022 16:54:12 +0100 Subject: [PATCH 03/21] Merge branch 'develop' into new_image_presentation --- database.sql | 133 +++-- doc/FAQ.md | 21 +- doc/database.md | 2 +- doc/database/db_diaspora-contact.md | 52 ++ doc/database/db_fcontact.md | 51 -- src/Console/Relocate.php | 6 +- src/DI.php | 5 + src/Database/DBStructure.php | 2 +- src/Database/Database.php | 18 +- src/Database/PostUpdate.php | 5 + src/Factory/Api/Mastodon/Status.php | 31 +- src/Model/Contact.php | 30 +- src/Model/FContact.php | 161 ------ src/Module/Api/Mastodon/Accounts/Statuses.php | 17 +- src/Module/NoScrape.php | 1 - src/Module/Photo.php | 6 + src/Module/Profile/Status.php | 2 +- src/Network/Probe.php | 39 +- src/Object/Api/Mastodon/Account.php | 4 +- src/Object/Image.php | 52 +- src/Protocol/DFRN.php | 8 +- src/Protocol/Diaspora.php | 529 +++++++++--------- .../Diaspora/Entity/DiasporaContact.php | 140 +++++ .../Diaspora/Factory/DiasporaContact.php | 102 ++++ .../Diaspora/Repository/DiasporaContact.php | 283 ++++++++++ src/Protocol/WebFingerUri.php | 113 ++++ src/Worker/Delivery.php | 5 +- src/Worker/ExpirePosts.php | 2 +- src/Worker/UpdateFContact.php | 41 -- static/dbstructure.config.php | 70 +-- static/dbview.config.php | 69 ++- tests/src/Protocol/WebFingerUriTest.php | 135 +++++ update.php | 8 +- 33 files changed, 1438 insertions(+), 705 deletions(-) create mode 100644 doc/database/db_diaspora-contact.md delete mode 100644 doc/database/db_fcontact.md delete mode 100644 src/Model/FContact.php create mode 100644 src/Protocol/Diaspora/Entity/DiasporaContact.php create mode 100644 src/Protocol/Diaspora/Factory/DiasporaContact.php create mode 100644 src/Protocol/Diaspora/Repository/DiasporaContact.php create mode 100644 src/Protocol/WebFingerUri.php delete mode 100644 src/Worker/UpdateFContact.php create mode 100644 tests/src/Protocol/WebFingerUriTest.php diff --git a/database.sql b/database.sql index 7b3156f6f7..3169bac03a 100644 --- a/database.sql +++ b/database.sql @@ -1,6 +1,6 @@ -- ------------------------------------------ -- Friendica 2022.12-dev (Giant Rhubarb) --- DB_UPDATE_VERSION 1497 +-- DB_UPDATE_VERSION 1500 -- ------------------------------------------ @@ -578,6 +578,40 @@ CREATE TABLE IF NOT EXISTS `delayed-post` ( FOREIGN KEY (`wid`) REFERENCES `workerqueue` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE ) DEFAULT COLLATE utf8mb4_general_ci COMMENT='Posts that are about to be distributed at a later time'; +-- +-- TABLE diaspora-contact +-- +CREATE TABLE IF NOT EXISTS `diaspora-contact` ( + `uri-id` int unsigned NOT NULL COMMENT 'Id of the item-uri table entry that contains the contact URL', + `addr` varchar(255) COMMENT '', + `alias` varchar(255) COMMENT '', + `nick` varchar(255) COMMENT '', + `name` varchar(255) COMMENT '', + `given-name` varchar(255) COMMENT '', + `family-name` varchar(255) COMMENT '', + `photo` varchar(255) COMMENT '', + `photo-medium` varchar(255) COMMENT '', + `photo-small` varchar(255) COMMENT '', + `batch` varchar(255) COMMENT '', + `notify` varchar(255) COMMENT '', + `poll` varchar(255) COMMENT '', + `subscribe` varchar(255) COMMENT '', + `searchable` boolean COMMENT '', + `pubkey` text COMMENT '', + `gsid` int unsigned COMMENT 'Global Server ID', + `created` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT '', + `updated` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT '', + `interacting_count` int unsigned DEFAULT 0 COMMENT 'Number of contacts this contact interactes with', + `interacted_count` int unsigned DEFAULT 0 COMMENT 'Number of contacts that interacted with this contact', + `post_count` int unsigned DEFAULT 0 COMMENT 'Number of posts and comments', + PRIMARY KEY(`uri-id`), + UNIQUE INDEX `addr` (`addr`), + INDEX `alias` (`alias`), + INDEX `gsid` (`gsid`), + FOREIGN KEY (`uri-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE, + FOREIGN KEY (`gsid`) REFERENCES `gserver` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT +) DEFAULT COLLATE utf8mb4_general_ci COMMENT='Diaspora compatible contacts - used in the Diaspora implementation'; + -- -- TABLE diaspora-interaction -- @@ -633,39 +667,6 @@ CREATE TABLE IF NOT EXISTS `event` ( FOREIGN KEY (`uri-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE ) DEFAULT COLLATE utf8mb4_general_ci COMMENT='Events'; --- --- TABLE fcontact --- -CREATE TABLE IF NOT EXISTS `fcontact` ( - `id` int unsigned NOT NULL auto_increment COMMENT 'sequential ID', - `guid` varbinary(255) NOT NULL DEFAULT '' COMMENT 'unique id', - `url` varbinary(383) NOT NULL DEFAULT '' COMMENT '', - `uri-id` int unsigned COMMENT 'Id of the item-uri table entry that contains the fcontact url', - `name` varchar(255) NOT NULL DEFAULT '' COMMENT '', - `photo` varbinary(383) NOT NULL DEFAULT '' COMMENT '', - `request` varbinary(383) NOT NULL DEFAULT '' COMMENT '', - `nick` varchar(255) NOT NULL DEFAULT '' COMMENT '', - `addr` varchar(255) NOT NULL DEFAULT '' COMMENT '', - `batch` varbinary(383) NOT NULL DEFAULT '' COMMENT '', - `notify` varbinary(383) NOT NULL DEFAULT '' COMMENT '', - `poll` varbinary(383) NOT NULL DEFAULT '' COMMENT '', - `confirm` varbinary(383) NOT NULL DEFAULT '' COMMENT '', - `priority` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '', - `network` char(4) NOT NULL DEFAULT '' COMMENT '', - `alias` varbinary(383) NOT NULL DEFAULT '' COMMENT '', - `pubkey` text COMMENT '', - `created` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT '', - `updated` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT '', - `interacting_count` int unsigned DEFAULT 0 COMMENT 'Number of contacts this contact interactes with', - `interacted_count` int unsigned DEFAULT 0 COMMENT 'Number of contacts that interacted with this contact', - `post_count` int unsigned DEFAULT 0 COMMENT 'Number of posts and comments', - PRIMARY KEY(`id`), - INDEX `addr` (`addr`(32)), - UNIQUE INDEX `url` (`url`(190)), - UNIQUE INDEX `uri-id` (`uri-id`), - FOREIGN KEY (`uri-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE -) DEFAULT COLLATE utf8mb4_general_ci COMMENT='Diaspora compatible contacts - used in the Diaspora implementation'; - -- -- TABLE fetch-entry -- @@ -2802,11 +2803,11 @@ CREATE VIEW `account-view` AS SELECT `contact`.`blocked` AS `blocked`, `contact`.`notify` AS `dfrn-notify`, `contact`.`poll` AS `dfrn-poll`, - `fcontact`.`guid` AS `diaspora-guid`, - `fcontact`.`batch` AS `diaspora-batch`, - `fcontact`.`notify` AS `diaspora-notify`, - `fcontact`.`poll` AS `diaspora-poll`, - `fcontact`.`alias` AS `diaspora-alias`, + `item-uri`.`guid` AS `diaspora-guid`, + `diaspora-contact`.`batch` AS `diaspora-batch`, + `diaspora-contact`.`notify` AS `diaspora-notify`, + `diaspora-contact`.`poll` AS `diaspora-poll`, + `diaspora-contact`.`alias` AS `diaspora-alias`, `apcontact`.`uuid` AS `ap-uuid`, `apcontact`.`type` AS `ap-type`, `apcontact`.`following` AS `ap-following`, @@ -2824,7 +2825,7 @@ CREATE VIEW `account-view` AS SELECT FROM `contact` LEFT JOIN `item-uri` ON `item-uri`.`id` = `contact`.`uri-id` LEFT JOIN `apcontact` ON `apcontact`.`uri-id` = `contact`.`uri-id` - LEFT JOIN `fcontact` ON `fcontact`.`uri-id` = contact.`uri-id` + LEFT JOIN `diaspora-contact` ON `diaspora-contact`.`uri-id` = contact.`uri-id` LEFT JOIN `gserver` ON `gserver`.`id` = contact.`gsid` WHERE `contact`.`uid` = 0; @@ -2903,14 +2904,14 @@ CREATE VIEW `account-user-view` AS SELECT `ucontact`.`reason` AS `reason`, `contact`.`notify` AS `dfrn-notify`, `contact`.`poll` AS `dfrn-poll`, - `fcontact`.`guid` AS `diaspora-guid`, - `fcontact`.`batch` AS `diaspora-batch`, - `fcontact`.`notify` AS `diaspora-notify`, - `fcontact`.`poll` AS `diaspora-poll`, - `fcontact`.`alias` AS `diaspora-alias`, - `fcontact`.`interacting_count` AS `diaspora-interacting_count`, - `fcontact`.`interacted_count` AS `diaspora-interacted_count`, - `fcontact`.`post_count` AS `diaspora-post_count`, + `item-uri`.`guid` AS `diaspora-guid`, + `diaspora-contact`.`batch` AS `diaspora-batch`, + `diaspora-contact`.`notify` AS `diaspora-notify`, + `diaspora-contact`.`poll` AS `diaspora-poll`, + `diaspora-contact`.`alias` AS `diaspora-alias`, + `diaspora-contact`.`interacting_count` AS `diaspora-interacting_count`, + `diaspora-contact`.`interacted_count` AS `diaspora-interacted_count`, + `diaspora-contact`.`post_count` AS `diaspora-post_count`, `apcontact`.`uuid` AS `ap-uuid`, `apcontact`.`type` AS `ap-type`, `apcontact`.`following` AS `ap-following`, @@ -2929,7 +2930,7 @@ CREATE VIEW `account-user-view` AS SELECT INNER JOIN `contact` ON `contact`.`uri-id` = `ucontact`.`uri-id` AND `contact`.`uid` = 0 LEFT JOIN `item-uri` ON `item-uri`.`id` = `ucontact`.`uri-id` LEFT JOIN `apcontact` ON `apcontact`.`uri-id` = `ucontact`.`uri-id` - LEFT JOIN `fcontact` ON `fcontact`.`uri-id` = `ucontact`.`uri-id` AND `fcontact`.`network` = 'dspr' + LEFT JOIN `diaspora-contact` ON `diaspora-contact`.`uri-id` = `ucontact`.`uri-id` LEFT JOIN `gserver` ON `gserver`.`id` = contact.`gsid`; -- @@ -3006,3 +3007,37 @@ CREATE VIEW `profile_field-view` AS SELECT `profile_field`.`edited` AS `edited` FROM `profile_field` INNER JOIN `permissionset` ON `permissionset`.`id` = `profile_field`.`psid`; + +-- +-- VIEW diaspora-contact-view +-- +DROP VIEW IF EXISTS `diaspora-contact-view`; +CREATE VIEW `diaspora-contact-view` AS SELECT + `diaspora-contact`.`uri-id` AS `uri-id`, + `item-uri`.`uri` AS `url`, + `item-uri`.`guid` AS `guid`, + `diaspora-contact`.`addr` AS `addr`, + `diaspora-contact`.`alias` AS `alias`, + `diaspora-contact`.`nick` AS `nick`, + `diaspora-contact`.`name` AS `name`, + `diaspora-contact`.`given-name` AS `given-name`, + `diaspora-contact`.`family-name` AS `family-name`, + `diaspora-contact`.`photo` AS `photo`, + `diaspora-contact`.`photo-medium` AS `photo-medium`, + `diaspora-contact`.`photo-small` AS `photo-small`, + `diaspora-contact`.`batch` AS `batch`, + `diaspora-contact`.`notify` AS `notify`, + `diaspora-contact`.`poll` AS `poll`, + `diaspora-contact`.`subscribe` AS `subscribe`, + `diaspora-contact`.`searchable` AS `searchable`, + `diaspora-contact`.`pubkey` AS `pubkey`, + `gserver`.`url` AS `baseurl`, + `diaspora-contact`.`gsid` AS `gsid`, + `diaspora-contact`.`created` AS `created`, + `diaspora-contact`.`updated` AS `updated`, + `diaspora-contact`.`interacting_count` AS `interacting_count`, + `diaspora-contact`.`interacted_count` AS `interacted_count`, + `diaspora-contact`.`post_count` AS `post_count` + FROM `diaspora-contact` + INNER JOIN `item-uri` ON `item-uri`.`id` = `diaspora-contact`.`uri-id` + LEFT JOIN `gserver` ON `gserver`.`id` = `diaspora-contact`.`gsid`; diff --git a/doc/FAQ.md b/doc/FAQ.md index 0f9e115022..63e04a7c3f 100644 --- a/doc/FAQ.md +++ b/doc/FAQ.md @@ -179,26 +179,22 @@ The available features are client specific and may differ. * [AndStatus](http://andstatus.org) ([F-Droid](https://f-droid.org/repository/browse/?fdid=org.andstatus.app), [Google Play](https://play.google.com/store/apps/details?id=org.andstatus.app)) * [B4X for Pleroma & Mastodon](https://github.com/AnywhereSoftware/B4X-Pleroma) -* [Fedi](https://play.google.com/store/apps/details?id=com.fediverse.app) +* [Fedi](https://github.com/Big-Fig/Fediverse.app) ([Google Play](https://play.google.com/store/apps/details?id=com.fediverse.app) * [Fedilab](https://fedilab.app) ([F-Droid](https://f-droid.org/app/fr.gouv.etalab.mastodon), [Google Play](https://play.google.com/store/apps/details?id=app.fedilab.android)) * [Friendiqa](https://git.friendi.ca/lubuwest/Friendiqa) ([F-Droid](https://git.friendi.ca/lubuwest/Friendiqa#install), [Google Play](https://play.google.com/store/apps/details?id=org.qtproject.friendiqa)) * [Husky](https://git.sr.ht/~captainepoch/husky) ([F-Droid](https://f-droid.org/repository/browse/?fdid=su.xash.husky), [Google Play](https://play.google.com/store/apps/details?id=su.xash.husky)) -* [Mastodon for Android](https://github.com/mastodon/mastodon-android) (F-Droid: Pending, [Google-Play](https://play.google.com/store/apps/details?id=org.joinmastodon.android)) -* [Subway Tooter](https://github.com/tateisu/SubwayTooter) -* [Tooot](https://tooot.app/) +* [Mastodon](https://github.com/mastodon/mastodon-android) ([F-Droid](https://f-droid.org/en/packages/org.joinmastodon.android/), [Google Play](https://play.google.com/store/apps/details?id=org.joinmastodon.android)) +* [Subway Tooter](https://github.com/tateisu/SubwayTooter) ([F-Droid](https://android.izzysoft.de/repo/apk/jp.juggler.subwaytooter)) +* [Tooot](https://tooot.app/) ([Google Play](https://play.google.com/store/apps/details?id=com.xmflsct.app.tooot)) * [Tusky](https://tusky.app) ([F-Droid](https://f-droid.org/repository/browse/?fdid=com.keylesspalace.tusky), [Google Play](https://play.google.com/store/apps/details?id=com.keylesspalace.tusky)) * [Twidere](https://github.com/TwidereProject/Twidere-Android) ([F-Droid](https://f-droid.org/repository/browse/?fdid=org.mariotaku.twidere), [Google Play](https://play.google.com/store/apps/details?id=com.twidere.twiderex)) * [TwidereX](https://github.com/TwidereProject/TwidereX-Android) ([F-Droid](https://f-droid.org/en/packages/com.twidere.twiderex/), [Google Play](https://play.google.com/store/apps/details?id=com.twidere.twiderex)) * [Yuito](https://github.com/accelforce/Yuito) ([Google Play](https://play.google.com/store/apps/details?id=net.accelf.yuito)) -#### SailfishOS - -* [Friendly](https://openrepos.net/content/fabrixxm/friendly), last update: 2018 - #### iOS * [B4X for Pleroma & Mastodon](https://github.com/AnywhereSoftware/B4X-Pleroma) ([AppStore](https://apps.apple.com/app/b4x-pleroma/id1538396871)) -* [Fedi](https://fediapp.com) ([AppStore](https://apps.apple.com/de/app/fedi-for-pleroma-and-mastodon/id1478806281)) +* [Fedi](https://github.com/Big-Fig/Fediverse.app) ([AppStore](https://apps.apple.com/de/app/fedi-for-pleroma-and-mastodon/id1478806281)) * [Mastodon for iPhone and iPad](https://joinmastodon.org/apps) ([AppStore](https://apps.apple.com/us/app/mastodon-for-iphone/id1571998974)) * [Stella*](https://www.stella-app.net/) ([AppStore](https://apps.apple.com/us/app/stella-for-mastodon-twitter/id921372048)) * [Tooot](https://github.com/tooot-app) ([AppStore](https://apps.apple.com/app/id1549772269), Data collection (not linked to identity) @@ -207,18 +203,19 @@ The available features are client specific and may differ. #### Linux * [Choqok](https://choqok.kde.org) -* [Whalebird](https://whalebird.social) -* [TheDesk](https://ja.mstdn.wiki/TheDesk) +* [Whalebird](https://whalebird.social/en/desktop/contents) ([GitHub](https://github.com/h3poteto/whalebird-desktop)) +* [TheDesk](https://thedesk.top/en/) ([GitHub](https://github.com/cutls/TheDesk)) * [Toot](https://toot.readthedocs.io/en/latest/) * [Tootle](https://github.com/bleakgrey/tootle) #### macOS -* [Mastonaut](https://mastonaut.app/) ([AppStore](https://apps.apple.com/us/app/mastonaut/id1450757574)), closed source +* [TheDesk](https://thedesk.top/en/) ([GitHub](https://github.com/cutls/TheDesk)) * [Whalebird](https://whalebird.social/en/desktop/contents) ([AppStore](https://apps.apple.com/de/app/whalebird/id1378283354), [GitHub](https://github.com/h3poteto/whalebird-desktop)) #### Windows +* [TheDesk](https://thedesk.top/en/) ([GitHub](https://github.com/cutls/TheDesk)) * [Whalebird](https://whalebird.social/en/desktop/contents) ([Website Download](https://whalebird.social/en/desktop/contents/downloads#windows), [GitHub](https://github.com/h3poteto/whalebird-desktop)) #### Web Frontend diff --git a/doc/database.md b/doc/database.md index ec8d16b2cd..9fa438c8d2 100644 --- a/doc/database.md +++ b/doc/database.md @@ -23,10 +23,10 @@ Database Tables | [contact-relation](help/database/db_contact-relation) | Contact relations | | [conv](help/database/db_conv) | private messages | | [delayed-post](help/database/db_delayed-post) | Posts that are about to be distributed at a later time | +| [diaspora-contact](help/database/db_diaspora-contact) | Diaspora compatible contacts - used in the Diaspora implementation | | [diaspora-interaction](help/database/db_diaspora-interaction) | Signed Diaspora Interaction | | [endpoint](help/database/db_endpoint) | ActivityPub endpoints - used in the ActivityPub implementation | | [event](help/database/db_event) | Events | -| [fcontact](help/database/db_fcontact) | Diaspora compatible contacts - used in the Diaspora implementation | | [fetch-entry](help/database/db_fetch-entry) | | | [fetched-activity](help/database/db_fetched-activity) | Id of fetched activities | | [fsuggest](help/database/db_fsuggest) | friend suggestion stuff | diff --git a/doc/database/db_diaspora-contact.md b/doc/database/db_diaspora-contact.md new file mode 100644 index 0000000000..4beaeb019f --- /dev/null +++ b/doc/database/db_diaspora-contact.md @@ -0,0 +1,52 @@ +Table diaspora-contact +=========== + +Diaspora compatible contacts - used in the Diaspora implementation + +Fields +------ + +| Field | Description | Type | Null | Key | Default | Extra | +| ----------------- | ------------------------------------------------------------ | ------------ | ---- | --- | ------------------- | ----- | +| uri-id | Id of the item-uri table entry that contains the contact URL | int unsigned | NO | PRI | NULL | | +| addr | | varchar(255) | YES | | NULL | | +| alias | | varchar(255) | YES | | NULL | | +| nick | | varchar(255) | YES | | NULL | | +| name | | varchar(255) | YES | | NULL | | +| given-name | | varchar(255) | YES | | NULL | | +| family-name | | varchar(255) | YES | | NULL | | +| photo | | varchar(255) | YES | | NULL | | +| photo-medium | | varchar(255) | YES | | NULL | | +| photo-small | | varchar(255) | YES | | NULL | | +| batch | | varchar(255) | YES | | NULL | | +| notify | | varchar(255) | YES | | NULL | | +| poll | | varchar(255) | YES | | NULL | | +| subscribe | | varchar(255) | YES | | NULL | | +| searchable | | boolean | YES | | NULL | | +| pubkey | | text | YES | | NULL | | +| gsid | Global Server ID | int unsigned | YES | | NULL | | +| created | | datetime | NO | | 0001-01-01 00:00:00 | | +| updated | | datetime | NO | | 0001-01-01 00:00:00 | | +| interacting_count | Number of contacts this contact interactes with | int unsigned | YES | | 0 | | +| interacted_count | Number of contacts that interacted with this contact | int unsigned | YES | | 0 | | +| post_count | Number of posts and comments | int unsigned | YES | | 0 | | + +Indexes +------------ + +| Name | Fields | +| ------- | ------------ | +| PRIMARY | uri-id | +| addr | UNIQUE, addr | +| alias | alias | +| gsid | gsid | + +Foreign Keys +------------ + +| Field | Target Table | Target Field | +|-------|--------------|--------------| +| uri-id | [item-uri](help/database/db_item-uri) | id | +| gsid | [gserver](help/database/db_gserver) | id | + +Return to [database documentation](help/database) diff --git a/doc/database/db_fcontact.md b/doc/database/db_fcontact.md deleted file mode 100644 index 095c47aafd..0000000000 --- a/doc/database/db_fcontact.md +++ /dev/null @@ -1,51 +0,0 @@ -Table fcontact -=========== - -Diaspora compatible contacts - used in the Diaspora implementation - -Fields ------- - -| Field | Description | Type | Null | Key | Default | Extra | -| ----------------- | ------------------------------------------------------------- | ---------------- | ---- | --- | ------------------- | -------------- | -| id | sequential ID | int unsigned | NO | PRI | NULL | auto_increment | -| guid | unique id | varbinary(255) | NO | | | | -| url | | varbinary(383) | NO | | | | -| uri-id | Id of the item-uri table entry that contains the fcontact url | int unsigned | YES | | NULL | | -| name | | varchar(255) | NO | | | | -| photo | | varbinary(383) | NO | | | | -| request | | varbinary(383) | NO | | | | -| nick | | varchar(255) | NO | | | | -| addr | | varchar(255) | NO | | | | -| batch | | varbinary(383) | NO | | | | -| notify | | varbinary(383) | NO | | | | -| poll | | varbinary(383) | NO | | | | -| confirm | | varbinary(383) | NO | | | | -| priority | | tinyint unsigned | NO | | 0 | | -| network | | char(4) | NO | | | | -| alias | | varbinary(383) | NO | | | | -| pubkey | | text | YES | | NULL | | -| created | | datetime | NO | | 0001-01-01 00:00:00 | | -| updated | | datetime | NO | | 0001-01-01 00:00:00 | | -| interacting_count | Number of contacts this contact interactes with | int unsigned | YES | | 0 | | -| interacted_count | Number of contacts that interacted with this contact | int unsigned | YES | | 0 | | -| post_count | Number of posts and comments | int unsigned | YES | | 0 | | - -Indexes ------------- - -| Name | Fields | -| ------- | ---------------- | -| PRIMARY | id | -| addr | addr(32) | -| url | UNIQUE, url(190) | -| uri-id | UNIQUE, uri-id | - -Foreign Keys ------------- - -| Field | Target Table | Target Field | -|-------|--------------|--------------| -| uri-id | [item-uri](help/database/db_item-uri) | id | - -Return to [database documentation](help/database) diff --git a/src/Console/Relocate.php b/src/Console/Relocate.php index 729bee392f..a90802d81e 100644 --- a/src/Console/Relocate.php +++ b/src/Console/Relocate.php @@ -131,9 +131,9 @@ HELP; $this->out('Updating event table fields'); $this->database->replaceInTableFields('event', ['uri'], $old_url, $new_url); - $this->out('Updating fcontact table fields'); - $this->database->replaceInTableFields('fcontact', ['url', 'photo', 'request', 'batch', 'poll', 'confirm', 'alias'], $old_url, $new_url); - $this->database->replaceInTableFields('fcontact', ['addr'], $old_host, $new_host); + $this->out('Updating diaspora-contact table fields'); + $this->database->replaceInTableFields('diaspora-contact', ['alias', 'photo', 'photo-medium', 'photo-small', 'batch', 'notify', 'poll', 'subscribe'], $old_url, $new_url); + $this->database->replaceInTableFields('diaspora-contact', ['addr'], $old_host, $new_host); $this->out('Updating fsuggest table fields'); $this->database->replaceInTableFields('fsuggest', ['url', 'request', 'photo'], $old_url, $new_url); diff --git a/src/DI.php b/src/DI.php index 3a8a9b6d24..59f48fcb5f 100644 --- a/src/DI.php +++ b/src/DI.php @@ -599,6 +599,11 @@ abstract class DI return self::$dice->create(Protocol\Activity::class); } + public static function dsprContact(): Protocol\Diaspora\Repository\DiasporaContact + { + return self::$dice->create(Protocol\Diaspora\Repository\DiasporaContact::class); + } + // // "Security" namespace instances // diff --git a/src/Database/DBStructure.php b/src/Database/DBStructure.php index 74bd5b4237..6e50c9fb28 100644 --- a/src/Database/DBStructure.php +++ b/src/Database/DBStructure.php @@ -74,7 +74,7 @@ class DBStructure $old_tables = ['fserver', 'gcign', 'gcontact', 'gcontact-relation', 'gfollower' ,'glink', 'item-delivery-data', 'item-activity', 'item-content', 'item_id', 'participation', 'poll', 'poll_result', 'queue', 'retriever_rule', 'deliverq', 'dsprphotoq', 'ffinder', 'sign', 'spam', 'term', 'user-item', 'thread', 'item', 'challenge', - 'auth_codes', 'tokens', 'clients', 'profile_check', 'host', 'conversation']; + 'auth_codes', 'tokens', 'clients', 'profile_check', 'host', 'conversation', 'fcontact']; $tables = DBA::selectToArray('INFORMATION_SCHEMA.TABLES', ['TABLE_NAME'], ['TABLE_SCHEMA' => DBA::databaseName(), 'TABLE_TYPE' => 'BASE TABLE']); diff --git a/src/Database/Database.php b/src/Database/Database.php index 036e6ec2cb..b5d4963923 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -833,7 +833,7 @@ class Database /** * Check if data exists * - * @param string $table Table name in format schema.table (while scheme is optiona) + * @param string $table Table name in format [schema.]table * @param array $condition Array of fields for condition * * @return boolean Are there rows for that condition? @@ -1017,7 +1017,7 @@ class Database /** * Insert a row into a table. Field value objects will be cast as string. * - * @param string $table Table name in format schema.table (while scheme is optiona) + * @param string $table Table name in format [schema.]table * @param array $param parameter array * @param int $duplicate_mode What to do on a duplicated entry * @@ -1068,7 +1068,7 @@ class Database * Inserts a row with the provided data in the provided table. * If the data corresponds to an existing row through a UNIQUE or PRIMARY index constraints, it updates the row instead. * - * @param string $table Table name in format schema.table (while scheme is optiona) + * @param string $table Table name in format [schema.]table * @param array $param parameter array * @return boolean was the insert successful? * @throws \Exception @@ -1116,7 +1116,7 @@ class Database * * This function can be extended in the future to accept a table array as well. * - * @param string $table Table name in format schema.table (while scheme is optiona) + * @param string $table Table name in format [schema.]table * @return boolean was the lock successful? * @throws \Exception */ @@ -1314,7 +1314,7 @@ class Database * Only set $old_fields to a boolean value when you are sure that you will update a single row. * When you set $old_fields to "true" then $fields must contain all relevant fields! * - * @param string $table Table name in format schema.table (while scheme is optiona) + * @param string $table Table name in format [schema.]table * @param array $fields contains the fields that are updated * @param array $condition condition array with the key values * @param array|boolean $old_fields array with the old field values that are about to be replaced (true = update on duplicate, false = don't update identical fields) @@ -1380,7 +1380,7 @@ class Database /** * Retrieve a single record from a table and returns it in an associative array * - * @param string $table Table name in format schema.table (while scheme is optiona) + * @param string $table Table name in format [schema.]table * @param array $fields Array of selected fields, empty for all * @param array $condition Array of fields for condition * @param array $params Array of several parameters @@ -1406,7 +1406,7 @@ class Database /** * Select rows from a table and fills an array with the data * - * @param string $table Table name in format schema.table (while scheme is optiona) + * @param string $table Table name in format [schema.]table * @param array $fields Array of selected fields, empty for all * @param array $condition Array of fields for condition * @param array $params Array of several parameters @@ -1479,7 +1479,7 @@ class Database * * $data = DBA::select($table, $fields, $condition, $params); * - * @param string $table Table name in format schema.table (while scheme is optiona) + * @param string $table Table name in format [schema.]table * @param array $fields Array of selected fields, empty for all * @param array $condition Array of fields for condition * @param array $params Array of several parameters @@ -1519,7 +1519,7 @@ class Database /** * Counts the rows from a table satisfying the provided condition * - * @param string $table Table name in format schema.table (while scheme is optiona) + * @param string $table Table name in format [schema.]table * @param array $condition Array of fields for condition * @param array $params Array of several parameters * diff --git a/src/Database/PostUpdate.php b/src/Database/PostUpdate.php index adc88b13e8..e57d92ceb7 100644 --- a/src/Database/PostUpdate.php +++ b/src/Database/PostUpdate.php @@ -899,6 +899,11 @@ class PostUpdate return true; } + if (!DBStructure::existsTable('fcontact')) { + DI::config()->set('system', 'post_update_version', 1425); + return true; + } + $condition = ["`uri-id` IS NULL"]; Logger::info('Start', ['rest' => DBA::count('fcontact', $condition)]); diff --git a/src/Factory/Api/Mastodon/Status.php b/src/Factory/Api/Mastodon/Status.php index dba74b1407..94b42ce79c 100644 --- a/src/Factory/Api/Mastodon/Status.php +++ b/src/Factory/Api/Mastodon/Status.php @@ -86,7 +86,7 @@ class Status extends BaseFactory */ public function createFromUriId(int $uriId, int $uid = 0, bool $reblog = true): \Friendica\Object\Api\Mastodon\Status { - $fields = ['uri-id', 'uid', 'author-id', 'author-uri-id', 'author-link', 'causer-uri-id', 'post-reason', 'starred', 'app', 'title', 'body', 'raw-body', 'content-warning', 'question-id', + $fields = ['uri-id', 'uid', 'author-id', 'causer-id', 'author-uri-id', 'author-link', 'causer-uri-id', 'post-reason', 'starred', 'app', 'title', 'body', 'raw-body', 'content-warning', 'question-id', 'created', 'network', 'thr-parent-id', 'parent-author-id', 'language', 'uri', 'plink', 'private', 'vid', 'gravity', 'featured', 'has-media', 'quote-uri-id']; $item = Post::selectFirst($fields, ['uri-id' => $uriId, 'uid' => [0, $uid]], ['order' => ['uid' => true]]); if (!$item) { @@ -97,9 +97,18 @@ class Status extends BaseFactory throw new HTTPException\NotFoundException('Item with URI ID ' . $uriId . ' not found' . ($uid ? ' for user ' . $uid : '.')); } - $is_reshare = $reblog && !is_null($item['causer-uri-id']) && ($item['post-reason'] == Item::PR_ANNOUNCEMENT); - - $account = $this->mstdnAccountFactory->createFromUriId($is_reshare ? $item['causer-uri-id']:$item['author-uri-id'], $uid); + if (($item['gravity'] == Item::GRAVITY_ACTIVITY) && ($item['vid'] == Verb::getID(Activity::ANNOUNCE))) { + $is_reshare = true; + $account = $this->mstdnAccountFactory->createFromUriId($item['author-uri-id'], $uid); + $uriId = $item['thr-parent-id']; + $item = Post::selectFirst($fields, ['uri-id' => $uriId, 'uid' => [0, $uid]], ['order' => ['uid' => true]]); + if (!$item) { + throw new HTTPException\NotFoundException('Item with URI ID ' . $uriId . ' not found' . ($uid ? ' for user ' . $uid : '.')); + } + } else { + $is_reshare = $reblog && !is_null($item['causer-uri-id']) && ($item['causer-id'] != $item['author-id']) && ($item['post-reason'] == Item::PR_ANNOUNCEMENT); + $account = $this->mstdnAccountFactory->createFromUriId($is_reshare ? $item['causer-uri-id'] : $item['author-uri-id'], $uid); + } $count_announce = Post::countPosts([ 'thr-parent-id' => $uriId, @@ -190,19 +199,13 @@ class Status extends BaseFactory } } - if ($item['vid'] == Verb::getID(Activity::ANNOUNCE)) { - $reshare = $this->createFromUriId($item['thr-parent-id'], $uid)->toArray(); - $reshared_item = Post::selectFirst(['title', 'body'], ['uri-id' => $item['thr-parent-id'],'uid' => [0, $uid]]); - $item['title'] = $reshared_item['title'] ?? $item['title']; - $item['body'] = $reshared_item['body'] ?? $item['body']; - } else { - $item['body'] = $this->contentItem->addSharedPost($item); - $item['raw-body'] = $this->contentItem->addSharedPost($item, $item['raw-body']); - $reshare = []; - } + $item['body'] = $this->contentItem->addSharedPost($item); + $item['raw-body'] = $this->contentItem->addSharedPost($item, $item['raw-body']); if ($is_reshare) { $reshare = $this->createFromUriId($uriId, $uid, false)->toArray(); + } else { + $reshare = []; } return new \Friendica\Object\Api\Mastodon\Status($item, $account, $counts, $userAttributes, $sensitive, $application, $mentions, $tags, $card, $attachments, $reshare, $poll); diff --git a/src/Model/Contact.php b/src/Model/Contact.php index 34ce8e684e..e80f0752ea 100644 --- a/src/Model/Contact.php +++ b/src/Model/Contact.php @@ -1395,7 +1395,7 @@ class Contact } if ($data['network'] == Protocol::DIASPORA) { - FContact::updateFromProbeArray($data); + DI::dsprContact()->updateFromProbeArray($data); } self::updateFromProbeArray($contact_id, $data); @@ -2057,9 +2057,10 @@ class Contact * @param integer $cid contact id * @param string $size One of the Proxy::SIZE_* constants * @param string $updated Contact update date + * @param bool $static If "true" a parameter is added to convert the avatar to a static one * @return string avatar link */ - public static function getAvatarUrlForId(int $cid, string $size = '', string $updated = '', string $guid = ''): string + public static function getAvatarUrlForId(int $cid, string $size = '', string $updated = '', string $guid = '', bool $static = false): string { // We have to fetch the "updated" variable when it wasn't provided // The parameter can be provided to improve performance @@ -2089,7 +2090,15 @@ class Contact $url .= Proxy::PIXEL_LARGE . '/'; break; } - return $url . ($guid ?: $cid) . ($updated ? '?ts=' . strtotime($updated) : ''); + $query_params = []; + if ($updated) { + $query_params['ts'] = strtotime($updated); + } + if ($static) { + $query_params['static'] = true; + } + + return $url . ($guid ?: $cid) . (!empty($query_params) ? '?' . http_build_query($query_params) : ''); } /** @@ -2114,9 +2123,10 @@ class Contact * @param integer $cid contact id * @param string $size One of the Proxy::SIZE_* constants * @param string $updated Contact update date + * @param bool $static If "true" a parameter is added to convert the header to a static one * @return string header link */ - public static function getHeaderUrlForId(int $cid, string $size = '', string $updated = '', string $guid = ''): string + public static function getHeaderUrlForId(int $cid, string $size = '', string $updated = '', string $guid = '', bool $static = false): string { // We have to fetch the "updated" variable when it wasn't provided // The parameter can be provided to improve performance @@ -2147,7 +2157,15 @@ class Contact break; } - return $url . ($guid ?: $cid) . ($updated ? '?ts=' . strtotime($updated) : ''); + $query_params = []; + if ($updated) { + $query_params['ts'] = strtotime($updated); + } + if ($static) { + $query_params['static'] = true; + } + + return $url . ($guid ?: $cid) . (!empty($query_params) ? '?' . http_build_query($query_params) : ''); } /** @@ -2468,7 +2486,7 @@ class Contact $ret = Probe::uri($contact['url'], $network, $contact['uid']); if ($ret['network'] == Protocol::DIASPORA) { - FContact::updateFromProbeArray($ret); + DI::dsprContact()->updateFromProbeArray($ret); } return self::updateFromProbeArray($id, $ret); diff --git a/src/Model/FContact.php b/src/Model/FContact.php deleted file mode 100644 index e13dcd46c9..0000000000 --- a/src/Model/FContact.php +++ /dev/null @@ -1,161 +0,0 @@ -. - * - */ - -namespace Friendica\Model; - -use Friendica\Core\Logger; -use Friendica\Core\Protocol; -use Friendica\Core\Worker; -use Friendica\Database\DBA; -use Friendica\DI; -use Friendica\Model\Item; -use Friendica\Network\Probe; -use Friendica\Util\DateTimeFormat; -use Friendica\Util\Strings; - -class FContact -{ - /** - * Fetches data for a given handle - * - * @param string $handle The handle - * @param boolean $update true = always update, false = never update, null = update when not found or outdated - * - * @return array the queried data - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - * @throws \ImagickException - */ - public static function getByURL(string $handle, $update = null): array - { - Logger::debug('Fetch fcontact', ['handle' => $handle, 'update' => $update]); - $person = DBA::selectFirst('fcontact', [], ['network' => Protocol::DIASPORA, 'addr' => $handle]); - if (!DBA::isResult($person)) { - $urls = [$handle, str_replace('http://', 'https://', $handle), Strings::normaliseLink($handle)]; - $person = DBA::selectFirst('fcontact', [], ['network' => Protocol::DIASPORA, 'url' => $urls]); - } - - if (DBA::isResult($person)) { - Logger::debug('In cache', ['handle' => $handle]); - - if (is_null($update)) { - $update = empty($person['guid']) || empty($person['uri-id']) || ($person['created'] <= DBA::NULL_DATETIME); - if (GServer::getNextUpdateDate(true, $person['created'], $person['updated'], false) < DateTimeFormat::utcNow()) { - Logger::debug('Start background update', ['handle' => $handle]); - Worker::add(['priority' => Worker::PRIORITY_LOW, 'dont_fork' => true], 'UpdateFContact', $handle); - } - } - } elseif (is_null($update)) { - $update = true; - } else { - $person = []; - } - - if ($update) { - Logger::info('create or refresh', ['handle' => $handle]); - $data = Probe::uri($handle, Protocol::DIASPORA); - - // Note that Friendica contacts will return a "Diaspora person" - // if Diaspora connectivity is enabled on their server - if ($data['network'] ?? '' === Protocol::DIASPORA) { - self::updateFromProbeArray($data); - - $person = self::getByURL($handle, false); - } - } - - return $person; - } - - /** - * Updates the fcontact table - * - * @param array $arr The fcontact data - * @throws \Exception - */ - public static function updateFromProbeArray(array $arr) - { - $uriid = ItemURI::insert(['uri' => $arr['url'], 'guid' => $arr['guid']]); - - $fcontact = DBA::selectFirst('fcontact', ['created'], ['url' => $arr['url'], 'network' => $arr['network']]); - $contact = Contact::getByUriId($uriid, ['id', 'created']); - $apcontact = APContact::getByURL($arr['url'], false); - if (!empty($apcontact)) { - $interacted = $apcontact['following_count']; - $interacting = $apcontact['followers_count']; - $posts = $apcontact['statuses_count']; - } elseif (!empty($contact['id'])) { - $last_interaction = DateTimeFormat::utc('now - 180 days'); - - $interacted = DBA::count('contact-relation', ["`cid` = ? AND NOT `follows` AND `last-interaction` > ?", $contact['id'], $last_interaction]); - $interacting = DBA::count('contact-relation', ["`relation-cid` = ? AND NOT `follows` AND `last-interaction` > ?", $contact['id'], $last_interaction]); - $posts = DBA::count('post', ['author-id' => $contact['id'], 'gravity' => [Item::GRAVITY_PARENT, Item::GRAVITY_COMMENT]]); - } - - $fields = [ - 'name' => $arr['name'], - 'photo' => $arr['photo'], - 'request' => $arr['request'], - 'nick' => $arr['nick'], - 'addr' => strtolower($arr['addr']), - 'guid' => $arr['guid'], - 'batch' => $arr['batch'], - 'notify' => $arr['notify'], - 'poll' => $arr['poll'], - 'confirm' => $arr['confirm'], - 'alias' => $arr['alias'], - 'pubkey' => $arr['pubkey'], - 'uri-id' => $uriid, - 'interacting_count' => $interacting ?? 0, - 'interacted_count' => $interacted ?? 0, - 'post_count' => $posts ?? 0, - 'updated' => DateTimeFormat::utcNow(), - ]; - - if (empty($fcontact['created'])) { - $fields['created'] = $fields['updated']; - } elseif (!empty($contact['created']) && ($fcontact['created'] <= DBA::NULL_DATETIME)) { - $fields['created'] = $contact['created']; - } - - $fields = DI::dbaDefinition()->truncateFieldsForTable('fcontact', $fields); - DBA::update('fcontact', $fields, ['url' => $arr['url'], 'network' => $arr['network']], true); - } - - /** - * get a url (scheme://domain.tld/u/user) from a given Diaspora* - * fcontact guid - * - * @param string $fcontact_guid Hexadecimal string guid - * @return string|null the contact url or null - * @throws \Exception - */ - public static function getUrlByGuid(string $fcontact_guid) - { - Logger::info('fcontact', ['guid' => $fcontact_guid]); - - $fcontact = DBA::selectFirst('fcontact', ['url'], ["`url` != ? AND `network` = ? AND `guid` = ?", '', Protocol::DIASPORA, $fcontact_guid]); - if (DBA::isResult($fcontact)) { - return $fcontact['url']; - } - - return null; - } -} diff --git a/src/Module/Api/Mastodon/Accounts/Statuses.php b/src/Module/Api/Mastodon/Accounts/Statuses.php index 7de8699401..b9bb63ae78 100644 --- a/src/Module/Api/Mastodon/Accounts/Statuses.php +++ b/src/Module/Api/Mastodon/Accounts/Statuses.php @@ -80,8 +80,15 @@ class Statuses extends BaseApi } if (!$request['pinned'] && !$request['only_media']) { - $condition = DBA::mergeConditions($condition, ["(`gravity` IN (?, ?) OR (`gravity` = ? AND `vid` = ?))", - Item::GRAVITY_PARENT, Item::GRAVITY_COMMENT, Item::GRAVITY_ACTIVITY, Verb::getID(Activity::ANNOUNCE)]); + if ($request['exclude_replies']) { + $condition = DBA::mergeConditions($condition, ["(`gravity` = ? OR (`gravity` = ? AND `vid` = ?))", + Item::GRAVITY_PARENT, Item::GRAVITY_ACTIVITY, Verb::getID(Activity::ANNOUNCE)]); + } else { + $condition = DBA::mergeConditions($condition, ["(`gravity` IN (?, ?) OR (`gravity` = ? AND `vid` = ?))", + Item::GRAVITY_PARENT, Item::GRAVITY_COMMENT, Item::GRAVITY_ACTIVITY, Verb::getID(Activity::ANNOUNCE)]); + } + } elseif ($request['exclude_replies']) { + $condition = DBA::mergeConditions($condition, ['gravity' => Item::GRAVITY_PARENT]); } if (!empty($request['max_id'])) { @@ -97,16 +104,10 @@ class Statuses extends BaseApi $params['order'] = ['uri-id']; } - if (($request['pinned'] || $request['only_media']) && $request['exclude_replies']) { - $condition = DBA::mergeConditions($condition, ['gravity' => Item::GRAVITY_PARENT]); - } - if ($request['pinned']) { $items = DBA::select('collection-view', ['uri-id'], $condition, $params); } elseif ($request['only_media']) { $items = DBA::select('media-view', ['uri-id'], $condition, $params); - } elseif ($request['exclude_replies']) { - $items = Post::selectThreadForUser($uid, ['uri-id'], $condition, $params); } else { $items = Post::selectForUser($uid, ['uri-id'], $condition, $params); } diff --git a/src/Module/NoScrape.php b/src/Module/NoScrape.php index 718c95c107..8e5850ac0b 100644 --- a/src/Module/NoScrape.php +++ b/src/Module/NoScrape.php @@ -23,7 +23,6 @@ namespace Friendica\Module; use Friendica\BaseModule; use Friendica\Core\System; -use Friendica\Database\DBA; use Friendica\DI; use Friendica\Model\APContact; use Friendica\Model\User; diff --git a/src/Module/Photo.php b/src/Module/Photo.php index a4ef6ab414..78e403e6c7 100644 --- a/src/Module/Photo.php +++ b/src/Module/Photo.php @@ -182,6 +182,12 @@ class Photo extends BaseModule throw new HTTPException\InternalServerErrorException($error); } + if (!empty($request['static'])) { + $img = new Image($imgdata, $photo['type']); + $img->toStatic(); + $imgdata = $img->asString(); + } + // if customsize is set and image is not a gif, resize it if ($photo['type'] !== 'image/gif' && $customsize > 0 && $customsize <= Proxy::PIXEL_THUMB && $square_resize) { $img = new Image($imgdata, $photo['type']); diff --git a/src/Module/Profile/Status.php b/src/Module/Profile/Status.php index 0a4a17e462..2ffe91feb5 100644 --- a/src/Module/Profile/Status.php +++ b/src/Module/Profile/Status.php @@ -173,7 +173,7 @@ class Status extends BaseProfile $condition = DBA::mergeConditions($condition, ["((`gravity` = ? AND `wall`) OR (`gravity` = ? AND `vid` = ? AND `origin` - AND `thr-parent-id` IN (SELECT `uri-id` FROM `post` WHERE `gravity` = ? AND `network` IN (?, ?))))", + AND EXISTS(SELECT `uri-id` FROM `post` WHERE `uri-id` = `post-user-view`.`thr-parent-id` AND `gravity` = ? AND `network` IN (?, ?))))", Item::GRAVITY_PARENT, Item::GRAVITY_ACTIVITY, Verb::getID(Activity::ANNOUNCE), Item::GRAVITY_PARENT, Protocol::ACTIVITYPUB, Protocol::DFRN]); $condition = DBA::mergeConditions($condition, ['uid' => $profile['uid'], 'network' => Protocol::FEDERATED, diff --git a/src/Network/Probe.php b/src/Network/Probe.php index 5c188f2522..d79cd4055f 100644 --- a/src/Network/Probe.php +++ b/src/Network/Probe.php @@ -110,7 +110,8 @@ class Probe */ private static function rearrangeData(array $data): array { - $fields = ['name', 'nick', 'guid', 'url', 'addr', 'alias', 'photo', 'header', + $fields = ['name', 'given_name', 'family_name', 'nick', 'guid', 'url', 'addr', 'alias', + 'photo', 'photo_medium', 'photo_small', 'header', 'account-type', 'community', 'keywords', 'location', 'about', 'xmpp', 'matrix', 'hide', 'batch', 'notify', 'poll', 'request', 'confirm', 'subscribe', 'poco', 'following', 'followers', 'inbox', 'outbox', 'sharedinbox', @@ -124,7 +125,7 @@ class Probe if (in_array($field, $numeric_fields)) { $newdata[$field] = (int)$data[$field]; } else { - $newdata[$field] = $data[$field]; + $newdata[$field] = trim($data[$field]); } } elseif (!in_array($field, $numeric_fields)) { $newdata[$field] = ''; @@ -1290,9 +1291,19 @@ class Probe $data['name'] = $search->item(0)->nodeValue; } + $search = $xpath->query("//*[contains(concat(' ', @class, ' '), ' given_name ')]", $vcard); // */ + if ($search->length > 0) { + $data["given_name"] = $search->item(0)->nodeValue; + } + + $search = $xpath->query("//*[contains(concat(' ', @class, ' '), ' family_name ')]", $vcard); // */ + if ($search->length > 0) { + $data["family_name"] = $search->item(0)->nodeValue; + } + $search = $xpath->query("//*[contains(concat(' ', @class, ' '), ' searchable ')]", $vcard); // */ if ($search->length > 0) { - $data['searchable'] = $search->item(0)->nodeValue; + $data['hide'] = (strtolower($search->item(0)->nodeValue) != 'true'); } $search = $xpath->query("//*[contains(concat(' ', @class, ' '), ' key ')]", $vcard); // */ @@ -1309,7 +1320,7 @@ class Probe } } - $avatar = []; + $avatars = []; if (!empty($vcard)) { $photos = $xpath->query("//*[contains(concat(' ', @class, ' '), ' photo ') or contains(concat(' ', @class, ' '), ' avatar ')]", $vcard); // */ foreach ($photos as $photo) { @@ -1319,20 +1330,27 @@ class Probe } if (isset($attr['src']) && isset($attr['width'])) { - $avatar[$attr['width']] = $attr['src']; + $avatars[$attr['width']] = self::fixAvatar($attr['src'], $data['baseurl']); } // We don't have a width. So we just take everything that we got. // This is a Hubzilla workaround which doesn't send a width. - if ((sizeof($avatar) == 0) && !empty($attr['src'])) { - $avatar[] = $attr['src']; + if (!$avatars && !empty($attr['src'])) { + $avatars[] = self::fixAvatar($attr['src'], $data['baseurl']); } } } - if (sizeof($avatar)) { - ksort($avatar); - $data['photo'] = self::fixAvatar(array_pop($avatar), $data['baseurl']); + if ($avatars) { + ksort($avatars); + $data['photo'] = array_pop($avatars); + if ($avatars) { + $data['photo_medium'] = array_pop($avatars); + } + + if ($avatars) { + $data['photo_small'] = array_pop($avatars); + } } if ($dfrn) { @@ -1356,7 +1374,6 @@ class Probe } } - return $data; } diff --git a/src/Object/Api/Mastodon/Account.php b/src/Object/Api/Mastodon/Account.php index 3369a18c00..afc739e1c6 100644 --- a/src/Object/Api/Mastodon/Account.php +++ b/src/Object/Api/Mastodon/Account.php @@ -109,9 +109,9 @@ class Account extends BaseDataTransferObject $this->note = BBCode::convertForUriId($account['uri-id'], $account['about'], BBCode::EXTERNAL); $this->url = $account['url']; $this->avatar = Contact::getAvatarUrlForId($account['id'] ?? 0 ?: $account['pid'], Proxy::SIZE_SMALL, $account['updated'], $account['guid'] ?? ''); - $this->avatar_static = $this->avatar; + $this->avatar_static = Contact::getAvatarUrlForId($account['id'] ?? 0 ?: $account['pid'], Proxy::SIZE_SMALL, $account['updated'], $account['guid'] ?? '', true); $this->header = Contact::getHeaderUrlForId($account['id'] ?? 0 ?: $account['pid'], '', $account['updated'], $account['guid'] ?? ''); - $this->header_static = $this->header; + $this->header_static = Contact::getHeaderUrlForId($account['id'] ?? 0 ?: $account['pid'], '', $account['updated'], $account['guid'] ?? '', true); $this->followers_count = $account['ap-followers_count'] ?? $account['diaspora-interacted_count'] ?? 0; $this->following_count = $account['ap-following_count'] ?? $account['diaspora-interacting_count'] ?? 0; $this->statuses_count = $account['ap-statuses_count'] ?? $account['diaspora-post_count'] ?? 0; diff --git a/src/Object/Image.php b/src/Object/Image.php index 87401304db..6eb8620031 100644 --- a/src/Object/Image.php +++ b/src/Object/Image.php @@ -25,6 +25,7 @@ use Exception; use Friendica\DI; use Friendica\Util\Images; use Imagick; +use ImagickDraw; use ImagickPixel; use GDImage; use kornrunner\Blurhash\Blurhash; @@ -64,7 +65,7 @@ class Image } $this->type = $type; - if ($this->isImagick() && $this->loadData($data)) { + if ($this->isImagick() && (empty($data) || $this->loadData($data))) { return; } else { // Failed to load with Imagick, fallback @@ -732,11 +733,6 @@ class Image */ public function getBlurHash(): string { - if ($this->isImagick()) { - // Imagick is not supported - return ''; - } - $width = $this->getWidth(); $height = $this->getHeight(); @@ -750,10 +746,14 @@ class Image for ($y = 0; $y < $height; ++$y) { $row = []; for ($x = 0; $x < $width; ++$x) { - $index = imagecolorat($this->image, $x, $y); - $colors = imagecolorsforindex($this->image, $index); - - $row[] = [$colors['red'], $colors['green'], $colors['blue']]; + if ($this->isImagick()) { + $colors = $this->image->getImagePixelColor($x, $y)->getColor(); + $row[] = [$colors['r'], $colors['g'], $colors['b']]; + } else { + $index = imagecolorat($this->image, $x, $y); + $colors = @imagecolorsforindex($this->image, $index); + $row[] = [$colors['red'], $colors['green'], $colors['blue']]; + } } $pixels[] = $row; } @@ -775,25 +775,37 @@ class Image */ public function getFromBlurHash(string $blurhash, int $width, int $height) { - if ($this->isImagick()) { - // Imagick is not supported - return; - } - $scaled = Images::getScalingDimensions($width, $height, 90); $pixels = Blurhash::decode($blurhash, $scaled['width'], $scaled['height']); - $this->image = imagecreatetruecolor($scaled['width'], $scaled['height']); + if ($this->isImagick()) { + $this->image = new Imagick(); + $draw = new ImagickDraw(); + $this->image->newImage($scaled['width'], $scaled['height'], '', 'png'); + } else { + $this->image = imagecreatetruecolor($scaled['width'], $scaled['height']); + } + for ($y = 0; $y < $scaled['height']; ++$y) { for ($x = 0; $x < $scaled['width']; ++$x) { [$r, $g, $b] = $pixels[$y][$x]; - imagesetpixel($this->image, $x, $y, imagecolorallocate($this->image, $r, $g, $b)); + if ($this->isImagick()) { + $draw->setFillColor("rgb($r, $g, $b)"); + $draw->point($x, $y); + } else { + imagesetpixel($this->image, $x, $y, imagecolorallocate($this->image, $r, $g, $b)); + } } } - $this->width = imagesx($this->image); - $this->height = imagesy($this->image); - $this->valid = true; + if ($this->isImagick()) { + $this->image->drawImage($draw); + } else { + $this->width = imagesx($this->image); + $this->height = imagesy($this->image); + } + + $this->valid = true; $this->scaleUp(min($width, $height)); } diff --git a/src/Protocol/DFRN.php b/src/Protocol/DFRN.php index a86cf2094d..92f3e81545 100644 --- a/src/Protocol/DFRN.php +++ b/src/Protocol/DFRN.php @@ -34,7 +34,6 @@ use Friendica\DI; use Friendica\Model\Contact; use Friendica\Model\Conversation; use Friendica\Model\Event; -use Friendica\Model\FContact; use Friendica\Model\GServer; use Friendica\Model\Item; use Friendica\Model\ItemURI; @@ -45,6 +44,7 @@ use Friendica\Model\Post; use Friendica\Model\Profile; use Friendica\Model\Tag; use Friendica\Model\User; +use Friendica\Network\HTTPException; use Friendica\Network\Probe; use Friendica\Util\Crypto; use Friendica\Util\DateTimeFormat; @@ -981,12 +981,12 @@ class DFRN } } - $fcontact = FContact::getByURL($contact['addr']); - if (empty($fcontact)) { + try { + $pubkey = DI::dsprContact()->getByAddr(WebFingerUri::fromString($contact['addr']))->pubKey; + } catch (HTTPException\NotFoundException|\InvalidArgumentException $e) { Logger::notice('Unable to find contact details for ' . $contact['id'] . ' - ' . $contact['addr']); return -22; } - $pubkey = $fcontact['pubkey'] ?? ''; } else { $pubkey = ''; } diff --git a/src/Protocol/Diaspora.php b/src/Protocol/Diaspora.php index 54f09e9d97..bb955dca1e 100644 --- a/src/Protocol/Diaspora.php +++ b/src/Protocol/Diaspora.php @@ -33,7 +33,6 @@ use Friendica\Database\DBA; use Friendica\DI; use Friendica\Model\Contact; use Friendica\Model\Conversation; -use Friendica\Model\FContact; use Friendica\Model\GServer; use Friendica\Model\Item; use Friendica\Model\ItemURI; @@ -42,6 +41,7 @@ use Friendica\Model\Post; use Friendica\Model\Tag; use Friendica\Model\User; use Friendica\Network\HTTPClient\Client\HttpClientAccept; +use Friendica\Network\HTTPException; use Friendica\Network\Probe; use Friendica\Util\Crypto; use Friendica\Util\DateTimeFormat; @@ -161,8 +161,12 @@ class Diaspora return false; } - $key = self::key($handle); - if ($key == '') { + try { + $key = self::key(WebFingerUri::fromString($handle)); + if ($key == '') { + throw new \InvalidArgumentException(); + } + } catch (\InvalidArgumentException $e) { Logger::notice("Couldn't get a key for handle " . $handle . ". Discarding."); return false; } @@ -300,8 +304,13 @@ class Diaspora } } - $key = self::key($author_addr); - if ($key == '') { + try { + $author = WebFingerUri::fromString($author_addr); + $key = self::key($author); + if ($key == '') { + throw new \InvalidArgumentException(); + } + } catch (\InvalidArgumentException $e) { Logger::notice("Couldn't get a key for handle " . $author_addr . ". Discarding."); if ($no_exit) { return false; @@ -322,8 +331,8 @@ class Diaspora return [ 'message' => (string)Strings::base64UrlDecode($base->data), - 'author' => XML::unescape($author_addr), - 'key' => (string)$key + 'author' => $author->getAddr(), + 'key' => (string)$key ]; } @@ -356,7 +365,7 @@ class Diaspora if ($children->header) { $public = true; - $author_link = str_replace('acct:', '', $children->header->author_id); + $idom = $children->header; } else { // This happens with posts from a relais if (empty($privKey)) { @@ -384,8 +393,13 @@ class Diaspora $inner_iv = base64_decode($idom->iv); $inner_aes_key = base64_decode($idom->aes_key); + } - $author_link = str_replace('acct:', '', $idom->author_id); + try { + $author = WebFingerUri::fromString($idom->author_id); + } catch (\Throwable $e) { + Logger::notice('Could not retrieve author URI.', ['idom' => $idom]); + throw new \Friendica\Network\HTTPException\BadRequestException(); } $dom = $basedom->children(ActivityNamespace::SALMON_ME); @@ -439,17 +453,11 @@ class Diaspora $inner_decrypted = self::aesDecrypt($inner_aes_key, $inner_iv, $inner_encrypted); } - if (!$author_link) { - Logger::notice('Could not retrieve author URI.'); - throw new \Friendica\Network\HTTPException\BadRequestException(); - } // Once we have the author URI, go to the web and try to find their public key - // (first this will look it up locally if it is in the fcontact cache) + // (first this will look it up locally if it is in the diaspora-contact cache) // This will also convert diaspora public key from pkcs#1 to pkcs#8 - - Logger::notice('Fetching key for '.$author_link); - $key = self::key($author_link); - + Logger::notice('Fetching key for ' . $author); + $key = self::key($author); if (!$key) { Logger::notice('Could not retrieve author key.'); throw new \Friendica\Network\HTTPException\BadRequestException(); @@ -465,9 +473,9 @@ class Diaspora Logger::notice('Message verified.'); return [ - 'message' => (string)$inner_decrypted, - 'author' => XML::unescape($author_link), - 'key' => (string)$key + 'message' => $inner_decrypted, + 'author' => $author->getAddr(), + 'key' => $key ]; } @@ -520,7 +528,7 @@ class Diaspora { // The sender is the handle of the contact that sent the message. // This will often be different with relayed messages (for example "like" and "comment") - $sender = $msg['author']; + $sender = WebFingerUri::fromString($msg['author']); // This is only needed for private postings since this is already done for public ones before if (is_null($fields)) { @@ -535,7 +543,7 @@ class Diaspora $type = $fields->getName(); - Logger::info('Received message', ['type' => $type, 'sender' => $sender, 'user' => $importer['uid']]); + Logger::info('Received message', ['type' => $type, 'sender' => $sender->getAddr(), 'user' => $importer['uid']]); switch ($type) { case 'account_migration': @@ -743,7 +751,7 @@ class Diaspora } if (isset($parent_author_signature)) { - $key = self::key($msg['author']); + $key = self::key(WebFingerUri::fromString($msg['author'])); if (empty($key)) { Logger::info('No key found for parent', ['author' => $msg['author']]); return false; @@ -755,8 +763,12 @@ class Diaspora } } - $key = self::key($fields->author); - if (empty($key)) { + try { + $key = self::key(WebFingerUri::fromString($fields->author)); + if (empty($key)) { + throw new \InvalidArgumentException(); + } + } catch (\Throwable $e) { Logger::info('No key found', ['author' => $fields->author]); return false; } @@ -772,55 +784,51 @@ class Diaspora /** * Fetches the public key for a given handle * - * @param string $handle The handle + * @param WebFingerUri $uri The handle * * @return string The public key - * @throws \Friendica\Network\HTTPException\InternalServerErrorException + * @throws InternalServerErrorException * @throws \ImagickException */ - private static function key(string $handle = null): string + private static function key(WebFingerUri $uri): string { - $handle = strval($handle); - - Logger::notice('Fetching diaspora key', ['handle' => $handle, 'callstack' => System::callstack(20)]); - - $fcontact = FContact::getByURL($handle); - if (!empty($fcontact['pubkey'])) { - return $fcontact['pubkey']; + Logger::notice('Fetching diaspora key', ['handle' => $uri->getAddr(), 'callstack' => System::callstack(20)]); + try { + return DI::dsprContact()->getByAddr($uri)->pubKey; + } catch (HTTPException\NotFoundException|\InvalidArgumentException $e) { + return ''; } - - return ''; } /** * Get a contact id for a given handle * - * @todo Move to Friendica\Model\Contact - * - * @param int $uid The user id - * @param string $handle The handle in the format user@domain.tld + * @param int $uid The user id + * @param WebFingerUri $uri * * @return array Contact data * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ - private static function contactByHandle(int $uid, string $handle): array + private static function contactByHandle(int $uid, WebFingerUri $uri): array { - return Contact::getByURL($handle, null, [], $uid); + return Contact::getByURL($uri->getAddr(), null, [], $uid); } /** * Checks if the given contact url does support ActivityPub * - * @param string $url profile url - * @param boolean $update true = always update, false = never update, null = update when not found or outdated + * @param string $url profile url or WebFinger address + * @param boolean|null $update true = always update, false = never update, null = update when not found or outdated * @return boolean * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ - public static function isSupportedByContactUrl(string $url, $update = null) + public static function isSupportedByContactUrl(string $url, ?bool $update = null): bool { - return !empty(FContact::getByURL($url, $update)); + $contact = Contact::getByURL($url, $update); + + return DI::dsprContact()->existsByUriId($contact['uri-id'] ?? 0); } /** @@ -874,21 +882,22 @@ class Diaspora /** * Fetches the contact id for a handle and checks if posting is allowed * - * @param array $importer Array of the importer user - * @param string $handle The checked handle in the format user@domain.tld - * @param bool $is_comment Is the check for a comment? + * @param array $importer Array of the importer user + * @param WebFingerUri $contact_uri The checked contact + * @param bool $is_comment Is the check for a comment? * * @return array|bool The contact data or false on error - * @throws \Exception + * @throws InternalServerErrorException + * @throws \ImagickException */ - private static function allowedContactByHandle(array $importer, string $handle, bool $is_comment = false) + private static function allowedContactByHandle(array $importer, WebFingerUri $contact_uri, bool $is_comment = false) { - $contact = self::contactByHandle($importer['uid'], $handle); + $contact = self::contactByHandle($importer['uid'], $contact_uri); if (!$contact) { - Logger::notice('A Contact for handle ' . $handle . ' and user ' . $importer['uid'] . ' was not found'); + Logger::notice('A Contact for handle ' . $contact_uri . ' and user ' . $importer['uid'] . ' was not found'); // If a contact isn't found, we accept it anyway if it is a comment if ($is_comment && ($importer['uid'] != 0)) { - return self::contactByHandle(0, $handle); + return self::contactByHandle(0, $contact_uri); } elseif ($is_comment) { return $importer; } else { @@ -897,7 +906,7 @@ class Diaspora } if (!self::postAllow($importer, $contact, $is_comment)) { - Logger::notice('The handle: ' . $handle . ' is not allowed to post to user ' . $importer['uid']); + Logger::notice('The handle: ' . $contact_uri . ' is not allowed to post to user ' . $importer['uid']); return false; } return $contact; @@ -966,7 +975,7 @@ class Diaspora // 0 => '[url=/people/0123456789abcdef]Foo Bar[/url]' // 1 => '0123456789abcdef' // 2 => 'Foo Bar' - $handle = FContact::getUrlByGuid($match[1]); + $handle = DI::dsprContact()->getUrlByGuid($match[1]); if ($handle) { $return = '@[url=' . $handle . ']' . $match[2] . '[/url]'; @@ -1011,7 +1020,7 @@ class Diaspora * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ - public static function storeByGuid(string $guid, string $server, bool $force) + private static function storeByGuid(string $guid, string $server, bool $force) { $serverparts = parse_url($server); @@ -1092,25 +1101,27 @@ class Diaspora return self::message($source_xml->root_guid, $server, ++$level); } - $author = ''; + $author_handle = ''; // Fetch the author - for the old and the new Diaspora version if ($source_xml->post->status_message && $source_xml->post->status_message->diaspora_handle) { - $author = (string)$source_xml->post->status_message->diaspora_handle; + $author_handle = (string)$source_xml->post->status_message->diaspora_handle; } elseif ($source_xml->author && ($source_xml->getName() == 'status_message')) { - $author = (string)$source_xml->author; + $author_handle = (string)$source_xml->author; } - // If this isn't a "status_message" then quit - if (!$author) { + try { + $author = WebFingerUri::fromString($author_handle); + } catch (\InvalidArgumentException $e) { + // If this isn't a "status_message" then quit Logger::info("Message doesn't seem to be a status message"); return false; } return [ 'message' => $x, - 'author' => $author, - 'key' => self::key($author) + 'author' => $author->getAddr(), + 'key' => self::key($author) ]; } @@ -1157,15 +1168,15 @@ class Diaspora /** * Fetches the item record of a given guid * - * @param int $uid The user id - * @param string $guid message guid - * @param string $author The handle of the item - * @param array $contact The contact of the item owner + * @param int $uid The user id + * @param string $guid message guid + * @param WebFingerUri $author + * @param array $contact The contact of the item owner * * @return array|bool the item record or false on failure * @throws \Exception */ - private static function parentItem(int $uid, string $guid, string $author, array $contact) + private static function parentItem(int $uid, string $guid, WebFingerUri $author, array $contact) { $fields = ['id', 'parent', 'body', 'wall', 'uri', 'guid', 'private', 'origin', 'author-name', 'author-link', 'author-avatar', 'gravity', @@ -1175,18 +1186,21 @@ class Diaspora $item = Post::selectFirst($fields, $condition); if (!DBA::isResult($item)) { - $person = FContact::getByURL($author); - $result = self::storeByGuid($guid, $person['url'], false); + try { + $result = self::storeByGuid($guid, DI::dsprContact()->getByAddr($author)->url, false); - // We don't have an url for items that arrived at the public dispatcher - if (!$result && !empty($contact['url'])) { - $result = self::storeByGuid($guid, $contact['url'], false); - } + // We don't have an url for items that arrived at the public dispatcher + if (!$result && !empty($contact['url'])) { + $result = self::storeByGuid($guid, $contact['url'], false); + } - if ($result) { - Logger::info('Fetched missing item ' . $guid . ' - result: ' . $result); + if ($result) { + Logger::info('Fetched missing item ' . $guid . ' - result: ' . $result); - $item = Post::selectFirst($fields, $condition); + $item = Post::selectFirst($fields, $condition); + } + } catch (HTTPException\NotFoundException $e) { + Logger::notice('Unable to retrieve author details', ['author' => $author->getAddr()]); } } @@ -1200,20 +1214,20 @@ class Diaspora } /** - * returns contact details + * returns contact details for the given user * - * @param array $def_contact The default contact if the person isn't found - * @param array $person The record of the person - * @param int $uid The user id + * @param array $def_contact The default details if the contact isn't found + * @param string $contact_url The url of the contact + * @param int $uid The user id * * @return array * 'cid' => contact id * 'network' => network type * @throws \Exception */ - private static function authorContactByUrl(array $def_contact, array $person, int $uid): array + private static function authorContactByUrl(array $def_contact, string $contact_url, int $uid): array { - $condition = ['nurl' => Strings::normaliseLink($person['url']), 'uid' => $uid]; + $condition = ['nurl' => Strings::normaliseLink($contact_url), 'uid' => $uid]; $contact = DBA::selectFirst('contact', ['id', 'network'], $condition); if (DBA::isResult($contact)) { $cid = $contact['id']; @@ -1318,21 +1332,27 @@ class Diaspora */ private static function receiveAccountMigration(array $importer, SimpleXMLElement $data): bool { - $old_handle = XML::unescape($data->author); - $new_handle = XML::unescape($data->profile->author); - $signature = XML::unescape($data->signature); - - $contact = self::contactByHandle($importer['uid'], $old_handle); - if (!$contact) { - Logger::notice('Cannot find contact for sender: ' . $old_handle . ' and user ' . $importer['uid']); + try { + $old_author = WebFingerUri::fromString(XML::unescape($data->author)); + $new_author = WebFingerUri::fromString(XML::unescape($data->profile->author)); + } catch (\Throwable $e) { + Logger::notice('Cannot find handles for sender and user', ['data' => $data]); return false; } - Logger::notice('Got migration for ' . $old_handle . ', to ' . $new_handle . ' with user ' . $importer['uid']); + $signature = XML::unescape($data->signature); + + $contact = self::contactByHandle($importer['uid'], $old_author); + if (!$contact) { + Logger::notice('Cannot find contact for sender: ' . $old_author . ' and user ' . $importer['uid']); + return false; + } + + Logger::notice('Got migration for ' . $old_author . ', to ' . $new_author . ' with user ' . $importer['uid']); // Check signature - $signed_text = 'AccountMigration:' . $old_handle . ':' . $new_handle; - $key = self::key($old_handle); + $signed_text = 'AccountMigration:' . $old_author . ':' . $new_author; + $key = self::key($old_author); if (!Crypto::rsaVerify($signed_text, $signature, $key, 'sha256')) { Logger::notice('No valid signature for migration.'); return false; @@ -1342,9 +1362,9 @@ class Diaspora self::receiveProfile($importer, $data->profile); // change the technical stuff in contact - $data = Probe::uri($new_handle); + $data = Probe::uri($new_author); if ($data['network'] == Protocol::PHANTOM) { - Logger::notice("Account for " . $new_handle . " couldn't be probed."); + Logger::notice("Account for " . $new_author . " couldn't be probed."); return false; } @@ -1360,7 +1380,7 @@ class Diaspora 'network' => $data['network'], ]; - Contact::update($fields, ['addr' => $old_handle]); + Contact::update($fields, ['addr' => $old_author->getAddr()]); Logger::notice('Contacts are updated.'); @@ -1377,15 +1397,15 @@ class Diaspora */ private static function receiveAccountDeletion(SimpleXMLElement $data): bool { - $author = XML::unescape($data->author); + $author_handle = XML::unescape($data->author); - $contacts = DBA::select('contact', ['id'], ['addr' => $author]); + $contacts = DBA::select('contact', ['id'], ['addr' => $author_handle]); while ($contact = DBA::fetch($contacts)) { Contact::remove($contact['id']); } DBA::close($contacts); - Logger::notice('Removed contacts for ' . $author); + Logger::notice('Removed contacts for ' . $author_handle); return true; } @@ -1393,27 +1413,24 @@ class Diaspora /** * Fetch the uri from our database if we already have this item (maybe from ourselves) * - * @param string $author Author handle - * @param string $guid Message guid - * @param boolean $onlyfound Only return uri when found in the database + * @param string $guid Message guid + * @param WebFingerUri|null $person_uri Optional person to derive the base URL from * - * @return string The constructed uri or the one from our database or empty string on if $onlyfound is true + * @return string The constructed uri or the one from our database or empty string * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ - private static function getUriFromGuid(string $author, string $guid, bool $onlyfound = false): string + private static function getUriFromGuid(string $guid, WebFingerUri $person_uri = null): string { $item = Post::selectFirst(['uri'], ['guid' => $guid]); - if (DBA::isResult($item)) { + if ($item) { return $item['uri']; - } elseif (!$onlyfound) { - $person = FContact::getByURL($author); - - $parts = parse_url($person['url']); - unset($parts['path']); - $host_url = (string)Uri::fromParts($parts); - - return $host_url . '/objects/' . $guid; + } elseif ($person_uri) { + try { + return DI::dsprContact()->selectOneByAddr($person_uri)->baseurl . '/objects/' . $guid; + } catch (HTTPException\NotFoundException|\InvalidArgumentException $e) { + return ''; + } } return ''; @@ -1444,31 +1461,31 @@ class Diaspora continue; } - $person = FContact::getByURL($match[3]); - if (empty($person)) { - continue; - } + try { + $contact = DI::dsprContact()->getByUrl(new Uri($match[3])); + Tag::storeByHash($uriid, $match[1], $contact->name ?: $contact->nick, $contact->url); + } catch (\Throwable $e) { - Tag::storeByHash($uriid, $match[1], $person['name'] ?: $person['nick'], $person['url']); + } } } /** * Processes an incoming comment * - * @param array $importer Array of the importer user - * @param string $sender The sender of the message + * @param array $importer Array of the importer user + * @param WebFingerUri $sender The sender of the message * @param SimpleXMLElement $data The message object - * @param string $xml The original XML of the message - * @param int $direction Indicates if the message had been fetched or pushed (self::PUSHED, self::FETCHED, self::FORCED_FETCH) + * @param string $xml The original XML of the message + * @param int $direction Indicates if the message had been fetched or pushed (self::PUSHED, self::FETCHED, self::FORCED_FETCH) * - * @return int The message id of the generated comment or "false" if there was an error + * @return bool The message id of the generated comment or "false" if there was an error * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ - private static function receiveComment(array $importer, string $sender, SimpleXMLElement $data, string $xml, int $direction): bool + private static function receiveComment(array $importer, WebFingerUri $sender, SimpleXMLElement $data, string $xml, int $direction): bool { - $author = XML::unescape($data->author); + $author = WebFingerUri::fromString(XML::unescape($data->author)); $guid = XML::unescape($data->guid); $parent_guid = XML::unescape($data->parent_guid); $text = XML::unescape($data->text); @@ -1481,7 +1498,7 @@ class Diaspora if (isset($data->thread_parent_guid)) { $thread_parent_guid = XML::unescape($data->thread_parent_guid); - $thr_parent = self::getUriFromGuid('', $thread_parent_guid, true); + $thr_parent = self::getUriFromGuid($thread_parent_guid); } else { $thr_parent = ''; } @@ -1505,14 +1522,15 @@ class Diaspora return false; } - $person = FContact::getByURL($author); - if (!is_array($person)) { - Logger::notice('Unable to find author details'); + try { + $author_url = (string)DI::dsprContact()->getByAddr($author)->url; + } catch (HTTPException\NotFoundException|\InvalidArgumentException $e) { + Logger::notice('Unable to find author details', ['author' => $author->getAddr()]); return false; } // Fetch the contact id - if we know this contact - $author_contact = self::authorContactByUrl($contact, $person, $importer['uid']); + $author_contact = self::authorContactByUrl($contact, $author_url, $importer['uid']); $datarray = []; @@ -1520,17 +1538,17 @@ class Diaspora $datarray['contact-id'] = $author_contact['cid']; $datarray['network'] = $author_contact['network']; - $datarray['author-link'] = $person['url']; - $datarray['author-id'] = Contact::getIdForURL($person['url'], 0); + $datarray['author-link'] = $author_url; + $datarray['author-id'] = Contact::getIdForURL($author_url); $datarray['owner-link'] = $contact['url']; - $datarray['owner-id'] = Contact::getIdForURL($contact['url'], 0); + $datarray['owner-id'] = Contact::getIdForURL($contact['url']); // Will be overwritten for sharing accounts in Item::insert $datarray = self::setDirection($datarray, $direction); $datarray['guid'] = $guid; - $datarray['uri'] = self::getUriFromGuid($author, $guid); + $datarray['uri'] = self::getUriFromGuid($guid, $author); $datarray['uri-id'] = ItemURI::insert(['uri' => $datarray['uri'], 'guid' => $datarray['guid']]); $datarray['verb'] = Activity::POST; @@ -1551,7 +1569,7 @@ class Diaspora $datarray['plink'] = self::plink($author, $guid, $toplevel_parent_item['guid']); $body = Markdown::toBBCode($text); - $datarray['body'] = self::replacePeopleGuid($body, $person['url']); + $datarray['body'] = self::replacePeopleGuid($body, $author_url); self::storeMentions($datarray['uri-id'], $text); Tag::storeRawTagsFromBody($datarray['uri-id'], $datarray['body']); @@ -1601,20 +1619,26 @@ class Diaspora */ private static function receiveConversationMessage(array $importer, array $contact, SimpleXMLElement $data, array $msg, $mesg, array $conversation): bool { - $author = XML::unescape($data->author); + $author_handle = XML::unescape($data->author); $guid = XML::unescape($data->guid); $subject = XML::unescape($data->subject); // "diaspora_handle" is the element name from the old version // "author" is the element name from the new version if ($mesg->author) { - $msg_author = XML::unescape($mesg->author); + $msg_author_handle = XML::unescape($mesg->author); } elseif ($mesg->diaspora_handle) { - $msg_author = XML::unescape($mesg->diaspora_handle); + $msg_author_handle = XML::unescape($mesg->diaspora_handle); } else { return false; } + try { + $msg_author_uri = WebFingerUri::fromString($msg_author_handle); + } catch (\InvalidArgumentException $e) { + return false; + } + $msg_guid = XML::unescape($mesg->guid); $msg_conversation_guid = XML::unescape($mesg->conversation_guid); $msg_text = XML::unescape($mesg->text); @@ -1625,23 +1649,20 @@ class Diaspora return false; } - $body = Markdown::toBBCode($msg_text); - $message_uri = $msg_author . ':' . $msg_guid; - - $person = FContact::getByURL($msg_author); + $msg_author = DI::dsprContact()->getByAddr($msg_author_uri); return Mail::insert([ 'uid' => $importer['uid'], 'guid' => $msg_guid, 'convid' => $conversation['id'], - 'from-name' => $person['name'], - 'from-photo' => $person['photo'], - 'from-url' => $person['url'], + 'from-name' => $msg_author->name, + 'from-photo' => (string)$msg_author->photo, + 'from-url' => (string)$msg_author->url, 'contact-id' => $contact['id'], 'title' => $subject, - 'body' => $body, - 'uri' => $message_uri, - 'parent-uri' => $author . ':' . $guid, + 'body' => Markdown::toBBCode($msg_text), + 'uri' => $msg_author_handle . ':' . $msg_guid, + 'parent-uri' => $author_handle . ':' . $guid, 'created' => $msg_created_at ]); } @@ -1658,7 +1679,7 @@ class Diaspora */ private static function receiveConversation(array $importer, array $msg, SimpleXMLElement $data) { - $author = XML::unescape($data->author); + $author_handle = XML::unescape($data->author); $guid = XML::unescape($data->guid); $subject = XML::unescape($data->subject); $created_at = DateTimeFormat::utc(XML::unescape($data->created_at)); @@ -1671,7 +1692,7 @@ class Diaspora return false; } - $contact = self::allowedContactByHandle($importer, $msg['author'], true); + $contact = self::allowedContactByHandle($importer, WebFingerUri::fromString($msg['author']), true); if (!$contact) { return false; } @@ -1685,7 +1706,7 @@ class Diaspora $r = DBA::insert('conv', [ 'uid' => $importer['uid'], 'guid' => $guid, - 'creator' => $author, + 'creator' => $author_handle, 'created' => $created_at, 'updated' => DateTimeFormat::utcNow(), 'subject' => $subject, @@ -1711,18 +1732,18 @@ class Diaspora /** * Processes "like" messages * - * @param array $importer Array of the importer user - * @param string $sender The sender of the message + * @param array $importer Array of the importer user + * @param WebFingerUri $sender The sender of the message * @param SimpleXMLElement $data The message object - * @param int $direction Indicates if the message had been fetched or pushed (self::PUSHED, self::FETCHED, self::FORCED_FETCH) + * @param int $direction Indicates if the message had been fetched or pushed (self::PUSHED, self::FETCHED, self::FORCED_FETCH) * * @return bool Success or failure * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ - private static function receiveLike(array $importer, string $sender, SimpleXMLElement $data, int $direction): bool + private static function receiveLike(array $importer, WebFingerUri $sender, SimpleXMLElement $data, int $direction): bool { - $author = XML::unescape($data->author); + $author = WebFingerUri::fromString(XML::unescape($data->author)); $guid = XML::unescape($data->guid); $parent_guid = XML::unescape($data->parent_guid); $parent_type = XML::unescape($data->parent_type); @@ -1753,14 +1774,15 @@ class Diaspora return false; } - $person = FContact::getByURL($author); - if (!is_array($person)) { - Logger::notice('Unable to find author details'); + try { + $author_url = (string)DI::dsprContact()->getByAddr($author)->url; + } catch (HTTPException\NotFoundException|\InvalidArgumentException $e) { + Logger::notice('Unable to find author details', ['author' => $author->getAddr()]); return false; } // Fetch the contact id - if we know this contact - $author_contact = self::authorContactByUrl($contact, $person, $importer['uid']); + $author_contact = self::authorContactByUrl($contact, $author_url, $importer['uid']); // "positive" = "false" would be a Dislike - wich isn't currently supported by Diaspora // We would accept this anyhow. @@ -1780,11 +1802,11 @@ class Diaspora $datarray = self::setDirection($datarray, $direction); - $datarray['owner-link'] = $datarray['author-link'] = $person['url']; - $datarray['owner-id'] = $datarray['author-id'] = Contact::getIdForURL($person['url'], 0); + $datarray['owner-link'] = $datarray['author-link'] = $author_url; + $datarray['owner-id'] = $datarray['author-id'] = Contact::getIdForURL($author_url); $datarray['guid'] = $guid; - $datarray['uri'] = self::getUriFromGuid($author, $guid); + $datarray['uri'] = self::getUriFromGuid($guid, $author); $datarray['verb'] = $verb; $datarray['gravity'] = Item::GRAVITY_ACTIVITY; @@ -1843,13 +1865,13 @@ class Diaspora */ private static function receiveMessage(array $importer, SimpleXMLElement $data): bool { - $author = XML::unescape($data->author); + $author_uri = WebFingerUri::fromString(XML::unescape($data->author)); $guid = XML::unescape($data->guid); $conversation_guid = XML::unescape($data->conversation_guid); $text = XML::unescape($data->text); $created_at = DateTimeFormat::utc(XML::unescape($data->created_at)); - $contact = self::allowedContactByHandle($importer, $author, true); + $contact = self::allowedContactByHandle($importer, $author_uri, true); if (!$contact) { return false; } @@ -1858,41 +1880,37 @@ class Diaspora GServer::setProtocol($contact['gsid'], Post\DeliveryData::DIASPORA); } - $conversation = null; - $condition = ['uid' => $importer['uid'], 'guid' => $conversation_guid]; $conversation = DBA::selectFirst('conv', [], $condition); - if (!DBA::isResult($conversation)) { Logger::notice('Conversation not available.'); return false; } - $message_uri = $author . ':' . $guid; - - $person = FContact::getByURL($author); - if (!$person) { - Logger::notice('Unable to find author details'); + try { + $author = DI::dsprContact()->getByAddr($author_uri); + } catch (HTTPException\NotFoundException|\InvalidArgumentException $e) { + Logger::notice('Unable to find author details', ['author' => $author_uri->getAddr()]); return false; } $body = Markdown::toBBCode($text); - $body = self::replacePeopleGuid($body, $person['url']); + $body = self::replacePeopleGuid($body, $author->url); return Mail::insert([ 'uid' => $importer['uid'], 'guid' => $guid, 'convid' => $conversation['id'], - 'from-name' => $person['name'], - 'from-photo' => $person['photo'], - 'from-url' => $person['url'], + 'from-name' => $author->name, + 'from-photo' => (string)$author->photo, + 'from-url' => (string)$author->url, 'contact-id' => $contact['id'], 'title' => $conversation['subject'], 'body' => $body, 'reply' => 1, - 'uri' => $message_uri, - 'parent-uri' => $author . ':' . $conversation['guid'], + 'uri' => $author_uri . ':' . $guid, + 'parent-uri' => $author_uri . ':' . $conversation['guid'], 'created' => $created_at ]); } @@ -1910,7 +1928,7 @@ class Diaspora */ private static function receiveParticipation(array $importer, SimpleXMLElement $data, int $direction): bool { - $author = strtolower(XML::unescape($data->author)); + $author = WebFingerUri::fromString(strtolower(XML::unescape($data->author))); $guid = XML::unescape($data->guid); $parent_guid = XML::unescape($data->parent_guid); @@ -1941,13 +1959,14 @@ class Diaspora return false; } - $person = FContact::getByURL($author); - if (!is_array($person)) { - Logger::notice('Person not found: ' . $author); + try { + $author_url = (string)DI::dsprContact()->getByAddr($author)->url; + } catch (HTTPException\NotFoundException|\InvalidArgumentException $e) { + Logger::notice('unable to find author details', ['author' => $author->getAddr()]); return false; } - $author_contact = self::authorContactByUrl($contact, $person, $importer['uid']); + $author_contact = self::authorContactByUrl($contact, $author_url, $importer['uid']); // Store participation $datarray = []; @@ -1960,11 +1979,11 @@ class Diaspora $datarray = self::setDirection($datarray, $direction); - $datarray['owner-link'] = $datarray['author-link'] = $person['url']; - $datarray['owner-id'] = $datarray['author-id'] = Contact::getIdForURL($person['url'], 0); + $datarray['owner-link'] = $datarray['author-link'] = $author_url; + $datarray['owner-id'] = $datarray['author-id'] = Contact::getIdForURL($author_url); $datarray['guid'] = $guid; - $datarray['uri'] = self::getUriFromGuid($author, $guid); + $datarray['uri'] = self::getUriFromGuid($guid, $author); $datarray['verb'] = Activity::FOLLOW; $datarray['gravity'] = Item::GRAVITY_ACTIVITY; @@ -2056,7 +2075,7 @@ class Diaspora */ private static function receiveProfile(array $importer, SimpleXMLElement $data): bool { - $author = strtolower(XML::unescape($data->author)); + $author = WebFingerUri::fromString(strtolower(XML::unescape($data->author))); $contact = self::contactByHandle($importer['uid'], $author); if (!$contact) { @@ -2084,16 +2103,13 @@ class Diaspora $keywords = implode(', ', $keywords); - $handle_parts = explode('@', $author); - $nick = $handle_parts[0]; - if ($name === '') { - $name = $handle_parts[0]; + $name = $author->getUser(); } if (preg_match('|^https?://|', $image_url) === 0) { // @TODO No HTTPS here? - $image_url = 'http://' . $handle_parts[1] . $image_url; + $image_url = 'http://' . $author->getFullHost() . $image_url; } Contact::updateAvatar($contact['id'], $image_url); @@ -2115,7 +2131,7 @@ class Diaspora $fields = ['name' => $name, 'location' => $location, 'name-date' => DateTimeFormat::utcNow(), 'about' => $about, - 'addr' => $author, 'nick' => $nick, 'keywords' => $keywords, + 'addr' => $author->getAddr(), 'nick' => $author->getUser(), 'keywords' => $keywords, 'unsearchable' => !$searchable, 'sensitive' => $nsfw]; if (!empty($birthday)) { @@ -2158,13 +2174,15 @@ class Diaspora */ private static function receiveContactRequest(array $importer, SimpleXMLElement $data): bool { - $author = XML::unescape($data->author); + $author_handle = XML::unescape($data->author); $recipient = XML::unescape($data->recipient); - if (!$author || !$recipient) { + if (!$author_handle || !$recipient) { return false; } + $author = WebFingerUri::fromString($author_handle); + // the current protocol version doesn't know these fields // That means that we will assume their existance if (isset($data->following)) { @@ -2222,22 +2240,24 @@ class Diaspora Logger::info("Author " . $author . " wants to listen to us."); } - $ret = FContact::getByURL($author); - - if (!$ret || ($ret['network'] != Protocol::DIASPORA)) { - Logger::notice("Cannot resolve diaspora handle " . $author . " for ".$recipient); + try { + $author_url = (string)DI::dsprContact()->getByAddr($author)->url; + } catch (HTTPException\NotFoundException|\InvalidArgumentException $e) { + Logger::notice('Cannot resolve diaspora handle for recipient', ['author' => $author->getAddr(), 'recipient' => $recipient]); return false; } - $cid = Contact::getIdForURL($ret['url'], $importer['uid']); + $cid = Contact::getIdForURL($author_url, $importer['uid']); if (!empty($cid)) { $contact = DBA::selectFirst('contact', [], ['id' => $cid, 'network' => Protocol::NATIVE_SUPPORT]); } else { $contact = []; } - $item = ['author-id' => Contact::getIdForURL($ret['url']), - 'author-link' => $ret['url']]; + $item = [ + 'author-id' => Contact::getIdForURL($author_url), + 'author-link' => $author_url + ]; $result = Contact::addRelationship($importer, $contact, $item, false); if ($result === true) { @@ -2262,12 +2282,15 @@ class Diaspora /** * Stores a reshare activity * - * @param array $item Array of reshare post - * @param integer $parent_message_id Id of the parent post - * @param string $guid GUID string of reshare action - * @param string $author Author handle + * @param array $item Array of reshare post + * @param integer $parent_message_id Id of the parent post + * @param string $guid GUID string of reshare action + * @param WebFingerUri $author Author handle + * @return false|void + * @throws InternalServerErrorException + * @throws \ImagickException */ - private static function addReshareActivity(array $item, int $parent_message_id, string $guid, string $author) + private static function addReshareActivity(array $item, int $parent_message_id, string $guid, WebFingerUri $author) { $parent = Post::selectFirst(['uri', 'guid'], ['id' => $parent_message_id]); @@ -2284,7 +2307,7 @@ class Diaspora $datarray['owner-id'] = $datarray['author-id']; $datarray['guid'] = $parent['guid'] . '-' . $guid; - $datarray['uri'] = self::getUriFromGuid($author, $datarray['guid']); + $datarray['uri'] = self::getUriFromGuid($datarray['guid'], $author); $datarray['thr-parent'] = $parent['uri']; $datarray['verb'] = $datarray['body'] = Activity::ANNOUNCE; @@ -2329,15 +2352,20 @@ class Diaspora */ private static function receiveReshare(array $importer, SimpleXMLElement $data, string $xml, int $direction): bool { - $author = XML::unescape($data->author); + $author = WebFingerUri::fromString(XML::unescape($data->author)); $guid = XML::unescape($data->guid); $created_at = DateTimeFormat::utc(XML::unescape($data->created_at)); - $root_author = XML::unescape($data->root_author); + try { + $root_author = WebFingerUri::fromString(XML::unescape($data->root_author)); + } catch (\InvalidArgumentException $e) { + return false; + } + $root_guid = XML::unescape($data->root_guid); /// @todo handle unprocessed property "provider_display_name" $public = XML::unescape($data->public); - $contact = self::allowedContactByHandle($importer, $author, false); + $contact = self::allowedContactByHandle($importer, $author); if (!$contact) { return false; } @@ -2351,8 +2379,9 @@ class Diaspora return true; } - $original_person = FContact::getByURL($root_author); - if (!$original_person) { + try { + $original_person = DI::dsprContact()->getByAddr($root_author); + } catch (HTTPException\NotFoundException $e) { return false; } @@ -2369,7 +2398,7 @@ class Diaspora $datarray['owner-id'] = $datarray['author-id']; $datarray['guid'] = $guid; - $datarray['uri'] = $datarray['thr-parent'] = self::getUriFromGuid($author, $guid); + $datarray['uri'] = $datarray['thr-parent'] = self::getUriFromGuid($guid, $author); $datarray['uri-id'] = ItemURI::insert(['uri' => $datarray['uri'], 'guid' => $datarray['guid']]); $datarray['verb'] = Activity::POST; @@ -2380,7 +2409,7 @@ class Diaspora $datarray = self::setDirection($datarray, $direction); - $datarray['quote-uri-id'] = self::getQuoteUriId($root_guid, $importer['uid'], $original_person['url']); + $datarray['quote-uri-id'] = self::getQuoteUriId($root_guid, $importer['uid'], $original_person->url); if (empty($datarray['quote-uri-id'])) { return false; } @@ -2448,19 +2477,18 @@ class Diaspora */ private static function itemRetraction(array $importer, array $contact, SimpleXMLElement $data): bool { - $author = XML::unescape($data->author); + $author_uri = WebFingerUri::fromString(XML::unescape($data->author)); $target_guid = XML::unescape($data->target_guid); $target_type = XML::unescape($data->target_type); - $person = FContact::getByURL($author); - if (!is_array($person)) { - Logger::notice('Unable to find author detail for ' . $author); + try { + $author = DI::dsprContact()->getByAddr($author_uri); + } catch (HTTPException\NotFoundException|\InvalidArgumentException $e) { + Logger::notice('Unable to find details for author', ['author' => $author_uri->getAddr()]); return false; } - if (empty($contact['url'])) { - $contact['url'] = $person['url']; - } + $contact_url = $contact['url'] ?? '' ?: (string)$author->url; // Fetch items that are about to be deleted $fields = ['uid', 'id', 'parent', 'author-link', 'uri-id']; @@ -2488,8 +2516,8 @@ class Diaspora $parent = Post::selectFirst(['author-link'], ['id' => $item['parent']]); // Only delete it if the parent author really fits - if (!Strings::compareLink($parent['author-link'], $contact['url']) && !Strings::compareLink($item['author-link'], $contact['url'])) { - Logger::info("Thread author " . $parent['author-link'] . " and item author " . $item['author-link'] . " don't fit to expected contact " . $contact['url']); + if (!Strings::compareLink($parent['author-link'], $contact_url) && !Strings::compareLink($item['author-link'], $contact_url)) { + Logger::info("Thread author " . $parent['author-link'] . " and item author " . $item['author-link'] . " don't fit to expected contact " . $contact_url); continue; } @@ -2505,14 +2533,14 @@ class Diaspora /** * Receives retraction messages * - * @param array $importer Array of the importer user - * @param string $sender The sender of the message + * @param array $importer Array of the importer user + * @param WebFingerUri $sender The sender of the message * @param SimpleXMLElement $data The message object * * @return bool Success * @throws \Exception */ - private static function receiveRetraction(array $importer, string $sender, SimpleXMLElement $data) + private static function receiveRetraction(array $importer, WebFingerUri $sender, SimpleXMLElement $data) { $target_type = XML::unescape($data->target_type); @@ -2639,14 +2667,14 @@ class Diaspora */ private static function receiveStatusMessage(array $importer, SimpleXMLElement $data, string $xml, int $direction) { - $author = XML::unescape($data->author); + $author = WebFingerUri::fromString(XML::unescape($data->author)); $guid = XML::unescape($data->guid); $created_at = DateTimeFormat::utc(XML::unescape($data->created_at)); $public = XML::unescape($data->public); $text = XML::unescape($data->text); $provider_display_name = XML::unescape($data->provider_display_name); - $contact = self::allowedContactByHandle($importer, $author, false); + $contact = self::allowedContactByHandle($importer, $author); if (!$contact) { return false; } @@ -2672,7 +2700,7 @@ class Diaspora $datarray = []; $datarray['guid'] = $guid; - $datarray['uri'] = $datarray['thr-parent'] = self::getUriFromGuid($author, $guid); + $datarray['uri'] = $datarray['thr-parent'] = self::getUriFromGuid($guid, $author); $datarray['uri-id'] = ItemURI::insert(['uri' => $datarray['uri'], 'guid' => $datarray['guid']]); // Attach embedded pictures to the body @@ -2954,13 +2982,13 @@ class Diaspora $logid = Strings::getRandomHex(4); - // We always try to use the data from the fcontact table. + // We always try to use the data from the diaspora-contact table. // This is important for transmitting data to Friendica servers. - if (!empty($contact['addr'])) { - $fcontact = FContact::getByURL($contact['addr']); - if (!empty($fcontact)) { - $dest_url = ($public_batch ? $fcontact['batch'] : $fcontact['notify']); - } + try { + $target = DI::dsprContact()->getByAddr(WebFingerUri::fromString($contact['addr'])); + $dest_url = $public_batch ? $target->batch : $target->notify; + } catch (HTTPException\NotFoundException|\InvalidArgumentException $e) { + } if (empty($dest_url)) { @@ -3029,18 +3057,19 @@ class Diaspora } // When sending content to Friendica contacts using the Diaspora protocol - // we have to fetch the public key from the fcontact. + // we have to fetch the public key from the diaspora-contact. // This is due to the fact that legacy DFRN had unique keys for every contact. $pubkey = $contact['pubkey']; if (!empty($contact['addr'])) { - $fcontact = FContact::getByURL($contact['addr']); - if (!empty($fcontact)) { - $pubkey = $fcontact['pubkey']; + try { + $pubkey = DI::dsprContact()->getByAddr(WebFingerUri::fromString($contact['addr']))->pubKey; + } catch (HTTPException\NotFoundException|\InvalidArgumentException $e) { + } } else { // The "addr" field should always be filled. // If this isn't the case, it will raise a notice some lines later. - // And in the log we will see where it came from and we can handle it there. + // And in the log we will see where it came from, and we can handle it there. Logger::notice('Empty addr', ['contact' => $contact ?? [], 'callstack' => System::callstack(20)]); } @@ -3089,16 +3118,16 @@ class Diaspora $owner = User::getOwnerDataById($item['uid']); } - $author = self::myHandle($owner); + $author_handle = self::myHandle($owner); $message = [ - 'author' => $author, + 'author' => $author_handle, 'guid' => System::createUUID(), 'parent_type' => 'Post', 'parent_guid' => $item['guid'] ]; - Logger::info('Send participation for ' . $item['guid'] . ' by ' . $author); + Logger::info('Send participation for ' . $item['guid'] . ' by ' . $author_handle); // It doesn't matter what we store, we only want to avoid sending repeated notifications for the same item DI::cache()->set($cachekey, $item['guid'], Duration::QUARTER_HOUR); @@ -4024,6 +4053,8 @@ class Diaspora * * @param integer $parent_id * @return boolean + * @throws InternalServerErrorException + * @throws \ImagickException */ private static function parentSupportDiaspora(int $parent_id): bool { @@ -4033,7 +4064,7 @@ class Diaspora return false; } - if (empty(FContact::getByURL($parent_post['author-link'], false))) { + if (!self::isSupportedByContactUrl($parent_post['author-link'], false)) { Logger::info('Parent author is no Diaspora contact.', ['parent-id' => $parent_id]); return false; } diff --git a/src/Protocol/Diaspora/Entity/DiasporaContact.php b/src/Protocol/Diaspora/Entity/DiasporaContact.php new file mode 100644 index 0000000000..7fbd2831f7 --- /dev/null +++ b/src/Protocol/Diaspora/Entity/DiasporaContact.php @@ -0,0 +1,140 @@ +. + * + */ + +namespace Friendica\Protocol\Diaspora\Entity; + +use Psr\Http\Message\UriInterface; + +/** + * @property-read $uriId + * @property-read $url + * @property-read $guid + * @property-read $addr + * @property-read $alias + * @property-read $nick + * @property-read $name + * @property-read $givenName + * @property-read $familyName + * @property-read $photo + * @property-read $photoMedium + * @property-read $photoSmall + * @property-read $batch + * @property-read $notify + * @property-read $poll + * @property-read $subscribe + * @property-read $searchable + * @property-read $pubKey + * @property-read $baseurl + * @property-read $gsid + * @property-read $created + * @property-read $updated + * @property-read $interacting_count + * @property-read $interacted_count + * @property-read $post_count + */ +class DiasporaContact extends \Friendica\BaseEntity +{ + /** @var int */ + protected $uriId; + /** @var UriInterface */ + protected $url; + /** @var string */ + protected $guid; + /** @var string */ + protected $addr; + /** @var UriInterface */ + protected $alias; + /** @var string */ + protected $nick; + /** @var string */ + protected $name; + /** @var string */ + protected $givenName; + /** @var string */ + protected $familyName; + /** @var UriInterface */ + protected $photo; + /** @var UriInterface */ + protected $photoMedium; + /** @var UriInterface */ + protected $photoSmall; + /** @var UriInterface */ + protected $batch; + /** @var UriInterface */ + protected $notify; + /** @var UriInterface */ + protected $poll; + /** @var UriInterface */ + protected $subscribe; + /** @var bool */ + protected $searchable; + /** @var string */ + protected $pubKey; + /** @var UriInterface */ + protected $baseurl; + /** @var int */ + protected $gsid; + /** @var \DateTime */ + protected $created; + /** @var \DateTime */ + protected $updated; + /** @var int */ + protected $interacting_count; + /** @var int */ + protected $interacted_count; + /** @var int */ + protected $post_count; + + public function __construct( + UriInterface $url, \DateTime $created, string $guid = null, string $addr = null, UriInterface $alias = null, + string $nick = null, string $name = null, string $givenName = null, string $familyName = null, + UriInterface $photo = null, UriInterface $photoMedium = null, UriInterface $photoSmall = null, + UriInterface $batch = null, UriInterface $notify = null, UriInterface $poll = null, UriInterface $subscribe = null, + bool $searchable = null, string $pubKey = null, UriInterface $baseurl = null, int $gsid = null, + \DateTime $updated = null, int $interacting_count = 0, int $interacted_count = 0, int $post_count = 0, int $uriId = null + ) { + $this->uriId = $uriId; + $this->url = $url; + $this->guid = $guid; + $this->addr = $addr; + $this->alias = $alias; + $this->nick = $nick; + $this->name = $name; + $this->givenName = $givenName; + $this->familyName = $familyName; + $this->photo = $photo; + $this->photoMedium = $photoMedium; + $this->photoSmall = $photoSmall; + $this->batch = $batch; + $this->notify = $notify; + $this->poll = $poll; + $this->subscribe = $subscribe; + $this->searchable = $searchable; + $this->pubKey = $pubKey; + $this->baseurl = $baseurl; + $this->gsid = $gsid; + $this->created = $created; + $this->updated = $updated; + $this->interacting_count = $interacting_count; + $this->interacted_count = $interacted_count; + $this->post_count = $post_count; + } +} diff --git a/src/Protocol/Diaspora/Factory/DiasporaContact.php b/src/Protocol/Diaspora/Factory/DiasporaContact.php new file mode 100644 index 0000000000..d5c91d200b --- /dev/null +++ b/src/Protocol/Diaspora/Factory/DiasporaContact.php @@ -0,0 +1,102 @@ +. + * + */ + +namespace Friendica\Protocol\Diaspora\Factory; + +use Friendica\Capabilities\ICanCreateFromTableRow; +use Friendica\Database\DBA; +use GuzzleHttp\Psr7\Uri; + +class DiasporaContact extends \Friendica\BaseFactory implements ICanCreateFromTableRow +{ + public function createFromTableRow(array $row): \Friendica\Protocol\Diaspora\Entity\DiasporaContact + { + return new \Friendica\Protocol\Diaspora\Entity\DiasporaContact( + new Uri($row['url']), + new \DateTime($row['created'], new \DateTimeZone('UTC')), + $row['guid'], + $row['addr'], + $row['alias'] ? new Uri($row['alias']) : null, + $row['nick'], + $row['name'], + $row['given-name'], + $row['family-name'], + $row['photo'] ? new Uri($row['photo']) : null, + $row['photo-medium'] ? new Uri($row['photo-medium']) : null, + $row['photo-small'] ? new Uri($row['photo-small']) : null, + $row['batch'] ? new Uri($row['batch']) : null, + $row['notify'] ? new Uri($row['notify']) : null, + $row['poll'] ? new Uri($row['poll']) : null, + $row['subscribe'] ? new Uri($row['subscribe']) : null, + $row['searchable'], + $row['pubkey'], + $row['baseurl'] ? new Uri($row['baseurl']) : null, + $row['gsid'], + $row['updated'] !== DBA::NULL_DATETIME ? new \DateTime($row['updated'], new \DateTimeZone('UTC')) : null, + $row['interacting_count'], + $row['interacted_count'], + $row['post_count'], + $row['uri-id'], + ); + } + + /** + * @param array $data Data returned by \Friendica\Network\Probe::uri() + * @param int $uriId The URI ID of the Diaspora contact URL + GUID + * @param \DateTime $created + * @param int $interacting_count + * @param int $interacted_count + * @param int $post_count + * @return \Friendica\Protocol\Diaspora\Entity\DiasporaContact + */ + public function createfromProbeData(array $data, int $uriId, \DateTime $created, int $interacting_count = 0, int $interacted_count = 0, int $post_count = 0): \Friendica\Protocol\Diaspora\Entity\DiasporaContact + { + $alias = $data['alias'] != $data['url'] ? $data['alias'] : null; + + return new \Friendica\Protocol\Diaspora\Entity\DiasporaContact( + new Uri($data['url']), + $created, + $data['guid'], + $data['addr'], + $alias ? new Uri($alias) : null, + $data['nick'], + $data['name'], + $data['given-name'] ?? '', + $data['family-name'] ?? '', + $data['photo'] ? new Uri($data['photo']) : null, + !empty($data['photo_medium']) ? new Uri($data['photo_medium']) : null, + !empty($data['photo_small']) ? new Uri($data['photo_small']) : null, + $data['batch'] ? new Uri($data['batch']) : null, + $data['notify'] ? new Uri($data['notify']) : null, + $data['poll'] ? new Uri($data['poll']) : null, + $data['subscribe'] ? new Uri($data['subscribe']) : null, + !$data['hide'], + $data['pubkey'], + $data['baseurl'] ? new Uri($data['baseurl']) : null, + $data['gsid'], + null, + $interacting_count, + $interacted_count, + $post_count, + $uriId, + ); + } +} diff --git a/src/Protocol/Diaspora/Repository/DiasporaContact.php b/src/Protocol/Diaspora/Repository/DiasporaContact.php new file mode 100644 index 0000000000..ac8d200aa8 --- /dev/null +++ b/src/Protocol/Diaspora/Repository/DiasporaContact.php @@ -0,0 +1,283 @@ +. + * + */ + +namespace Friendica\Protocol\Diaspora\Repository; + +use Friendica\BaseRepository; +use Friendica\Core\System; +use Friendica\Database\Database; +use Friendica\Database\Definition\DbaDefinition; +use Friendica\Model\APContact; +use Friendica\Model\Contact; +use Friendica\Model\Item; +use Friendica\Model\ItemURI; +use Friendica\Network\HTTPException; +use Friendica\Protocol\Diaspora\Entity; +use Friendica\Protocol\Diaspora\Factory; +use Friendica\Protocol\WebFingerUri; +use Friendica\Util\DateTimeFormat; +use Psr\Http\Message\UriInterface; +use Psr\Log\LoggerInterface; + +class DiasporaContact extends BaseRepository +{ + const ALWAYS_UPDATE = true; + const NEVER_UPDATE = false; + const UPDATE_IF_MISSING_OR_OUTDATED = null; + + protected static $table_name = 'diaspora-contact-view'; + + /** @var Factory\DiasporaContact */ + protected $factory; + /** @var DbaDefinition */ + private $definition; + + public function __construct(DbaDefinition $definition, Database $database, LoggerInterface $logger, Factory\DiasporaContact $factory) + { + parent::__construct($database, $logger, $factory); + + $this->definition = $definition; + } + + /** + * @param array $condition + * @param array $params + * @return Entity\DiasporaContact + * @throws HTTPException\NotFoundException + */ + public function selectOne(array $condition, array $params = []): Entity\DiasporaContact + { + return parent::_selectOne($condition, $params); + } + + /** + * @param int $uriId + * @return Entity\DiasporaContact + * @throws HTTPException\NotFoundException + */ + public function selectOneByUriId(int $uriId): Entity\DiasporaContact + { + return $this->selectOne(['uri-id' => $uriId]); + } + + /** + * @param UriInterface $uri + * @return Entity\DiasporaContact + * @throws HTTPException\NotFoundException + */ + public function selectOneByUri(UriInterface $uri): Entity\DiasporaContact + { + try { + return $this->selectOne(['url' => (string) $uri]); + } catch (HTTPException\NotFoundException $e) { + } + + try { + return $this->selectOne(['addr' => (string) $uri]); + } catch (HTTPException\NotFoundException $e) { + } + + return $this->selectOne(['alias' => (string) $uri]); + } + + /** + * @param WebFingerUri $uri + * @return Entity\DiasporaContact + * @throws HTTPException\NotFoundException + */ + public function selectOneByAddr(WebFingerUri $uri): Entity\DiasporaContact + { + return $this->selectOne(['addr' => $uri->getAddr()]); + } + + /** + * @param int $uriId + * @return bool + * @throws \Exception + */ + public function existsByUriId(int $uriId): bool + { + return $this->db->exists(self::$table_name, ['uri-id' => $uriId]); + } + + public function save(Entity\DiasporaContact $DiasporaContact): Entity\DiasporaContact + { + $uriId = $DiasporaContact->uriId ?? ItemURI::insert(['uri' => $DiasporaContact->url, 'guid' => $DiasporaContact->guid]); + + $fields = [ + 'uri-id' => $uriId, + 'addr' => $DiasporaContact->addr, + 'alias' => (string)$DiasporaContact->alias, + 'nick' => $DiasporaContact->nick, + 'name' => $DiasporaContact->name, + 'given-name' => $DiasporaContact->givenName, + 'family-name' => $DiasporaContact->familyName, + 'photo' => (string)$DiasporaContact->photo, + 'photo-medium' => (string)$DiasporaContact->photoMedium, + 'photo-small' => (string)$DiasporaContact->photoSmall, + 'batch' => (string)$DiasporaContact->batch, + 'notify' => (string)$DiasporaContact->notify, + 'poll' => (string)$DiasporaContact->poll, + 'subscribe' => (string)$DiasporaContact->subscribe, + 'searchable' => $DiasporaContact->searchable, + 'pubkey' => $DiasporaContact->pubKey, + 'gsid' => $DiasporaContact->gsid, + 'created' => $DiasporaContact->created->format(DateTimeFormat::MYSQL), + 'updated' => DateTimeFormat::utcNow(), + 'interacting_count' => $DiasporaContact->interacting_count, + 'interacted_count' => $DiasporaContact->interacted_count, + 'post_count' => $DiasporaContact->post_count, + ]; + + // Limit the length on incoming fields + $fields = $this->definition->truncateFieldsForTable('diaspora-contact', $fields); + + $this->db->insert('diaspora-contact', $fields, Database::INSERT_UPDATE); + + return $this->selectOneByUriId($uriId); + } + + /** + * Fetch a Diaspora profile from a given WebFinger address and updates it depending on the mode + * + * @param WebFingerUri $uri Profile address + * @param boolean $update true = always update, false = never update, null = update when not found or outdated + * @return Entity\DiasporaContact + * @throws HTTPException\NotFoundException + */ + public function getByAddr(WebFingerUri $uri, ?bool $update = self::UPDATE_IF_MISSING_OR_OUTDATED): Entity\DiasporaContact + { + if ($update !== self::ALWAYS_UPDATE) { + try { + $dcontact = $this->selectOneByAddr($uri); + if ($update === self::NEVER_UPDATE) { + return $dcontact; + } + } catch (HTTPException\NotFoundException $e) { + if ($update === self::NEVER_UPDATE) { + throw $e; + } + + // This is necessary for Contact::getByURL in case the base contact record doesn't need probing, + // but we still need the result of a probe to create the missing diaspora-contact record. + $update = self::ALWAYS_UPDATE; + } + } + + $contact = Contact::getByURL($uri, $update, ['uri-id']); + if (empty($contact['uri-id'])) { + throw new HTTPException\NotFoundException('Diaspora profile with URI ' . $uri . ' not found'); + } + + return self::selectOneByUriId($contact['uri-id']); + } + + /** + * Fetch a Diaspora profile from a given profile URL and updates it depending on the mode + * + * @param UriInterface $uri Profile URL + * @param boolean $update true = always update, false = never update, null = update when not found or outdated + * @return Entity\DiasporaContact + * @throws HTTPException\NotFoundException + */ + public function getByUrl(UriInterface $uri, ?bool $update = self::UPDATE_IF_MISSING_OR_OUTDATED): Entity\DiasporaContact + { + if ($update !== self::ALWAYS_UPDATE) { + try { + $dcontact = $this->selectOneByUriId(ItemURI::getIdByURI($uri)); + if ($update === self::NEVER_UPDATE) { + return $dcontact; + } + } catch (HTTPException\NotFoundException $e) { + if ($update === self::NEVER_UPDATE) { + throw $e; + } + + // This is necessary for Contact::getByURL in case the base contact record doesn't need probing, + // but we still need the result of a probe to create the missing diaspora-contact record. + $update = self::ALWAYS_UPDATE; + } + } + + $contact = Contact::getByURL($uri, $update, ['uri-id']); + if (empty($contact['uri-id'])) { + throw new HTTPException\NotFoundException('Diaspora profile with URI ' . $uri . ' not found'); + } + + return self::selectOneByUriId($contact['uri-id']); + } + + /** + * Update or create a diaspora-contact entry via a probe array + * + * @param array $data Probe array + * @return Entity\DiasporaContact + * @throws \Exception + */ + public function updateFromProbeArray(array $data): Entity\DiasporaContact + { + $uriId = ItemURI::insert(['uri' => $data['url'], 'guid' => $data['guid']]); + + $contact = Contact::getByUriId($uriId, ['id', 'created']); + $apcontact = APContact::getByURL($data['url'], false); + if (!empty($apcontact)) { + $interacting_count = $apcontact['followers_count']; + $interacted_count = $apcontact['following_count']; + $post_count = $apcontact['statuses_count']; + } elseif (!empty($contact['id'])) { + $last_interaction = DateTimeFormat::utc('now - 180 days'); + + $interacting_count = $this->db->count('contact-relation', ["`relation-cid` = ? AND NOT `follows` AND `last-interaction` > ?", $contact['id'], $last_interaction]); + $interacted_count = $this->db->count('contact-relation', ["`cid` = ? AND NOT `follows` AND `last-interaction` > ?", $contact['id'], $last_interaction]); + $post_count = $this->db->count('post', ['author-id' => $contact['id'], 'gravity' => [Item::GRAVITY_PARENT, Item::GRAVITY_COMMENT]]); + } + + $DiasporaContact = $this->factory->createfromProbeData( + $data, + $uriId, + new \DateTime($contact['created'] ?? 'now', new \DateTimeZone('UTC')), + $interacting_count ?? 0, + $interacted_count ?? 0, + $post_count ?? 0 + ); + + $DiasporaContact = $this->save($DiasporaContact); + + $this->logger->info('Updated diaspora-contact', ['url' => (string) $DiasporaContact->url, 'callstack' => System::callstack(20)]); + + return $DiasporaContact; + } + + /** + * get a url (scheme://domain.tld/u/user) from a given contact guid + * + * @param mixed $guid Hexadecimal string guid + * + * @return string the contact url or null + * @throws \Exception + */ + public function getUrlByGuid(string $guid): ?string + { + $diasporaContact = $this->db->selectFirst(self::$table_name, ['url'], ['guid' => $guid]); + + return $diasporaContact['url'] ?? null; + } +} diff --git a/src/Protocol/WebFingerUri.php b/src/Protocol/WebFingerUri.php new file mode 100644 index 0000000000..b9959f2db6 --- /dev/null +++ b/src/Protocol/WebFingerUri.php @@ -0,0 +1,113 @@ +. + * + */ + +namespace Friendica\Protocol; + +use GuzzleHttp\Psr7\Uri; + +class WebFingerUri +{ + /** + * @var string + */ + private $user; + /** + * @var string + */ + private $host; + /** + * @var int|null + */ + private $port; + /** + * @var string|null + */ + private $path; + + private function __construct(string $user, string $host, int $port = null, string $path = null) + { + $this->user = $user; + $this->host = $host; + $this->port = $port; + $this->path = $path; + + $this->validate(); + } + + /** + * @param string $addr + * @return WebFingerUri + */ + public static function fromString(string $addr): WebFingerUri + { + $uri = new Uri('acct://' . preg_replace('/^acct:/', '', $addr)); + + return new self($uri->getUserInfo(), $uri->getHost(), $uri->getPort(), $uri->getPath()); + } + + private function validate() + { + if (!$this->user) { + throw new \InvalidArgumentException('WebFinger URI User part is required'); + } + + if (!$this->host) { + throw new \InvalidArgumentException('WebFinger URI Host part is required'); + } + } + + public function getUser(): string + { + return $this->user; + } + + public function getHost(): string + { + return $this->host; + } + + public function getFullHost(): string + { + return $this->host + . ($this->port ? ':' . $this->port : '') . + ($this->path ?: ''); + } + + public function getLongForm(): string + { + return 'acct:' . $this->getShortForm(); + } + + public function getShortForm(): string + { + return $this->user . '@' . $this->getFullHost(); + } + + public function getAddr(): string + { + return $this->getShortForm(); + } + + public function __toString(): string + { + return $this->getShortForm(); + } +} diff --git a/src/Worker/Delivery.php b/src/Worker/Delivery.php index 706adb401e..cec894480c 100644 --- a/src/Worker/Delivery.php +++ b/src/Worker/Delivery.php @@ -29,7 +29,6 @@ use Friendica\Core\Worker; use Friendica\Database\DBA; use Friendica\DI; use Friendica\Model\Contact; -use Friendica\Model\FContact; use Friendica\Model\GServer; use Friendica\Model\Item; use Friendica\Model\Post; @@ -94,7 +93,7 @@ class Delivery if ($item['verb'] == Activity::ANNOUNCE) { continue; } - + if ($item['id'] == $parent_id) { $parent = $item; } @@ -278,7 +277,7 @@ class Delivery private static function deliverDFRN(string $cmd, array $contact, array $owner, array $items, array $target_item, bool $public_message, bool $top_level, bool $followup, int $server_protocol = null) { // Transmit Diaspora reshares via Diaspora if the Friendica contact support Diaspora - if (Diaspora::getReshareDetails($target_item ?? []) && !empty(FContact::getByURL($contact['addr'], false))) { + if (Diaspora::getReshareDetails($target_item ?? []) && Diaspora::isSupportedByContactUrl($contact['addr'], false)) { Logger::info('Reshare will be transmitted via Diaspora', ['url' => $contact['url'], 'guid' => ($target_item['guid'] ?? '') ?: $target_item['id']]); self::deliverDiaspora($cmd, $contact, $owner, $items, $target_item, $public_message, $top_level, $followup); return; diff --git a/src/Worker/ExpirePosts.php b/src/Worker/ExpirePosts.php index 6f75d18559..a2ef33f330 100644 --- a/src/Worker/ExpirePosts.php +++ b/src/Worker/ExpirePosts.php @@ -189,7 +189,7 @@ class ExpirePosts AND NOT EXISTS(SELECT `uri-id` FROM `user-contact` WHERE `uri-id` = `item-uri`.`id`) AND NOT EXISTS(SELECT `uri-id` FROM `contact` WHERE `uri-id` = `item-uri`.`id`) AND NOT EXISTS(SELECT `uri-id` FROM `apcontact` WHERE `uri-id` = `item-uri`.`id`) - AND NOT EXISTS(SELECT `uri-id` FROM `fcontact` WHERE `uri-id` = `item-uri`.`id`) + AND NOT EXISTS(SELECT `uri-id` FROM `diaspora-contact` WHERE `uri-id` = `item-uri`.`id`) AND NOT EXISTS(SELECT `uri-id` FROM `inbox-status` WHERE `uri-id` = `item-uri`.`id`) AND NOT EXISTS(SELECT `uri-id` FROM `post-delivery` WHERE `uri-id` = `item-uri`.`id`) AND NOT EXISTS(SELECT `uri-id` FROM `post-delivery` WHERE `inbox-id` = `item-uri`.`id`) diff --git a/src/Worker/UpdateFContact.php b/src/Worker/UpdateFContact.php deleted file mode 100644 index 260e071715..0000000000 --- a/src/Worker/UpdateFContact.php +++ /dev/null @@ -1,41 +0,0 @@ -. - * - */ - -namespace Friendica\Worker; - -use Friendica\Core\Logger; -use Friendica\Model\FContact; - -class UpdateFContact -{ - /** - * Update fcontact data via probe - * - * @param string $handle Contact handle - * @return void - */ - public static function execute(string $handle) - { - $success = FContact::getByURL($handle, true); - - Logger::info('Updated from probe', ['handle' => $handle, 'success' => $success]); - } -} diff --git a/static/dbstructure.config.php b/static/dbstructure.config.php index f8e01e51ee..d5bbed666a 100644 --- a/static/dbstructure.config.php +++ b/static/dbstructure.config.php @@ -55,7 +55,7 @@ use Friendica\Database\DBA; if (!defined('DB_UPDATE_VERSION')) { - define('DB_UPDATE_VERSION', 1497); + define('DB_UPDATE_VERSION', 1500); } return [ @@ -637,6 +637,39 @@ return [ "wid" => ["wid"], ] ], + "diaspora-contact" => [ + "comment" => "Diaspora compatible contacts - used in the Diaspora implementation", + "fields" => [ + "uri-id" => ["type" => "int unsigned", "not null" => "1", "primary" => "1", "foreign" => ["item-uri" => "id"], "comment" => "Id of the item-uri table entry that contains the contact URL"], + "addr" => ["type" => "varchar(255)", "comment" => ""], + "alias" => ["type" => "varchar(255)", "comment" => ""], + "nick" => ["type" => "varchar(255)", "comment" => ""], + "name" => ["type" => "varchar(255)", "comment" => ""], + "given-name" => ["type" => "varchar(255)", "comment" => ""], + "family-name" => ["type" => "varchar(255)", "comment" => ""], + "photo" => ["type" => "varchar(255)", "comment" => ""], + "photo-medium" => ["type" => "varchar(255)", "comment" => ""], + "photo-small" => ["type" => "varchar(255)", "comment" => ""], + "batch" => ["type" => "varchar(255)", "comment" => ""], + "notify" => ["type" => "varchar(255)", "comment" => ""], + "poll" => ["type" => "varchar(255)", "comment" => ""], + "subscribe" => ["type" => "varchar(255)", "comment" => ""], + "searchable" => ["type" => "boolean", "comment" => ""], + "pubkey" => ["type" => "text", "comment" => ""], + "gsid" => ["type" => "int unsigned", "foreign" => ["gserver" => "id", "on delete" => "restrict"], "comment" => "Global Server ID"], + "created" => ["type" => "datetime", "not null" => "1", "default" => DBA::NULL_DATETIME, "comment" => ""], + "updated" => ["type" => "datetime", "not null" => "1", "default" => DBA::NULL_DATETIME, "comment" => ""], + "interacting_count" => ["type" => "int unsigned", "default" => 0, "comment" => "Number of contacts this contact interactes with"], + "interacted_count" => ["type" => "int unsigned", "default" => 0, "comment" => "Number of contacts that interacted with this contact"], + "post_count" => ["type" => "int unsigned", "default" => 0, "comment" => "Number of posts and comments"], + ], + "indexes" => [ + "PRIMARY" => ["uri-id"], + "addr" => ["UNIQUE", "addr"], + "alias" => ["alias"], + "gsid" => ["gsid"], + ] + ], "diaspora-interaction" => [ "comment" => "Signed Diaspora Interaction", "fields" => [ @@ -690,39 +723,6 @@ return [ "uri-id" => ["uri-id"], ] ], - "fcontact" => [ - "comment" => "Diaspora compatible contacts - used in the Diaspora implementation", - "fields" => [ - "id" => ["type" => "int unsigned", "not null" => "1", "extra" => "auto_increment", "primary" => "1", "comment" => "sequential ID"], - "guid" => ["type" => "varbinary(255)", "not null" => "1", "default" => "", "comment" => "unique id"], - "url" => ["type" => "varbinary(383)", "not null" => "1", "default" => "", "comment" => ""], - "uri-id" => ["type" => "int unsigned", "foreign" => ["item-uri" => "id"], "comment" => "Id of the item-uri table entry that contains the fcontact url"], - "name" => ["type" => "varchar(255)", "not null" => "1", "default" => "", "comment" => ""], - "photo" => ["type" => "varbinary(383)", "not null" => "1", "default" => "", "comment" => ""], - "request" => ["type" => "varbinary(383)", "not null" => "1", "default" => "", "comment" => ""], - "nick" => ["type" => "varchar(255)", "not null" => "1", "default" => "", "comment" => ""], - "addr" => ["type" => "varchar(255)", "not null" => "1", "default" => "", "comment" => ""], - "batch" => ["type" => "varbinary(383)", "not null" => "1", "default" => "", "comment" => ""], - "notify" => ["type" => "varbinary(383)", "not null" => "1", "default" => "", "comment" => ""], - "poll" => ["type" => "varbinary(383)", "not null" => "1", "default" => "", "comment" => ""], - "confirm" => ["type" => "varbinary(383)", "not null" => "1", "default" => "", "comment" => ""], - "priority" => ["type" => "tinyint unsigned", "not null" => "1", "default" => "0", "comment" => ""], - "network" => ["type" => "char(4)", "not null" => "1", "default" => "", "comment" => ""], - "alias" => ["type" => "varbinary(383)", "not null" => "1", "default" => "", "comment" => ""], - "pubkey" => ["type" => "text", "comment" => ""], - "created" => ["type" => "datetime", "not null" => "1", "default" => DBA::NULL_DATETIME, "comment" => ""], - "updated" => ["type" => "datetime", "not null" => "1", "default" => DBA::NULL_DATETIME, "comment" => ""], - "interacting_count" => ["type" => "int unsigned", "default" => 0, "comment" => "Number of contacts this contact interactes with"], - "interacted_count" => ["type" => "int unsigned", "default" => 0, "comment" => "Number of contacts that interacted with this contact"], - "post_count" => ["type" => "int unsigned", "default" => 0, "comment" => "Number of posts and comments"], - ], - "indexes" => [ - "PRIMARY" => ["id"], - "addr" => ["addr(32)"], - "url" => ["UNIQUE", "url(190)"], - "uri-id" => ["UNIQUE", "uri-id"], - ] - ], "fetch-entry" => [ "comment" => "", "fields" => [ @@ -870,7 +870,7 @@ return [ "fields" => [ "id" => ["type" => "int unsigned", "not null" => "1", "extra" => "auto_increment", "primary" => "1", "comment" => "sequential ID"], "uid" => ["type" => "mediumint unsigned", "not null" => "1", "default" => "0", "foreign" => ["user" => "uid"], "comment" => "User id"], - "fid" => ["type" => "int unsigned", "relation" => ["fcontact" => "id"], "comment" => "deprecated"], + "fid" => ["type" => "int unsigned", "comment" => "deprecated"], "contact-id" => ["type" => "int unsigned", "not null" => "1", "default" => "0", "foreign" => ["contact" => "id"], "comment" => ""], "suggest-cid" => ["type" => "int unsigned", "foreign" => ["contact" => "id"], "comment" => "Suggested contact"], "knowyou" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => ""], diff --git a/static/dbview.config.php b/static/dbview.config.php index 66f3de3b63..c82e7dc77f 100644 --- a/static/dbview.config.php +++ b/static/dbview.config.php @@ -155,7 +155,6 @@ "author-hidden" => ["author", "hidden"], "author-updated" => ["author", "updated"], "author-gsid" => ["author", "gsid"], - "author-uri-id" => ["author", "uri-id"], "owner-id" => ["post-user", "owner-id"], "owner-uri-id" => ["owner", "uri-id"], "owner-link" => ["owner", "url"], @@ -332,7 +331,6 @@ "author-hidden" => ["author", "hidden"], "author-updated" => ["author", "updated"], "author-gsid" => ["author", "gsid"], - "author-uri-id" => ["author", "uri-id"], "owner-id" => ["post-thread-user", "owner-id"], "owner-uri-id" => ["owner", "uri-id"], "owner-link" => ["owner", "url"], @@ -495,7 +493,6 @@ "author-hidden" => ["author", "hidden"], "author-updated" => ["author", "updated"], "author-gsid" => ["author", "gsid"], - "author-uri-id" => ["author", "uri-id"], "owner-id" => ["post", "owner-id"], "owner-uri-id" => ["owner", "uri-id"], "owner-link" => ["owner", "url"], @@ -634,7 +631,6 @@ "author-hidden" => ["author", "hidden"], "author-updated" => ["author", "updated"], "author-gsid" => ["author", "gsid"], - "author-uri-id" => ["author", "uri-id"], "owner-id" => ["post-thread", "owner-id"], "owner-uri-id" => ["owner", "uri-id"], "owner-link" => ["owner", "url"], @@ -994,11 +990,11 @@ "blocked" => ["contact", "blocked"], "dfrn-notify" => ["contact", "notify"], "dfrn-poll" => ["contact", "poll"], - "diaspora-guid" => ["fcontact", "guid"], - "diaspora-batch" => ["fcontact", "batch"], - "diaspora-notify" => ["fcontact", "notify"], - "diaspora-poll" => ["fcontact", "poll"], - "diaspora-alias" => ["fcontact", "alias"], + "diaspora-guid" => ["item-uri", "guid"], + "diaspora-batch" => ["diaspora-contact", "batch"], + "diaspora-notify" => ["diaspora-contact", "notify"], + "diaspora-poll" => ["diaspora-contact", "poll"], + "diaspora-alias" => ["diaspora-contact", "alias"], "ap-uuid" => ["apcontact", "uuid"], "ap-type" => ["apcontact", "type"], "ap-following" => ["apcontact", "following"], @@ -1017,9 +1013,9 @@ "query" => "FROM `contact` LEFT JOIN `item-uri` ON `item-uri`.`id` = `contact`.`uri-id` LEFT JOIN `apcontact` ON `apcontact`.`uri-id` = `contact`.`uri-id` - LEFT JOIN `fcontact` ON `fcontact`.`uri-id` = contact.`uri-id` + LEFT JOIN `diaspora-contact` ON `diaspora-contact`.`uri-id` = contact.`uri-id` LEFT JOIN `gserver` ON `gserver`.`id` = contact.`gsid` - WHERE `contact`.`uid` = 0" + WHERE `contact`.`uid` = 0" ], "account-user-view" => [ "fields" => [ @@ -1093,14 +1089,14 @@ "reason" => ["ucontact", "reason"], "dfrn-notify" => ["contact", "notify"], "dfrn-poll" => ["contact", "poll"], - "diaspora-guid" => ["fcontact", "guid"], - "diaspora-batch" => ["fcontact", "batch"], - "diaspora-notify" => ["fcontact", "notify"], - "diaspora-poll" => ["fcontact", "poll"], - "diaspora-alias" => ["fcontact", "alias"], - "diaspora-interacting_count" => ["fcontact", "interacting_count"], - "diaspora-interacted_count" => ["fcontact", "interacted_count"], - "diaspora-post_count" => ["fcontact", "post_count"], + "diaspora-guid" => ["item-uri", "guid"], + "diaspora-batch" => ["diaspora-contact", "batch"], + "diaspora-notify" => ["diaspora-contact", "notify"], + "diaspora-poll" => ["diaspora-contact", "poll"], + "diaspora-alias" => ["diaspora-contact", "alias"], + "diaspora-interacting_count" => ["diaspora-contact", "interacting_count"], + "diaspora-interacted_count" => ["diaspora-contact", "interacted_count"], + "diaspora-post_count" => ["diaspora-contact", "post_count"], "ap-uuid" => ["apcontact", "uuid"], "ap-type" => ["apcontact", "type"], "ap-following" => ["apcontact", "following"], @@ -1120,7 +1116,7 @@ INNER JOIN `contact` ON `contact`.`uri-id` = `ucontact`.`uri-id` AND `contact`.`uid` = 0 LEFT JOIN `item-uri` ON `item-uri`.`id` = `ucontact`.`uri-id` LEFT JOIN `apcontact` ON `apcontact`.`uri-id` = `ucontact`.`uri-id` - LEFT JOIN `fcontact` ON `fcontact`.`uri-id` = `ucontact`.`uri-id` AND `fcontact`.`network` = 'dspr' + LEFT JOIN `diaspora-contact` ON `diaspora-contact`.`uri-id` = `ucontact`.`uri-id` LEFT JOIN `gserver` ON `gserver`.`id` = contact.`gsid`" ], "pending-view" => [ @@ -1190,5 +1186,36 @@ "query" => "FROM `profile_field` INNER JOIN `permissionset` ON `permissionset`.`id` = `profile_field`.`psid`" ], + "diaspora-contact-view" => [ + "fields" => [ + "uri-id" => ["diaspora-contact", "uri-id"], + "url" => ["item-uri", "uri"], + "guid" => ["item-uri", "guid"], + "addr" => ["diaspora-contact", "addr"], + "alias" => ["diaspora-contact", "alias"], + "nick" => ["diaspora-contact", "nick"], + "name" => ["diaspora-contact", "name"], + "given-name" => ["diaspora-contact", "given-name"], + "family-name" => ["diaspora-contact", "family-name"], + "photo" => ["diaspora-contact", "photo"], + "photo-medium" => ["diaspora-contact", "photo-medium"], + "photo-small" => ["diaspora-contact", "photo-small"], + "batch" => ["diaspora-contact", "batch"], + "notify" => ["diaspora-contact", "notify"], + "poll" => ["diaspora-contact", "poll"], + "subscribe" => ["diaspora-contact", "subscribe"], + "searchable" => ["diaspora-contact", "searchable"], + "pubkey" => ["diaspora-contact", "pubkey"], + "baseurl" => ["gserver", "url"], + "gsid" => ["diaspora-contact", "gsid"], + "created" => ["diaspora-contact", "created"], + "updated" => ["diaspora-contact", "updated"], + "interacting_count" => ["diaspora-contact", "interacting_count"], + "interacted_count" => ["diaspora-contact", "interacted_count"], + "post_count" => ["diaspora-contact", "post_count"], + ], + "query" => "FROM `diaspora-contact` + INNER JOIN `item-uri` ON `item-uri`.`id` = `diaspora-contact`.`uri-id` + LEFT JOIN `gserver` ON `gserver`.`id` = `diaspora-contact`.`gsid`" + ], ]; - diff --git a/tests/src/Protocol/WebFingerUriTest.php b/tests/src/Protocol/WebFingerUriTest.php new file mode 100644 index 0000000000..7378040d25 --- /dev/null +++ b/tests/src/Protocol/WebFingerUriTest.php @@ -0,0 +1,135 @@ +. + * + * Main database structure configuration file. + * + * Here are described all the tables, fields and indexes Friendica needs to work. + * The entry order is mostly alphabetic - with the exception of tables that are used in foreign keys. + * + * Syntax (braces indicate optionale values): + * "" => [ + * "comment" => "Description of the table", + * "fields" => [ + * "" => [ + * "type" => "{()} ", + * "not null" => 0|1, + * {"extra" => "auto_increment",} + * {"default" => "",} + * {"default" => NULL_DATE,} (for datetime fields) + * {"primary" => "1",} + * {"foreign|relation" => ["" => ""],} + * "comment" => "Description of the fields" + * ], + * ... + * ], + * "indexes" => [ + * "PRIMARY" => ["", ...], + * "" => [{"UNIQUE",} "{()}", ...] + * ... + * ], + * ], + * + * Whenever possible prefer "foreign" before "relation" with the foreign keys. + * "foreign" adds true foreign keys on the database level, while "relation" is just an indicator of a table relation without any consequences + * + * If you need to make any change, make sure to increment the DB_UPDATE_VERSION constant value below. + * + */ + +namespace Friendica\Test\src\Protocol; + +use Friendica\Protocol\WebFingerUri; +use PHPUnit\Framework\TestCase; + +class WebFingerUriTest extends TestCase +{ + public function dataFromString(): array + { + return [ + 'long' => [ + 'expectedLong' => 'acct:selma@www.example.com:8080/friend', + 'expectedShort' => 'selma@www.example.com:8080/friend', + 'input' => 'acct:selma@www.example.com:8080/friend', + ], + 'short' => [ + 'expectedLong' => 'acct:selma@www.example.com:8080/friend', + 'expectedShort' => 'selma@www.example.com:8080/friend', + 'input' => 'selma@www.example.com:8080/friend', + ], + 'minimal' => [ + 'expectedLong' => 'acct:bob@example.com', + 'expectedShort' => 'bob@example.com', + 'input' => 'bob@example.com', + ], + 'acct:' => [ + 'expectedLong' => 'acct:alice@example.acct:90', + 'expectedShort' => 'alice@example.acct:90', + 'input' => 'alice@example.acct:90', + ], + ]; + } + + /** + * @dataProvider dataFromString + * @param string $expectedLong + * @param string $expectedShort + * @param string $input + * @return void + */ + public function testFromString(string $expectedLong, string $expectedShort, string $input) + { + $uri = WebFingerUri::fromString($input); + + $this->assertEquals($expectedLong, $uri->getLongForm()); + $this->assertEquals($expectedShort, $uri->getShortForm()); + } + + public function dataFromStringFailure() + { + return [ + 'missing user' => [ + 'input' => 'example.com', + ], + 'missing user @' => [ + 'input' => '@example.com', + ], + 'missing host' => [ + 'input' => 'alice', + ], + 'missing host @' => [ + 'input' => 'alice@', + ], + 'missing everything' => [ + 'input' => '', + ], + ]; + } + + /** + * @dataProvider dataFromStringFailure + * @param string $input + * @return void + */ + public function testFromStringFailure(string $input) + { + $this->expectException(\InvalidArgumentException::class); + + WebFingerUri::fromString($input); + } +} diff --git a/update.php b/update.php index 1767470571..12a3fb74e6 100644 --- a/update.php +++ b/update.php @@ -974,7 +974,7 @@ function update_1429() return Update::FAILED; } - if (!DBA::e("UPDATE `fcontact` SET `uri-id` = null WHERE NOT `uri-id` IS NULL")) { + if (DBStructure::existsTable('fcontact') && !DBA::e("UPDATE `fcontact` SET `uri-id` = null WHERE NOT `uri-id` IS NULL")) { return Update::FAILED; } @@ -1013,6 +1013,10 @@ function update_1438() function update_1439() { + if (!DBStructure::existsTable('fcontact')) { + return Update::SUCCESS; + } + $intros = DBA::select('intro', ['id', 'fid'], ["NOT `fid` IS NULL AND `fid` != ?", 0]); while ($intro = DBA::fetch($intros)) { $fcontact = DBA::selectFirst('fcontact', ['url'], ['id' => $intro['fid']]); @@ -1024,6 +1028,8 @@ function update_1439() } } DBA::close($intros); + + return Update::SUCCESS; } function update_1440() From 81454547076f242dfe2fc19ab86d659b28405eb2 Mon Sep 17 00:00:00 2001 From: Marek Bachmann Date: Tue, 6 Dec 2022 00:48:28 +0100 Subject: [PATCH 04/21] Revert "Merge branch 'develop' into new_image_presentation" This reverts commit 76f4ba7685dc3c722464d3b5f8be8e1b08fef05a, reversing changes made to 733543505be2345b1a763b4924413569193773cc. --- src/Model/Item.php | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/Model/Item.php b/src/Model/Item.php index 152543b916..634dbbd0ea 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -21,6 +21,8 @@ namespace Friendica\Model; +use DOMDocument; +use DOMXPath; use Friendica\Content\Text\BBCode; use Friendica\Content\Text\HTML; use Friendica\Core\Hook; @@ -3085,9 +3087,10 @@ class Item ]; Hook::callAll('prepare_body', $hook_data); // Remove old images - $hook_data['html'] = preg_replace('|(.*)|', '', $hook_data['html']); - $grid = self::make_image_grid($hook_data); - $s = $hook_data['html'] . $grid; +// $hook_data['html'] = preg_replace('|(.*)|', '', $hook_data['html']); +// $grid = self::make_image_grid($hook_data); +// $s = $hook_data['html'] . $grid; + $s = $hook_data['html']; unset($hook_data); if (!$attach) { @@ -3131,6 +3134,27 @@ class Item return $hook_data['html']; } + /** + * This function removes images at the very end of a post based on the assumption that this images are interpreted + * as attachments + * @param array $rendered_html + * @return array + */ + private function cutAttachedImages(array &$rendered_html) { + $doc = new DOMDocument(); + $doc->loadHTML($rendered_html); + + $xpathsearch = new DOMXPath($doc); + $nodes = $xpathsearch->query("*"); + + return $nodes; + } + + /** + * @param array $data + * @return string|void + * @throws \Friendica\Network\HTTPException\ServiceUnavailableException + */ private function make_image_grid(array &$data) { $item = $data['item']; @@ -3149,6 +3173,7 @@ class Item 'preview' => $preview_url, 'attachment' => $attachment, ]); + // @todo add some fany ai to divide images equally on both columns if ($count % 2 == 0) { $img_tags_fc[] = $img_tag; } else { From 7fb0f018dfb92f05290d454d4ff47771957638d4 Mon Sep 17 00:00:00 2001 From: Marek Bachmann Date: Mon, 5 Dec 2022 03:27:51 +0100 Subject: [PATCH 05/21] First draft for using a image grid to display attached images. --- src/Model/Item.php | 37 ++++++++++++++++- view/templates/content/image_grid.tpl | 14 +++++++ view/templates/content/image_grid_column.tpl | 1 + view/theme/frio/css/image_grid.css | 43 ++++++++++++++++++++ 4 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 view/templates/content/image_grid.tpl create mode 100644 view/templates/content/image_grid_column.tpl create mode 100644 view/theme/frio/css/image_grid.css diff --git a/src/Model/Item.php b/src/Model/Item.php index 8026d567cb..1fff0a5869 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -2991,6 +2991,7 @@ class Item $a = DI::app(); Hook::callAll('prepare_body_init', $item); + // In order to provide theme developers more possibilities, event items // are treated differently. if ($item['object-type'] === Activity\ObjectType::EVENT && isset($item['event-id'])) { @@ -3049,6 +3050,7 @@ class Item $item['body'] = self::replaceVisualAttachments($attachments, $item['body'] ?? ''); $item['body'] = preg_replace("/\s*\[attachment .*?\].*?\[\/attachment\]\s*/ism", "\n", $item['body']); + self::putInCache($item); $item['body'] = $body; $s = $item["rendered-html"]; @@ -3082,7 +3084,10 @@ class Item 'filter_reasons' => $filter_reasons ]; Hook::callAll('prepare_body', $hook_data); - $s = $hook_data['html']; + // Remove old images + $hook_data['html'] = preg_replace('|(.*)|', '', $hook_data['html']); + $grid = self::make_image_grid($hook_data); + $s = $hook_data['html'] . $grid; unset($hook_data); if (!$attach) { @@ -3126,6 +3131,36 @@ class Item return $hook_data['html']; } + private function make_image_grid(array &$data) + { + $item = $data['item']; + if ($item['has-media']) { + $attachments = Post\Media::splitAttachments($item['uri-id'], [], $item['has-media'] ?? false); + if (count($attachments['visual']) > 1) { + $img_tags = array(); + foreach ($attachments['visual'] as $attachment) { + $src_url = Post\Media::getUrlForId($attachment['id']); + $preview_url = Post\Media::getPreviewUrlForId($attachment['id'], ($attachment['width'] > $attachment['height']) ? Proxy::SIZE_MEDIUM : Proxy::SIZE_LARGE); + $img_tag = array( + '$image' => [ + 'src' => $src_url, + 'preview' => $preview_url, + 'attachment' => $attachment, + ]); + $img_tags[] = $img_tag; + } + $img_grid = Renderer::replaceMacros(Renderer::getMarkupTemplate('content/image_grid.tpl'), [ + 'columns' => [ + 'fc' => $img_tags[0], + 'sc' => $img_tags[1], + ], + ]); + return $img_grid; + } + } + + } + /** * Check if the body contains a link * diff --git a/view/templates/content/image_grid.tpl b/view/templates/content/image_grid.tpl new file mode 100644 index 0000000000..7a06d33726 --- /dev/null +++ b/view/templates/content/image_grid.tpl @@ -0,0 +1,14 @@ + + +
+
+ {{foreach $columns.fc as $img}} + {{include file="content/image.tpl" image=$img}} + {{/foreach}} +
+
+ {{foreach $columns.sc as $img}} + {{include file="content/image.tpl" image=$img}} + {{/foreach}} +
+
\ No newline at end of file diff --git a/view/templates/content/image_grid_column.tpl b/view/templates/content/image_grid_column.tpl new file mode 100644 index 0000000000..6a58a4bda2 --- /dev/null +++ b/view/templates/content/image_grid_column.tpl @@ -0,0 +1 @@ +
{{$images}}
\ No newline at end of file diff --git a/view/theme/frio/css/image_grid.css b/view/theme/frio/css/image_grid.css new file mode 100644 index 0000000000..dd29dd201d --- /dev/null +++ b/view/theme/frio/css/image_grid.css @@ -0,0 +1,43 @@ +* { + box-sizing: border-box; +} + +.row { + display: -ms-flexbox; /* IE10 */ + display: flex; + -ms-flex-wrap: wrap; /* IE10 */ + flex-wrap: wrap; + padding: 0 4px; +} + +/* Create four equal columns that sits next to each other */ +.column { + -ms-flex: 50%; /* IE10 */ + flex: 50%; + max-width: 50%; + padding: 0 4px; +} + +.column img { + margin-top: 8px; + vertical-align: middle; + width: 100%; +} + +/*!* Responsive layout - makes a two column-layout instead of four columns *!*/ +/*@media screen and (max-width: 50px) {*/ +/* .column {*/ +/* -ms-flex: 50%;*/ +/* flex: 50%;*/ +/* max-width: 50%;*/ +/* }*/ +/*}*/ + +/* Responsive layout - makes the two columns stack on top of each other instead of next to each other */ +@media screen and (max-width: 150px) { + .column { + -ms-flex: 100%; + flex: 100%; + max-width: 100%; + } +} \ No newline at end of file From 458f22d303e6afe653a296c377f177ad428e616d Mon Sep 17 00:00:00 2001 From: Marek Bachmann Date: Mon, 5 Dec 2022 04:23:34 +0100 Subject: [PATCH 06/21] Support more than 2 images --- src/Model/Item.php | 16 ++++++++++++---- view/templates/content/image_grid.tpl | 12 ++++++++---- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/Model/Item.php b/src/Model/Item.php index 1fff0a5869..152543b916 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -3137,7 +3137,9 @@ class Item if ($item['has-media']) { $attachments = Post\Media::splitAttachments($item['uri-id'], [], $item['has-media'] ?? false); if (count($attachments['visual']) > 1) { - $img_tags = array(); + $img_tags_fc = array(); + $img_tags_sc = array(); + $count = 0; foreach ($attachments['visual'] as $attachment) { $src_url = Post\Media::getUrlForId($attachment['id']); $preview_url = Post\Media::getPreviewUrlForId($attachment['id'], ($attachment['width'] > $attachment['height']) ? Proxy::SIZE_MEDIUM : Proxy::SIZE_LARGE); @@ -3147,12 +3149,18 @@ class Item 'preview' => $preview_url, 'attachment' => $attachment, ]); - $img_tags[] = $img_tag; + if ($count % 2 == 0) { + $img_tags_fc[] = $img_tag; + } else { + $img_tags_sc[] = $img_tag; + } + ++$count; } + $img_grid = Renderer::replaceMacros(Renderer::getMarkupTemplate('content/image_grid.tpl'), [ 'columns' => [ - 'fc' => $img_tags[0], - 'sc' => $img_tags[1], + 'fc' => $img_tags_fc, + 'sc' => $img_tags_sc, ], ]); return $img_grid; diff --git a/view/templates/content/image_grid.tpl b/view/templates/content/image_grid.tpl index 7a06d33726..1a8648d189 100644 --- a/view/templates/content/image_grid.tpl +++ b/view/templates/content/image_grid.tpl @@ -2,13 +2,17 @@
- {{foreach $columns.fc as $img}} - {{include file="content/image.tpl" image=$img}} + {{foreach $columns.fc as $fc}} + {{foreach $fc as $img}} + {{include file="content/image.tpl" image=$img}} + {{/foreach}} {{/foreach}}
- {{foreach $columns.sc as $img}} - {{include file="content/image.tpl" image=$img}} + {{foreach $columns.sc as $sc}} + {{foreach $sc as $img}} + {{include file="content/image.tpl" image=$img}} + {{/foreach}} {{/foreach}}
\ No newline at end of file From 9778b60e64974eaf51aa45a2659cbe53993239a7 Mon Sep 17 00:00:00 2001 From: Marek Bachmann Date: Tue, 6 Dec 2022 01:14:15 +0100 Subject: [PATCH 07/21] cleaned up my git mess :-/ anyways, should work now --- src/Model/Item.php | 38 -------------------------------------- 1 file changed, 38 deletions(-) diff --git a/src/Model/Item.php b/src/Model/Item.php index b7e7fe92c9..634dbbd0ea 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -3134,44 +3134,6 @@ class Item return $hook_data['html']; } - private function make_image_grid(array &$data) - { - $item = $data['item']; - if ($item['has-media']) { - $attachments = Post\Media::splitAttachments($item['uri-id'], [], $item['has-media'] ?? false); - if (count($attachments['visual']) > 1) { - $img_tags_fc = array(); - $img_tags_sc = array(); - $count = 0; - foreach ($attachments['visual'] as $attachment) { - $src_url = Post\Media::getUrlForId($attachment['id']); - $preview_url = Post\Media::getPreviewUrlForId($attachment['id'], ($attachment['width'] > $attachment['height']) ? Proxy::SIZE_MEDIUM : Proxy::SIZE_LARGE); - $img_tag = array( - '$image' => [ - 'src' => $src_url, - 'preview' => $preview_url, - 'attachment' => $attachment, - ]); - if ($count % 2 == 0) { - $img_tags_fc[] = $img_tag; - } else { - $img_tags_sc[] = $img_tag; - } - ++$count; - } - - $img_grid = Renderer::replaceMacros(Renderer::getMarkupTemplate('content/image_grid.tpl'), [ - 'columns' => [ - 'fc' => $img_tags_fc, - 'sc' => $img_tags_sc, - ], - ]); - return $img_grid; - } - } - - } - /** * This function removes images at the very end of a post based on the assumption that this images are interpreted * as attachments From 99e20d1b009680904fe828e4159f7ef00d79778b Mon Sep 17 00:00:00 2001 From: Marek Bachmann Date: Wed, 7 Dec 2022 13:22:28 +0100 Subject: [PATCH 08/21] Temporary hack to remove images after last text line by DOM inspection --- src/Model/Item.php | 58 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 45 insertions(+), 13 deletions(-) diff --git a/src/Model/Item.php b/src/Model/Item.php index 634dbbd0ea..767c41acfd 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -3086,11 +3086,6 @@ class Item 'filter_reasons' => $filter_reasons ]; Hook::callAll('prepare_body', $hook_data); - // Remove old images -// $hook_data['html'] = preg_replace('|(.*)|', '', $hook_data['html']); -// $grid = self::make_image_grid($hook_data); -// $s = $hook_data['html'] . $grid; - $s = $hook_data['html']; unset($hook_data); if (!$attach) { @@ -3128,9 +3123,15 @@ class Item $s = HTML::applyContentFilter($s, $filter_reasons); + if (count($attachments['visual']) > 1) { + // make imgae grid only for multiple images + $s = self::cutAttachedImages($s); + $grid = self::make_image_grid($item, $attachments); + $s .= $grid; + } + $hook_data = ['item' => $item, 'html' => $s]; Hook::callAll('prepare_body_final', $hook_data); - return $hook_data['html']; } @@ -3140,14 +3141,47 @@ class Item * @param array $rendered_html * @return array */ - private function cutAttachedImages(array &$rendered_html) { + private function cutAttachedImages($rendered_html) + { $doc = new DOMDocument(); + libxml_use_internal_errors(true); $doc->loadHTML($rendered_html); + libxml_clear_errors(); - $xpathsearch = new DOMXPath($doc); - $nodes = $xpathsearch->query("*"); + $root = $doc->getElementsByTagName("p")[0]; - return $nodes; + $lastTextNode = null; + if ($root && $root->childNodes) { + foreach ($root->childNodes as $node) { + if ($node->nodeName == "#text" && strlen(trim($node->nodeValue)) > 0) { + $lastTextNode = $node; + } + } + } + + if ($lastTextNode == null) { + // no text at all, return nothing: + return ''; + } + + $toremove = array(); + if ($lastTextNode) { + $sibling = $lastTextNode->nextSibling; + while ($sibling) { + $toremove[] = array($sibling); + $sibling = $sibling->nextSibling; + } + foreach ($toremove as $remove) { + $root->removeChild($remove[0]); + } + $html = ''; + foreach ($root->childNodes as $node) { + $html .= $node->ownerDocument->saveHTML($node); + } + return $html; + } + + return $rendered_html; } /** @@ -3155,11 +3189,9 @@ class Item * @return string|void * @throws \Friendica\Network\HTTPException\ServiceUnavailableException */ - private function make_image_grid(array &$data) + private function make_image_grid(array $item, array $attachments) { - $item = $data['item']; if ($item['has-media']) { - $attachments = Post\Media::splitAttachments($item['uri-id'], [], $item['has-media'] ?? false); if (count($attachments['visual']) > 1) { $img_tags_fc = array(); $img_tags_sc = array(); From d590df9a72eb842813dff45d24d2698698a6f958 Mon Sep 17 00:00:00 2001 From: Marek Bachmann Date: Wed, 7 Dec 2022 18:50:16 +0100 Subject: [PATCH 09/21] set character encoding to utf-8 for parsing rendered-html --- src/Model/Item.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Model/Item.php b/src/Model/Item.php index 767c41acfd..e7bf057b4c 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -3143,9 +3143,9 @@ class Item */ private function cutAttachedImages($rendered_html) { - $doc = new DOMDocument(); + $doc = new DOMDocument('1.0', 'UTF-8'); libxml_use_internal_errors(true); - $doc->loadHTML($rendered_html); + $doc->loadHTML(mb_convert_encoding($rendered_html, 'html-entities', 'utf-8')); libxml_clear_errors(); $root = $doc->getElementsByTagName("p")[0]; From d270a98e862624f4d240c849d2a42c6bf57216e2 Mon Sep 17 00:00:00 2001 From: Marek Bachmann Date: Thu, 8 Dec 2022 20:39:51 +0100 Subject: [PATCH 10/21] Image grid logic --- src/Model/Item.php | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/src/Model/Item.php b/src/Model/Item.php index e7bf057b4c..1d3e3afbac 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -3193,9 +3193,9 @@ class Item { if ($item['has-media']) { if (count($attachments['visual']) > 1) { - $img_tags_fc = array(); - $img_tags_sc = array(); - $count = 0; + + $img_tags_landscape = array(); + $img_tags_portrait = array(); foreach ($attachments['visual'] as $attachment) { $src_url = Post\Media::getUrlForId($attachment['id']); $preview_url = Post\Media::getPreviewUrlForId($attachment['id'], ($attachment['width'] > $attachment['height']) ? Proxy::SIZE_MEDIUM : Proxy::SIZE_LARGE); @@ -3205,22 +3205,34 @@ class Item 'preview' => $preview_url, 'attachment' => $attachment, ]); - // @todo add some fany ai to divide images equally on both columns - if ($count % 2 == 0) { - $img_tags_fc[] = $img_tag; - } else { - $img_tags_sc[] = $img_tag; - } - ++$count; + ($attachment['width'] > $attachment['height']) ? ($img_tags_landscape[] = $img_tag) : ($img_tags_portrait[] = $img_tag); } - $img_grid = Renderer::replaceMacros(Renderer::getMarkupTemplate('content/image_grid.tpl'), [ + $landscapesCount = count($img_tags_landscape); + $portraitsCount = count($img_tags_portrait); + + // @todo add some fany ai to divide images equally on both columns + $img_tags_fc = array(); + $img_tags_sc = array(); + if ($landscapesCount == 0) { + // only portrait + for ($i = 0; $i < $portraitsCount; $i++) { + ($i % 2 == 0) ? ($img_tags_fc[] = $img_tags_portrait[$i]) : ($img_tags_sc[] = $img_tags_portrait[$i]); + } + } + if ($portraitsCount == 0) { + // ony landscapes + for ($i = 0; $i < $landscapesCount; $i++) { + ($i % 2 == 0) ? ($img_tags_fc[] = $img_tags_landscape[$i]) : ($img_tags_sc[] = $img_tags_landscape[$i]); + } + } + + return Renderer::replaceMacros(Renderer::getMarkupTemplate('content/image_grid.tpl'), [ 'columns' => [ 'fc' => $img_tags_fc, 'sc' => $img_tags_sc, ], ]); - return $img_grid; } } From d786f225eec56dbef7dacdc65af0b65ed4643163 Mon Sep 17 00:00:00 2001 From: Marek Bachmann Date: Sat, 10 Dec 2022 22:54:50 +0100 Subject: [PATCH 11/21] Added image grid generation to addVisualAttachments --- src/Model/Item.php | 73 +++++++++++++++++++++------ view/templates/content/image_grid.tpl | 8 +-- 2 files changed, 60 insertions(+), 21 deletions(-) diff --git a/src/Model/Item.php b/src/Model/Item.php index 5f7c64a13a..0c31d437ff 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -3130,18 +3130,56 @@ class Item $s = HTML::applyContentFilter($s, $filter_reasons); - if (count($attachments['visual']) > 1) { - // make imgae grid only for multiple images - $s = self::cutAttachedImages($s); - $grid = self::make_image_grid($item, $attachments); - $s .= $grid; - } +// if (count($attachments['visual']) > 1) { +// // make imgae grid only for multiple images +// $s = self::cutAttachedImages($s); +// $grid = self::make_image_grid($item, $attachments); +// $s .= $grid; +// } $hook_data = ['item' => $item, 'html' => $s]; Hook::callAll('prepare_body_final', $hook_data); return $hook_data['html']; } + /** + * @param array $images + * @return string + * @throws \Friendica\Network\HTTPException\ServiceUnavailableException + */ + public static function makeImageGrid(array $images): string + { + $img_tags_landscape = array(); + $img_tags_portrait = array(); + foreach ($images as $image) { + ($image['attachment']['width'] > $image['attachment']['height']) ? ($img_tags_landscape[] = $image) : ($img_tags_portrait[] = $image); + } + + // @todo add some fany ai to divide images equally on both columns + $img_tags_fc = array(); + $img_tags_sc = array(); + if (count($img_tags_landscape) == 0) { + // only portrait + for ($i = 0; $i < count($img_tags_portrait); $i++) { + ($i % 2 == 0) ? ($img_tags_fc[] = $img_tags_portrait[$i]) : ($img_tags_sc[] = $img_tags_portrait[$i]); + } + } + if (count($img_tags_portrait) == 0) { + // ony landscapes + for ($i = 0; $i < count($img_tags_landscape); $i++) { + ($i % 2 == 0) ? ($img_tags_fc[] = $img_tags_landscape[$i]) : ($img_tags_sc[] = $img_tags_landscape[$i]); + } + } + + $media = Renderer::replaceMacros(Renderer::getMarkupTemplate('content/image_grid.tpl'), [ + 'columns' => [ + 'fc' => $img_tags_fc, + 'sc' => $img_tags_sc, + ], + ]); + return $media; + } + /** * This function removes images at the very end of a post based on the assumption that this images are interpreted * as attachments @@ -3396,16 +3434,21 @@ class Item } } - foreach ($images as $image) { - $media = Renderer::replaceMacros(Renderer::getMarkupTemplate('content/image.tpl'), [ - '$image' => $image, + $media = ''; + if (count($images) > 1) { + $media = self::makeImageGrid($images); + } + elseif (count($images) == 1) { + $media = $media = Renderer::replaceMacros(Renderer::getMarkupTemplate('content/image.tpl'), [ + '$image' => $images[0], ]); - // On Diaspora posts the attached pictures are leading - if ($item['network'] == Protocol::DIASPORA) { - $leading .= $media; - } else { - $trailing .= $media; - } + } + + // On Diaspora posts the attached pictures are leading + if ($item['network'] == Protocol::DIASPORA) { + $leading .= $media; + } else { + $trailing .= $media; } if ($shared) { diff --git a/view/templates/content/image_grid.tpl b/view/templates/content/image_grid.tpl index 1a8648d189..8f5cbabcae 100644 --- a/view/templates/content/image_grid.tpl +++ b/view/templates/content/image_grid.tpl @@ -2,17 +2,13 @@
- {{foreach $columns.fc as $fc}} - {{foreach $fc as $img}} + {{foreach $columns.fc as $img}} {{include file="content/image.tpl" image=$img}} - {{/foreach}} {{/foreach}}
- {{foreach $columns.sc as $sc}} - {{foreach $sc as $img}} + {{foreach $columns.sc as $img}} {{include file="content/image.tpl" image=$img}} - {{/foreach}} {{/foreach}}
\ No newline at end of file From cc048bca38d5700556edd7b40dd1f139407febb0 Mon Sep 17 00:00:00 2001 From: Marek Bachmann Date: Sat, 10 Dec 2022 22:58:43 +0100 Subject: [PATCH 12/21] remove old code --- src/Model/Item.php | 110 --------------------------------------------- 1 file changed, 110 deletions(-) diff --git a/src/Model/Item.php b/src/Model/Item.php index 0c31d437ff..165e31d12d 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -3130,13 +3130,6 @@ class Item $s = HTML::applyContentFilter($s, $filter_reasons); -// if (count($attachments['visual']) > 1) { -// // make imgae grid only for multiple images -// $s = self::cutAttachedImages($s); -// $grid = self::make_image_grid($item, $attachments); -// $s .= $grid; -// } - $hook_data = ['item' => $item, 'html' => $s]; Hook::callAll('prepare_body_final', $hook_data); return $hook_data['html']; @@ -3180,109 +3173,6 @@ class Item return $media; } - /** - * This function removes images at the very end of a post based on the assumption that this images are interpreted - * as attachments - * @param array $rendered_html - * @return array - */ - private function cutAttachedImages($rendered_html) - { - $doc = new DOMDocument('1.0', 'UTF-8'); - libxml_use_internal_errors(true); - $doc->loadHTML(mb_convert_encoding($rendered_html, 'html-entities', 'utf-8')); - libxml_clear_errors(); - - $root = $doc->getElementsByTagName("p")[0]; - - $lastTextNode = null; - if ($root && $root->childNodes) { - foreach ($root->childNodes as $node) { - if ($node->nodeName == "#text" && strlen(trim($node->nodeValue)) > 0) { - $lastTextNode = $node; - } - } - } - - if ($lastTextNode == null) { - // no text at all, return nothing: - return ''; - } - - $toremove = array(); - if ($lastTextNode) { - $sibling = $lastTextNode->nextSibling; - while ($sibling) { - $toremove[] = array($sibling); - $sibling = $sibling->nextSibling; - } - foreach ($toremove as $remove) { - $root->removeChild($remove[0]); - } - $html = ''; - foreach ($root->childNodes as $node) { - $html .= $node->ownerDocument->saveHTML($node); - } - return $html; - } - - return $rendered_html; - } - - /** - * @param array $data - * @return string|void - * @throws \Friendica\Network\HTTPException\ServiceUnavailableException - */ - private function make_image_grid(array $item, array $attachments) - { - if ($item['has-media']) { - if (count($attachments['visual']) > 1) { - - $img_tags_landscape = array(); - $img_tags_portrait = array(); - foreach ($attachments['visual'] as $attachment) { - $src_url = Post\Media::getUrlForId($attachment['id']); - $preview_url = Post\Media::getPreviewUrlForId($attachment['id'], ($attachment['width'] > $attachment['height']) ? Proxy::SIZE_MEDIUM : Proxy::SIZE_LARGE); - $img_tag = array( - '$image' => [ - 'src' => $src_url, - 'preview' => $preview_url, - 'attachment' => $attachment, - ]); - ($attachment['width'] > $attachment['height']) ? ($img_tags_landscape[] = $img_tag) : ($img_tags_portrait[] = $img_tag); - } - - $landscapesCount = count($img_tags_landscape); - $portraitsCount = count($img_tags_portrait); - - // @todo add some fany ai to divide images equally on both columns - $img_tags_fc = array(); - $img_tags_sc = array(); - if ($landscapesCount == 0) { - // only portrait - for ($i = 0; $i < $portraitsCount; $i++) { - ($i % 2 == 0) ? ($img_tags_fc[] = $img_tags_portrait[$i]) : ($img_tags_sc[] = $img_tags_portrait[$i]); - } - } - if ($portraitsCount == 0) { - // ony landscapes - for ($i = 0; $i < $landscapesCount; $i++) { - ($i % 2 == 0) ? ($img_tags_fc[] = $img_tags_landscape[$i]) : ($img_tags_sc[] = $img_tags_landscape[$i]); - } - } - - return Renderer::replaceMacros(Renderer::getMarkupTemplate('content/image_grid.tpl'), [ - 'columns' => [ - 'fc' => $img_tags_fc, - 'sc' => $img_tags_sc, - ], - ]); - } - } - - } - /** * Check if the body contains a link * From 2eb170bdbd4f56dd75a8413f3d256ef346a79b74 Mon Sep 17 00:00:00 2001 From: Marek Bachmann Date: Sat, 10 Dec 2022 23:37:03 +0100 Subject: [PATCH 13/21] Added some logic for image grid column balancing --- src/Model/Item.php | 66 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 55 insertions(+), 11 deletions(-) diff --git a/src/Model/Item.php b/src/Model/Item.php index 165e31d12d..dedf905162 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -3151,26 +3151,70 @@ class Item // @todo add some fany ai to divide images equally on both columns $img_tags_fc = array(); $img_tags_sc = array(); - if (count($img_tags_landscape) == 0) { - // only portrait - for ($i = 0; $i < count($img_tags_portrait); $i++) { - ($i % 2 == 0) ? ($img_tags_fc[] = $img_tags_portrait[$i]) : ($img_tags_sc[] = $img_tags_portrait[$i]); + if (count($img_tags_landscape) == 0 || count($img_tags_portrait) == 0) { + if (count($img_tags_landscape) == 0) { + // only portrait + for ($i = 0; $i < count($img_tags_portrait); $i++) { + ($i % 2 == 0) ? ($img_tags_fc[] = $img_tags_portrait[$i]) : ($img_tags_sc[] = $img_tags_portrait[$i]); + } } - } - if (count($img_tags_portrait) == 0) { - // ony landscapes - for ($i = 0; $i < count($img_tags_landscape); $i++) { - ($i % 2 == 0) ? ($img_tags_fc[] = $img_tags_landscape[$i]) : ($img_tags_sc[] = $img_tags_landscape[$i]); + if (count($img_tags_portrait) == 0) { + // ony landscapes + for ($i = 0; $i < count($img_tags_landscape); $i++) { + ($i % 2 == 0) ? ($img_tags_fc[] = $img_tags_landscape[$i]) : ($img_tags_sc[] = $img_tags_landscape[$i]); + } } + } else { + // Mix of landscape and portrait images + $domformat = (count($img_tags_landscape) > count($img_tags_portrait)) ? 'landscape' : 'portrait'; + if ($domformat == 'landscape') { + // More landscapes than portraits + for ($l = 0; $l < count($img_tags_landscape); $l++) { + // use two landscapes for on portrait + if ((count($img_tags_landscape) > $l + 1) && (count($img_tags_portrait) > $l)) { + // we have one more landscape that can be used for the l-th portrait + $img_tags_fc[] = $img_tags_landscape[$l]; + $img_tags_fc[] = $img_tags_landscape[$l + 1]; + $img_tags_sc[] = $img_tags_portrait[$l]; + $l++; + } elseif (count($img_tags_portrait) <= $l) { + // no more portraits available but landscapes + $img_tags_fc[] = $img_tags_landscape[$l]; + } + } + } + if ($domformat == 'portrait') { + // More portraits than landscapes + $l = 0; + for ($p = 0; $p < count($img_tags_portrait); $p++) { + // use two landscapes for on portrait + if ((count($img_tags_landscape) > $l + 1)) { + // we have one more landscape that can be used for the p-th portrait + $img_tags_sc[] = $img_tags_landscape[$l]; + $img_tags_sc[] = $img_tags_landscape[$l + 1]; + $img_tags_fc[] = $img_tags_portrait[$p]; + // used to landscapes: + $l += 2; + } else { + // no more landscapes available + if ($p % 2 == 0 ) { + $img_tags_fc[] = $img_tags_landscape[$l]; + } else { + $img_tags_sc[] = $img_tags_landscape[$l]; + } + + } + } + } + } - $media = Renderer::replaceMacros(Renderer::getMarkupTemplate('content/image_grid.tpl'), [ + return Renderer::replaceMacros(Renderer::getMarkupTemplate('content/image_grid.tpl'), [ 'columns' => [ 'fc' => $img_tags_fc, 'sc' => $img_tags_sc, ], ]); - return $media; } /** From 236a4c819b9aec34971d86be0408547d29735117 Mon Sep 17 00:00:00 2001 From: Marek Bachmann Date: Sat, 10 Dec 2022 23:44:23 +0100 Subject: [PATCH 14/21] Removed not needed image_grid_column.tpl --- view/templates/content/image_grid_column.tpl | 1 - 1 file changed, 1 deletion(-) delete mode 100644 view/templates/content/image_grid_column.tpl diff --git a/view/templates/content/image_grid_column.tpl b/view/templates/content/image_grid_column.tpl deleted file mode 100644 index 6a58a4bda2..0000000000 --- a/view/templates/content/image_grid_column.tpl +++ /dev/null @@ -1 +0,0 @@ -
{{$images}}
\ No newline at end of file From 412ba461e7893ca6e717e989be946ed05deab7ca Mon Sep 17 00:00:00 2001 From: Marek Bachmann Date: Sat, 10 Dec 2022 23:45:42 +0100 Subject: [PATCH 15/21] Removed unused imports --- src/Model/Item.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Model/Item.php b/src/Model/Item.php index dedf905162..d134ca7d6e 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -21,8 +21,6 @@ namespace Friendica\Model; -use DOMDocument; -use DOMXPath; use Friendica\Content\Text\BBCode; use Friendica\Content\Text\HTML; use Friendica\Core\Hook; @@ -30,7 +28,6 @@ use Friendica\Core\Logger; use Friendica\Core\Protocol; use Friendica\Core\Renderer; use Friendica\Core\System; -use Friendica\Model\Tag; use Friendica\Core\Worker; use Friendica\Database\DBA; use Friendica\DI; From fe2c4f7c4b592e9991034a53378952bb52f8de68 Mon Sep 17 00:00:00 2001 From: Marek Bachmann Date: Sat, 10 Dec 2022 23:47:37 +0100 Subject: [PATCH 16/21] Restored $s = $hook_data['html']; call which was removed on error --- src/Model/Item.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Model/Item.php b/src/Model/Item.php index d134ca7d6e..921d35882d 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -3090,6 +3090,8 @@ class Item 'filter_reasons' => $filter_reasons ]; Hook::callAll('prepare_body', $hook_data); + $s = $hook_data['html']; + unset($hook_data); if (!$attach) { From 58cc270da8c9ea838c75886d279ffe422dc5e42b Mon Sep 17 00:00:00 2001 From: Marek Bachmann Date: Sun, 11 Dec 2022 17:09:28 +0100 Subject: [PATCH 17/21] fixed bug in portrait iteration shortened var names added some comments --- src/Model/Item.php | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/src/Model/Item.php b/src/Model/Item.php index 3b9477fc31..23cd4b7b0e 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -3148,37 +3148,38 @@ class Item } // @todo add some fany ai to divide images equally on both columns - $img_tags_fc = array(); - $img_tags_sc = array(); + // Image for first column (fc) and second column (sc) + $imgs_fc = array(); + $imgs_sc = array(); if (count($img_tags_landscape) == 0 || count($img_tags_portrait) == 0) { if (count($img_tags_landscape) == 0) { // only portrait for ($i = 0; $i < count($img_tags_portrait); $i++) { - ($i % 2 == 0) ? ($img_tags_fc[] = $img_tags_portrait[$i]) : ($img_tags_sc[] = $img_tags_portrait[$i]); + ($i % 2 == 0) ? ($imgs_fc[] = $img_tags_portrait[$i]) : ($imgs_sc[] = $img_tags_portrait[$i]); } } if (count($img_tags_portrait) == 0) { // ony landscapes for ($i = 0; $i < count($img_tags_landscape); $i++) { - ($i % 2 == 0) ? ($img_tags_fc[] = $img_tags_landscape[$i]) : ($img_tags_sc[] = $img_tags_landscape[$i]); + ($i % 2 == 0) ? ($imgs_fc[] = $img_tags_landscape[$i]) : ($imgs_sc[] = $img_tags_landscape[$i]); } } } else { - // Mix of landscape and portrait images - $domformat = (count($img_tags_landscape) > count($img_tags_portrait)) ? 'landscape' : 'portrait'; + // Mix of landscape and portrait images. Which format is dominating (domformat)? + $domformat = (count($img_tags_landscape) >= count($img_tags_portrait)) ? 'landscape' : 'portrait'; if ($domformat == 'landscape') { - // More landscapes than portraits + // More or equal landscapes than portraits for ($l = 0; $l < count($img_tags_landscape); $l++) { // use two landscapes for on portrait if ((count($img_tags_landscape) > $l + 1) && (count($img_tags_portrait) > $l)) { // we have one more landscape that can be used for the l-th portrait - $img_tags_fc[] = $img_tags_landscape[$l]; - $img_tags_fc[] = $img_tags_landscape[$l + 1]; - $img_tags_sc[] = $img_tags_portrait[$l]; + $imgs_fc[] = $img_tags_landscape[$l]; + $imgs_fc[] = $img_tags_landscape[$l + 1]; + $imgs_sc[] = $img_tags_portrait[$l]; $l++; } elseif (count($img_tags_portrait) <= $l) { // no more portraits available but landscapes - $img_tags_fc[] = $img_tags_landscape[$l]; + $imgs_fc[] = $img_tags_landscape[$l]; } } } @@ -3188,18 +3189,18 @@ class Item for ($p = 0; $p < count($img_tags_portrait); $p++) { // use two landscapes for on portrait if ((count($img_tags_landscape) > $l + 1)) { - // we have one more landscape that can be used for the p-th portrait - $img_tags_sc[] = $img_tags_landscape[$l]; - $img_tags_sc[] = $img_tags_landscape[$l + 1]; - $img_tags_fc[] = $img_tags_portrait[$p]; + // we have at least one more landscape that can be used for the p-th portrait + $imgs_sc[] = $img_tags_landscape[$l]; + $imgs_sc[] = $img_tags_landscape[$l + 1]; + $imgs_fc[] = $img_tags_portrait[$p]; // used to landscapes: $l += 2; } else { // no more landscapes available if ($p % 2 == 0 ) { - $img_tags_fc[] = $img_tags_landscape[$l]; + $imgs_fc[] = $img_tags_portrait[$p]; } else { - $img_tags_sc[] = $img_tags_landscape[$l]; + $imgs_sc[] = $img_tags_portrait[$p]; } } @@ -3210,8 +3211,8 @@ class Item return Renderer::replaceMacros(Renderer::getMarkupTemplate('content/image_grid.tpl'), [ 'columns' => [ - 'fc' => $img_tags_fc, - 'sc' => $img_tags_sc, + 'fc' => $imgs_fc, + 'sc' => $imgs_sc, ], ]); } From eb1cfd09a9eeefc2e31d1d4df0ecb8405b5a8d80 Mon Sep 17 00:00:00 2001 From: Marek Bachmann Date: Sun, 11 Dec 2022 18:25:19 +0100 Subject: [PATCH 18/21] added more sophisticated column balancing logic for portraits and landscapes --- src/Model/Item.php | 143 ++++++++++++++++++++++++++++----------------- 1 file changed, 91 insertions(+), 52 deletions(-) diff --git a/src/Model/Item.php b/src/Model/Item.php index 23cd4b7b0e..68fb01dd1c 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -3141,78 +3141,117 @@ class Item */ public static function makeImageGrid(array $images): string { - $img_tags_landscape = array(); - $img_tags_portrait = array(); + $landscapeimages = array(); + $portraitimages = array(); + foreach ($images as $image) { - ($image['attachment']['width'] > $image['attachment']['height']) ? ($img_tags_landscape[] = $image) : ($img_tags_portrait[] = $image); + ($image['attachment']['width'] > $image['attachment']['height']) ? ($landscapeimages[] = $image) : ($portraitimages[] = $image); } - // @todo add some fany ai to divide images equally on both columns // Image for first column (fc) and second column (sc) - $imgs_fc = array(); - $imgs_sc = array(); - if (count($img_tags_landscape) == 0 || count($img_tags_portrait) == 0) { - if (count($img_tags_landscape) == 0) { + $images_fc = array(); + $images_sc = array(); + $lcount = count($landscapeimages); + $pcount = count($portraitimages); + if ($lcount == 0 || $pcount == 0) { + if ($lcount == 0) { // only portrait - for ($i = 0; $i < count($img_tags_portrait); $i++) { - ($i % 2 == 0) ? ($imgs_fc[] = $img_tags_portrait[$i]) : ($imgs_sc[] = $img_tags_portrait[$i]); + for ($i = 0; $i < $pcount; $i++) { + ($i % 2 == 0) ? ($images_fc[] = $portraitimages[$i]) : ($images_sc[] = $portraitimages[$i]); } } - if (count($img_tags_portrait) == 0) { + if ($pcount == 0) { // ony landscapes - for ($i = 0; $i < count($img_tags_landscape); $i++) { - ($i % 2 == 0) ? ($imgs_fc[] = $img_tags_landscape[$i]) : ($imgs_sc[] = $img_tags_landscape[$i]); + for ($i = 0; $i < $lcount; $i++) { + ($i % 2 == 0) ? ($images_fc[] = $landscapeimages[$i]) : ($images_sc[] = $landscapeimages[$i]); } } } else { - // Mix of landscape and portrait images. Which format is dominating (domformat)? - $domformat = (count($img_tags_landscape) >= count($img_tags_portrait)) ? 'landscape' : 'portrait'; - if ($domformat == 'landscape') { - // More or equal landscapes than portraits - for ($l = 0; $l < count($img_tags_landscape); $l++) { - // use two landscapes for on portrait - if ((count($img_tags_landscape) > $l + 1) && (count($img_tags_portrait) > $l)) { - // we have one more landscape that can be used for the l-th portrait - $imgs_fc[] = $img_tags_landscape[$l]; - $imgs_fc[] = $img_tags_landscape[$l + 1]; - $imgs_sc[] = $img_tags_portrait[$l]; - $l++; - } elseif (count($img_tags_portrait) <= $l) { - // no more portraits available but landscapes - $imgs_fc[] = $img_tags_landscape[$l]; - } - } - } - if ($domformat == 'portrait') { - // More portraits than landscapes - $l = 0; - for ($p = 0; $p < count($img_tags_portrait); $p++) { - // use two landscapes for on portrait - if ((count($img_tags_landscape) > $l + 1)) { - // we have at least one more landscape that can be used for the p-th portrait - $imgs_sc[] = $img_tags_landscape[$l]; - $imgs_sc[] = $img_tags_landscape[$l + 1]; - $imgs_fc[] = $img_tags_portrait[$p]; - // used to landscapes: - $l += 2; + // Mix of landscape and portrait images. + if ($lcount == $pcount) { + // equal amount of landscapes and portraits + for ($l = 0; $l < $lcount; $l++) { + if ($l % 2 == 0) { + $images_fc[] = $landscapeimages[$l]; + $images_fc[] = $portraitimages[$l]; } else { - // no more landscapes available - if ($p % 2 == 0 ) { - $imgs_fc[] = $img_tags_portrait[$p]; - } else { - $imgs_sc[] = $img_tags_portrait[$p]; - } - + $images_sc[] = $portraitimages[$l]; + $images_sc[] = $landscapeimages[$l]; } } } + if ($lcount > $pcount) { + // More landscapes than portraits + $p = 0; + $l = 0; + while ($l < $lcount) { + if (($lcount > $l + 1) && ($pcount > $l)) { + // we have one more landscape that can be used for the l-th portrait + $images_fc[] = $landscapeimages[$l++]; + } + $images_fc[] = $landscapeimages[$l++]; + if ($pcount > $p) { + $images_sc[] = $portraitimages[$p++]; + } + } + } + if ($lcount < $pcount) { + // More portraits than landscapes + if ($lcount % 2 == 0 && $pcount % 2 == 0) { + /* + * even number of landscapes and portraits, but fewer landscapes than portraits. Iterate to the end + * of landscapes array + */ + $i = 0; + while ($i < $lcount) { + if ($i % 2 == 0) { + $images_fc[] = $landscapeimages[$i]; + $images_fc[] = $portraitimages[$i]; + } else { + $images_sc[] = $portraitimages[$i]; + $images_sc[] = $landscapeimages[$i]; + } + $i++; + } + // Rest portraits + while ($i < $pcount) { + if ($i % 2 == 0) { + $images_fc[] = $portraitimages[$i]; + } else { + $images_sc[] = $portraitimages[$i]; + } + $i++; + } + + } + if ($lcount % 2 != 0 && $pcount % 2 == 0) { + // uneven landscapes count even portraits count. + for ($p = 0; $p < $pcount; $p++) { + // --> First all portraits until + if ($p % 2 == 0) { + $images_fc[] = $portraitimages[$p]; + } else { + $images_sc[] = $portraitimages[$p]; + } + } + // and now the (uneven) landscapes + for ($l = 0; $l < $lcount; $l++) { + // --> First all portraits until + if ($l % 2 == 0) { + $images_fc[] = $landscapeimages[$l]; + } else { + $images_sc[] = $landscapeimages[$l]; + } + } + } + } } return Renderer::replaceMacros(Renderer::getMarkupTemplate('content/image_grid.tpl'), [ 'columns' => [ - 'fc' => $imgs_fc, - 'sc' => $imgs_sc, + 'fc' => $images_fc, + 'sc' => $images_sc, ], ]); } From bc70bc23a4788e01d2deb8e1859d8c6c3eb3f85e Mon Sep 17 00:00:00 2001 From: MarekBenjamin <117765478+MarekBenjamin@users.noreply.github.com> Date: Mon, 12 Dec 2022 21:45:57 +0100 Subject: [PATCH 19/21] Update image_grid.css --- view/theme/frio/css/image_grid.css | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/view/theme/frio/css/image_grid.css b/view/theme/frio/css/image_grid.css index dd29dd201d..5a39f6a4c9 100644 --- a/view/theme/frio/css/image_grid.css +++ b/view/theme/frio/css/image_grid.css @@ -24,15 +24,6 @@ width: 100%; } -/*!* Responsive layout - makes a two column-layout instead of four columns *!*/ -/*@media screen and (max-width: 50px) {*/ -/* .column {*/ -/* -ms-flex: 50%;*/ -/* flex: 50%;*/ -/* max-width: 50%;*/ -/* }*/ -/*}*/ - /* Responsive layout - makes the two columns stack on top of each other instead of next to each other */ @media screen and (max-width: 150px) { .column { @@ -40,4 +31,4 @@ flex: 100%; max-width: 100%; } -} \ No newline at end of file +} From 86b9cef273372ddbc522ab5df6bbc9157063a1f5 Mon Sep 17 00:00:00 2001 From: MarekBenjamin <117765478+MarekBenjamin@users.noreply.github.com> Date: Mon, 12 Dec 2022 21:56:42 +0100 Subject: [PATCH 20/21] Update image_grid.css Removed fall back to one column --- view/theme/frio/css/image_grid.css | 9 --------- 1 file changed, 9 deletions(-) diff --git a/view/theme/frio/css/image_grid.css b/view/theme/frio/css/image_grid.css index 5a39f6a4c9..2ecbcafea8 100644 --- a/view/theme/frio/css/image_grid.css +++ b/view/theme/frio/css/image_grid.css @@ -23,12 +23,3 @@ vertical-align: middle; width: 100%; } - -/* Responsive layout - makes the two columns stack on top of each other instead of next to each other */ -@media screen and (max-width: 150px) { - .column { - -ms-flex: 100%; - flex: 100%; - max-width: 100%; - } -} From 8690b311a873dd35c3c66d18a8fa677b5f01eea0 Mon Sep 17 00:00:00 2001 From: Marek Bachmann Date: Mon, 12 Dec 2022 22:57:16 +0100 Subject: [PATCH 21/21] Moved image_grid.css content to global.css and use unique class names for imagegrid style classes, removed id for div --- view/global.css | 30 +++++++++++++++++++++++++++ view/templates/content/image_grid.tpl | 14 ++++++------- view/theme/frio/css/image_grid.css | 25 ---------------------- 3 files changed, 36 insertions(+), 33 deletions(-) delete mode 100644 view/theme/frio/css/image_grid.css diff --git a/view/global.css b/view/global.css index 4101f0fe0a..81a1ba2095 100644 --- a/view/global.css +++ b/view/global.css @@ -680,3 +680,33 @@ span.required { audio { width: 100%; } + +/** + * Image grid settings START + **/ +.imagegrid-row { + display: -ms-flexbox; /* IE10 */ + display: flex; + -ms-flex-wrap: wrap; /* IE10 */ + flex-wrap: wrap; + padding: 0 4px; + box-sizing: border-box; +} + +/* Create four equal columns that sits next to each other */ +.imagegrid-column { + -ms-flex: 50%; /* IE10 */ + flex: 50%; + max-width: 50%; + padding: 0 4px; + box-sizing: border-box; +} + +.imagegrid-column img { + margin-top: 8px; + vertical-align: middle; + width: 100%; +} +/** + * Image grid settings END + **/ \ No newline at end of file diff --git a/view/templates/content/image_grid.tpl b/view/templates/content/image_grid.tpl index 8f5cbabcae..95e49ee3e1 100644 --- a/view/templates/content/image_grid.tpl +++ b/view/templates/content/image_grid.tpl @@ -1,13 +1,11 @@ - - -
-
- {{foreach $columns.fc as $img}} - {{include file="content/image.tpl" image=$img}} +
+
+ {{foreach $columns.fc as $img}} + {{include file="content/image.tpl" image=$img}} {{/foreach}}
-
- {{foreach $columns.sc as $img}} +
+ {{foreach $columns.sc as $img}} {{include file="content/image.tpl" image=$img}} {{/foreach}}
diff --git a/view/theme/frio/css/image_grid.css b/view/theme/frio/css/image_grid.css deleted file mode 100644 index 2ecbcafea8..0000000000 --- a/view/theme/frio/css/image_grid.css +++ /dev/null @@ -1,25 +0,0 @@ -* { - box-sizing: border-box; -} - -.row { - display: -ms-flexbox; /* IE10 */ - display: flex; - -ms-flex-wrap: wrap; /* IE10 */ - flex-wrap: wrap; - padding: 0 4px; -} - -/* Create four equal columns that sits next to each other */ -.column { - -ms-flex: 50%; /* IE10 */ - flex: 50%; - max-width: 50%; - padding: 0 4px; -} - -.column img { - margin-top: 8px; - vertical-align: middle; - width: 100%; -}