diff --git a/.distignore b/.distignore index 156eb54bd..a1a9e7aef 100644 --- a/.distignore +++ b/.distignore @@ -7,6 +7,8 @@ .github .gitignore .php_cs +.prettierignore +.prettierrc.js .svnignore .travis.yml .wordpress-org diff --git a/.editorconfig b/.editorconfig index a541e47e7..118ef466c 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,8 +1,10 @@ # This file is for unifying the coding style for different editors and IDEs # editorconfig.org - +# # WordPress Coding Standards # https://make.wordpress.org/core/handbook/coding-standards/ +# +# Be sure to keep this file in sync with the .prettierrc.js file. root = true diff --git a/.github/changelog/1674-from-description b/.github/changelog/1674-from-description new file mode 100644 index 000000000..86cefa97c --- /dev/null +++ b/.github/changelog/1674-from-description @@ -0,0 +1,5 @@ +Significance: minor +Type: added + +Post Preview now supports RTL languages. +Embed and wp-admin styles are optimized for RTL languages. diff --git a/.github/changelog/1900-from-description b/.github/changelog/1900-from-description new file mode 100644 index 000000000..cd3d6ad75 --- /dev/null +++ b/.github/changelog/1900-from-description @@ -0,0 +1,4 @@ +Significance: minor +Type: changed + +Set older unfederated posts to local visibility by default. diff --git a/.github/changelog/1907-from-description b/.github/changelog/1907-from-description new file mode 100644 index 000000000..66fb05b55 --- /dev/null +++ b/.github/changelog/1907-from-description @@ -0,0 +1,4 @@ +Significance: minor +Type: changed + +Minor performance improvement when querying posts of various types, by avoiding double queries. diff --git a/.github/changelog/1909-from-description b/.github/changelog/1909-from-description new file mode 100644 index 000000000..8ecf1deea --- /dev/null +++ b/.github/changelog/1909-from-description @@ -0,0 +1,4 @@ +Significance: minor +Type: changed + +The following tables now more closely match the appearance of other WordPress tables and can be filtered by status. diff --git a/.github/changelog/1913-from-description b/.github/changelog/1913-from-description new file mode 100644 index 000000000..09417ebb4 --- /dev/null +++ b/.github/changelog/1913-from-description @@ -0,0 +1,4 @@ +Significance: minor +Type: changed + +Follower tables now look closer to what other tables in WordPress look like. diff --git a/.github/changelog/1916-from-description b/.github/changelog/1916-from-description new file mode 100644 index 000000000..44feb81ed --- /dev/null +++ b/.github/changelog/1916-from-description @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +The `following` endpoint now returns the actual list of users being followed. diff --git a/.github/changelog/1918-from-description b/.github/changelog/1918-from-description new file mode 100644 index 000000000..443e75fe8 --- /dev/null +++ b/.github/changelog/1918-from-description @@ -0,0 +1,4 @@ +Significance: patch +Type: fixed + +Fixed an issue where the number of followers shown didn’t always match the actual follower list. diff --git a/.github/changelog/1919-from-description b/.github/changelog/1919-from-description new file mode 100644 index 000000000..b73dcd368 --- /dev/null +++ b/.github/changelog/1919-from-description @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Added initial support for Fediverse Starter Kits, allowing users to follow recommended accounts from a predefined list. diff --git a/.github/changelog/1920-from-description b/.github/changelog/1920-from-description new file mode 100644 index 000000000..2fbcf9560 --- /dev/null +++ b/.github/changelog/1920-from-description @@ -0,0 +1,4 @@ +Significance: patch +Type: fixed + +Ensure that the Actor-ID is always a URL. diff --git a/.github/changelog/1922-from-description b/.github/changelog/1922-from-description new file mode 100644 index 000000000..d83faba44 --- /dev/null +++ b/.github/changelog/1922-from-description @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +The featured tags endpoint is now available again for all profiles, showing the most frequently used tags by each user. diff --git a/.github/changelog/1925-from-description b/.github/changelog/1925-from-description new file mode 100644 index 000000000..ad58de22f --- /dev/null +++ b/.github/changelog/1925-from-description @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Followers and Following list tables now support Columns and Pagination screen options. diff --git a/.github/changelog/1928-from-description b/.github/changelog/1928-from-description new file mode 100644 index 000000000..766faf846 --- /dev/null +++ b/.github/changelog/1928-from-description @@ -0,0 +1,4 @@ +Significance: patch +Type: fixed + +Removed follower dates to avoid confusion, as they may not have accurately reflected the actual follow time. diff --git a/.github/changelog/1930-from-description b/.github/changelog/1930-from-description new file mode 100644 index 000000000..c1a0171df --- /dev/null +++ b/.github/changelog/1930-from-description @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Added a first version of the Follow form, allowing users to follow other Actors by username or profile link. diff --git a/.github/changelog/1931-from-description b/.github/changelog/1931-from-description new file mode 100644 index 000000000..ff7134145 --- /dev/null +++ b/.github/changelog/1931-from-description @@ -0,0 +1,4 @@ +Significance: patch +Type: fixed + +Fixed a bug in how follow requests were accepted to ensure they work correctly. diff --git a/.github/changelog/1932-from-description b/.github/changelog/1932-from-description new file mode 100644 index 000000000..64240a011 --- /dev/null +++ b/.github/changelog/1932-from-description @@ -0,0 +1,4 @@ +Significance: patch +Type: fixed + +Fixed missing avatar class so that CSS styles are correctly applied to ActivityPub avatars on the Dashboard. diff --git a/.github/changelog/1942-from-description b/.github/changelog/1942-from-description new file mode 100644 index 000000000..748b09a4e --- /dev/null +++ b/.github/changelog/1942-from-description @@ -0,0 +1,4 @@ +Significance: minor +Type: changed + +Step counts for the Welcome checklist now only take into account steps that are added in the Welcome class. diff --git a/.github/changelog/1943-from-description b/.github/changelog/1943-from-description new file mode 100644 index 000000000..b16182f84 --- /dev/null +++ b/.github/changelog/1943-from-description @@ -0,0 +1,4 @@ +Significance: patch +Type: fixed + +Prevent WordPress from loading all admin notices twice on ActivityPub settings pages. diff --git a/.github/changelog/1946-from-description b/.github/changelog/1946-from-description new file mode 100644 index 000000000..0fd93ceab --- /dev/null +++ b/.github/changelog/1946-from-description @@ -0,0 +1,4 @@ +Significance: minor +Type: changed + +Table actions are now faster by using the Custom Post Type ID instead of the remote user URI, thanks to the unified Actor Model. diff --git a/.github/changelog/1959-from-description b/.github/changelog/1959-from-description new file mode 100644 index 000000000..7017f8d21 --- /dev/null +++ b/.github/changelog/1959-from-description @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Ensure that all schedulers are registered during every plugin update. diff --git a/.github/changelog/1973-from-description b/.github/changelog/1973-from-description new file mode 100644 index 000000000..2ab5ec1a2 --- /dev/null +++ b/.github/changelog/1973-from-description @@ -0,0 +1,4 @@ +Significance: patch +Type: fixed + +Fixed a PHP error that prevented the Follower overview from loading. diff --git a/.github/changelog/1974-from-description b/.github/changelog/1974-from-description new file mode 100644 index 000000000..dc933277d --- /dev/null +++ b/.github/changelog/1974-from-description @@ -0,0 +1,4 @@ +Significance: minor +Type: changed + +Improved Account-Aliases handling by internally normalizing input formats. diff --git a/.github/workflows/gardening.yml b/.github/workflows/gardening.yml index f92e9d966..a46154cac 100644 --- a/.github/workflows/gardening.yml +++ b/.github/workflows/gardening.yml @@ -57,8 +57,10 @@ jobs: {"path": "integration", "label": "[Focus] Compatibility"}, {"path": "includes/class-mailer.php", "label": "[Feature] Notifications"}, {"path": "includes/class-blocks.php", "label": "[Focus] Editor"}, + {"path": "includes/class-cli.php", "label": "[Feature] CLI"}, {"path": "includes/collection", "label": "[Feature] Collections"}, {"path": "includes/rest", "label": "[Feature] REST API"}, + {"path": "includes/signature", "label": "[Feature] Signature"}, {"path": "includes/wp-admin", "label": "[Feature] WP Admin"}, {"path": "includes/wp-admin/import", "label": "[Feature] Import"}, {"path": "includes/wp-admin/class-health-check.php", "label": "[Feature] Health Check"} diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index 0759414bb..8d7918afd 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -20,7 +20,7 @@ jobs: php-versions: ['7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] include: - wp-version: latest - - wp-version: '6.4' + - wp-version: '6.5' php-versions: '7.2' - wp-version: trunk php-versions: '8.4' diff --git a/.gitignore b/.gitignore index 4e629a881..5e402bc67 100644 --- a/.gitignore +++ b/.gitignore @@ -1,18 +1,17 @@ -_site -.sass-cache -.jekyll-cache -.jekyll-metadata +/build/**/*.map /coverage/ /node_modules/ /vendor/ -package-lock.json -composer.lock -.DS_Store -.cursor +_site .idea/ +.cursor +.DS_Store .php_cs.cache .phpunit.result.cache +.sass-cache .vscode/settings.json .windsurf .windsurfrules .wp-env.override.json +composer.lock +package-lock.json diff --git a/.prettierignore b/.prettierignore index 6e56f88ee..fbac9e95d 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,13 +1,5 @@ build +coverage node_modules tests vendor - -# Temporary ignores while breaking out each component. -assets -src/follow-me -src/followers -src/reactions -src/remote-reply -src/reply -src/reply-intent diff --git a/.prettierrc.js b/.prettierrc.js index 405450da3..62cd2bd25 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -5,12 +5,6 @@ module.exports = { printWidth: 120, overrides: [ - { - files: '*.json', - options: { - useTabs: false, - }, - }, { files: '*.yml', options: { @@ -18,11 +12,5 @@ module.exports = { tabWidth: 2, }, }, - { - files: '*.md', - options: { - trimTrailingWhitespace: false, // Not a valid Prettier option, handled by editorconfig only - }, - }, ], }; diff --git a/.wordpress-org/blueprints/blueprint.json b/.wordpress-org/blueprints/blueprint.json index c337fa701..5d4e444af 100644 --- a/.wordpress-org/blueprints/blueprint.json +++ b/.wordpress-org/blueprints/blueprint.json @@ -1,43 +1,43 @@ { - "landingPage": "/wp-admin/options-general.php?page=activitypub", - "steps": [ - { - "step": "setSiteOptions", - "options": { - "permalink_structure": "/%postname%/" - } - }, - { - "step": "installPlugin", - "pluginZipFile": { - "resource": "wordpress.org/plugins", - "slug": "activitypub" - }, - "options": { - "activate": true - } - }, - { - "step": "login", - "username": "admin", - "password": "password" - }, - { - "step": "mkdir", - "path": "wordpress/wp-content/mu-plugins" - }, - { - "step": "writeFile", - "path": "wordpress/wp-content/mu-plugins/show-admin-notice-2.php", - "data": "

' . esc_html( 'Welcome and have fun 👋' ) . '

';\n}\n);\nadd_action('wp_ajax_dismiss_custom-admin-notice-2', function() {\ncheck_ajax_referer('custom-admin-notice-2', 'nonce');\n$user_id = get_current_user_id();\nif ( $user_id ) {\nupdate_user_option($user_id, 'dismissed_expose_blueprint_notice-2', 1, false);\nwp_send_json_success();\n} else {\nwp_send_json_error('User not found');\n}\n} );\nadd_action('admin_footer', function() {\n?>\n\n

' . esc_html( 'Welcome and have fun 👋' ) . '

';\n}\n);\nadd_action('wp_ajax_dismiss_custom-admin-notice-2', function() {\ncheck_ajax_referer('custom-admin-notice-2', 'nonce');\n$user_id = get_current_user_id();\nif ( $user_id ) {\nupdate_user_option($user_id, 'dismissed_expose_blueprint_notice-2', 1, false);\nwp_send_json_success();\n} else {\nwp_send_json_error('User not found');\n}\n} );\nadd_action('admin_footer', function() {\n?>\n\n + + + + + + + + diff --git a/.wp-env.json b/.wp-env.json index f9016f865..352e4780e 100644 --- a/.wp-env.json +++ b/.wp-env.json @@ -1,19 +1,19 @@ { - "core": null, - "plugins": [ "." ], - "env": { - "tests": { - "config": { - "WP_TESTS_DOMAIN": "example.org", - "WP_SITEURL": "http://example.org", - "WP_HOME": "http://example.org" - }, - "port": 80, - "mappings": { - "wp-content/plugins/activitypub": ".", - "wp-content/plugins/activitypub/tests": "./tests", - "wp-content/plugins/activitypub/coverage": "./coverage" - } - } - } + "core": null, + "plugins": [ "." ], + "env": { + "tests": { + "config": { + "WP_TESTS_DOMAIN": "example.org", + "WP_SITEURL": "http://example.org", + "WP_HOME": "http://example.org" + }, + "port": 80, + "mappings": { + "wp-content/plugins/activitypub": ".", + "wp-content/plugins/activitypub/tests": "./tests", + "wp-content/plugins/activitypub/coverage": "./coverage" + } + } + } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 3475bf5bc..a23ec8f6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,115 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [7.0.1] - 2025-07-10 +### Fixed +- When deleting interactions for cleaned up actors, we use the actor's URL again to retrieve their information instead of our internal ID. [#1915] + +## [7.0.0] - 2025-07-09 +### Added +- Added basic support for handling remote rejections of follow requests. [#1865] +- Added basic support for RFC-9421 style signatures for incoming activities. [#1849] +- Added initial Following support for Actors, hidden for now until plugins add support. [#1866] +- Added missing "Advanced Settings" details to Site Health debug information. [#1846] +- Added option to auto-approve reactions like likes and reposts. [#1847] +- Added support for namespaced attributes and the dcterms:subject field (FEP-b2b8), as a first step toward phasing out summary-based content warnings. [#1893] +- Added support for the WP Rest Cache plugin to help with caching REST API responses. [#1630] +- Documented support for FEP-844e. [#1868] +- Optional support for RFC-9421 style signatures for outgoing activities, including retry with Draft-Cavage-style signature. [#1858] +- Reactions block now supports customizing colors, borders, box-shadows, and typography. [#1826] +- Support for sending follow requests to remote actors is now in place, including outbox delivery and status updates—UI integration will follow later. [#1839] + +### Changed +- Comment feeds now show only comments by default, with a new `type` filter (e.g., `like`, `all`) to customize which reactions appear. [#1877] +- Consistent naming of Blog user in Block settings. [#1862] +- hs2019 signatures for incoming REST API requests now have their algorithm determined based on their public key. [#1848] +- Likes, comments, and reposts from the Fediverse now require either a name or `preferredUsername` to be set when the Discussion option `require_name_email` is set to true. It falls back to "Anonymous", if not. [#1811] +- Management of public/private keys for Actors now lives in the Actors collection, in preparation for Signature improvements down the line. [#1832] +- Notification emails for new reactions received from the Fediverse now link to the moderation page instead of the edit page, preventing errors and making comment management smoother. [#1887] +- Plugins now have full control over which Settings tabs are shown in Settings > Activitypub. [#1806] +- Reworked follower structure to simplify handling and enable reuse for following mechanism. [#1759] +- Screen options in the Activitypub settings page are now filterable. [#1802] +- Setting the blog identifier to empty will no longer trigger an error message about it being the same as an existing user name. [#1805] +- Step completion tracking in the Welcome tab now even works when the number of steps gets reduced. [#1809] +- The image attachment setting is no longer saved to the database if it matches the default value. [#1821] +- The welcome page now links to the correct profile when Blog Only mode was selected in the profile mode step. [#1807] +- Unified retrieval of comment avatars and re-used core filters to give access to third-part plugins. [#1812] + +### Fixed +- Allow interaction redirect URLs that contain an ampersand. [#1819] +- Comments received from the Fediverse no longer show an Edit link in the comment list, despite not being editable. [#1895] +- Fixed an issue where links to remote likes and boosts could open raw JSON instead of a proper page. [#1857] +- Fixed a potential error when getting an Activitypub ID based on a user ID. [#1889] +- HTTP signatures using the hs2019 algorithm now get accepted without error. [#1814] +- Improved compatibility with older follower data. [#1841] +- Inbox requests that are missing an `algorithm` parameter in their signature no longer create a PHP warning. [#1803] +- Interaction attempts that pass a webfinger ID instead of a URL will work again. [#1834] +- Names containing HTML entities now get displayed correctly in the Reactions block's list of users. [#1810] +- Prevent storage of empty or default post meta values. [#1829] +- The amount of avatars shown in the Reactions block no longer depends on the amount of likes, but is comment type agnostic. [#1835] +- The command-line interface extension, accidentally removed in a recent cleanup, has been restored. [#1878] +- The image attachment setting now correctly respects a value of 0, instead of falling back to the default. [#1822] +- The Welcome screen now loads with proper styling when shown as a fallback. [#1820] +- Using categories as hashtags has been removed to prevent conflicts with tags of the same name. [#1873] +- When verifying signatures on incoming requests, the digest header now gets checked as expected. [#1837] + +## [6.0.2] - 2025-06-11 +### Changed +- Reactions button color is now a little more theme agnostic. [#1795] + +### Fixed +- "Account Aliases" setting in user profiles get saved correctly again and no longer return empty. [#1798] +- Blocks updated in 6.0.0 are back to not showing up in feeds and federated posts. [#1794] +- Webfinger data from Pleroma instances no longer creates unexpected mention markup. [#1799] + +## [6.0.1] - 2025-06-09 +### Fixed +- Added fallback for follower list during migration to new database schema. [#1781] +- Avoids the button block breaking for users that don't have the `unfiltered_html` capability. + Blog users now get their correct post count displayed in the Editor and the front-end. [#1777] +- Improved follower migration: scheduler now more reliable and won't stop too early. [#1778] +- Update the Stream Connector integration to align with the new database schema. [#1787] + +## [6.0.0] - 2025-06-05 +### Added +- Enhanced markup of the "follow me" block, for a better Webmention and IndieWeb support. [#1771] +- The actor of the replied-to post is now included in cc or to based on the post's visibility. [#1711] + +### Changed +- "Reply on the Fediverse" now uses the Interactivity API for display on the frontend. [#1721] +- Bumped minimum required WordPress version to 6.5. [#1703] +- Default avatar and error handling for the reactions popover list. [#1719] +- Ensured that publishing a new blog post always sends a Create to the Fediverse. [#1713] +- Followers block has an updated design, new block variations, and uses the Interactivity API for display on the frontend. [#1747] +- Follow Me and Followers blocks can now list any user that is Activitypub-enabled, even if they have the Subscriber role. [#1754] +- Likes and Reposts for comments to a post are no longer attributed to the post itself. [#1735] +- New system to manage followers and followings more consistently using a unified actor type. [#1726] +- Re-enabled HTML support in excerpts and summaries to properly display hashtags and @-replies, now that Mastodon supports it. [#1731] +- Refactored to use CSS for effects instead of JavaScript, simplifying the code. [#1718] +- Refine the plugin’s handling and storage of remote actor data. [#1751] +- The Follow Me block now uses the latest Block Editor technology for display on the frontend. [#1691] +- The Reactions block now uses the latest Block Editor technology for display on the frontend. [#1722] + +### Removed +- Cleaned up the codebase and removed deprecated functions. [#1723] + +### Fixed +- Added forward compatibility for Editor Controls, fixing deprecated warnings in the Editor. [#1748] +- Avoid type mismatch when updating `activitypub_content_warning` meta values. [#1766] +- Default number of attachments now works correctly in block editor. [#1765] +- Fixed a bug in Site Health that caused a PHP warning and missing details for the WebFinger check. [#1733] +- Fixes a bug in WordPress 6.5 where the plugin settings in the Editor would fail to render, due to a backwards compatibility break. [#1760] +- Improved automated setup process for the Surge caching plugin. [#1724] +- Improved excerpt handling by removing shortcodes from summaries. [#1730] + +## [5.9.2] - 2025-05-16 +### Fixed +- Titles added through a Heading block in the Reactions block now stay properly hidden when there are no reactions. [#1709] + +## [5.9.1] - 2025-05-15 +### Fixed +- Fixed a bug where Reaction blocks without modified titles did not get displayed correctly. [#1705] + ## [5.9.0] - 2025-05-14 ### Added - ActivityPub embeds now support audios, videos, and up to 4 images. [#1645] @@ -37,7 +146,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Use `Audio` and `Video` type for Attachments, instead of the very generic `Document` type. [#1486] ### Deprecated -- Deprecated `rest_activitypub_outbox_query` filter in favor of `activitypub_rest_outbox_query`. +- Deprecated `rest_activitypub_outbox_query` filter in favor of `activitypub_rest_outbox_query`. Deprecated `activitypub_outbox_post` action in favor of `activitypub_rest_outbox_post`. [#1628] ### Fixed @@ -1212,6 +1321,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - initial +[7.0.1]: https://github.com/Automattic/wordpress-activitypub/compare/7.0.0...7.0.1 +[7.0.0]: https://github.com/Automattic/wordpress-activitypub/compare/6.0.2...7.0.0 +[6.0.2]: https://github.com/Automattic/wordpress-activitypub/compare/6.0.1...6.0.2 +[6.0.1]: https://github.com/Automattic/wordpress-activitypub/compare/6.0.0...6.0.1 +[6.0.0]: https://github.com/Automattic/wordpress-activitypub/compare/5.9.2...6.0.0 +[5.9.2]: https://github.com/Automattic/wordpress-activitypub/compare/5.9.1...5.9.2 +[5.9.1]: https://github.com/Automattic/wordpress-activitypub/compare/5.9.0...5.9.1 [5.9.0]: https://github.com/Automattic/wordpress-activitypub/compare/5.8.0...5.9.0 [5.8.0]: https://github.com/Automattic/wordpress-activitypub/compare/5.7.0...5.8.0 [5.7.0]: https://github.com/Automattic/wordpress-activitypub/compare/5.6.1...5.7.0 diff --git a/FEDERATION.md b/FEDERATION.md index 243459eb5..4db647899 100644 --- a/FEDERATION.md +++ b/FEDERATION.md @@ -5,8 +5,8 @@ The WordPress plugin largely follows ActivityPub's server-to-server specificatio ## Supported federation protocols and standards - [ActivityPub](https://www.w3.org/TR/activitypub/) (Server-to-Server) -- [WebFinger](https://swicg.github.io/activitypub-http-signature/) -- [HTTP Signatures](https://www.w3.org/community/reports/socialcg/CG-FINAL-apwf-20240608/) +- [WebFinger](https://www.w3.org/community/reports/socialcg/CG-FINAL-apwf-20240608/) +- [HTTP Signatures](https://swicg.github.io/activitypub-http-signature/) - [NodeInfo](https://nodeinfo.diaspora.software/) ## Supported FEPs @@ -19,6 +19,7 @@ The WordPress plugin largely follows ActivityPub's server-to-server specificatio - [FEP-fb2a: Actor metadata](https://codeberg.org/fediverse/fep/src/branch/main/fep/fb2a/fep-fb2a.md) - [FEP-b2b8: Long-form Text](https://codeberg.org/fediverse/fep/src/branch/main/fep/b2b8/fep-b2b8.md) - [FEP-7888: Demystifying the context property](https://codeberg.org/fediverse/fep/src/branch/main/fep/7888/fep-7888.md) +- [FEP-844e: Capability discovery](https://codeberg.org/fediverse/fep/src/branch/main/fep/844e/fep-844e.md) Partially supported FEPs diff --git a/activitypub.php b/activitypub.php index d1ee2845f..2d521f2e9 100644 --- a/activitypub.php +++ b/activitypub.php @@ -3,7 +3,7 @@ * Plugin Name: ActivityPub * Plugin URI: https://github.com/Automattic/wordpress-activitypub * Description: The ActivityPub protocol is a decentralized social networking protocol based upon the ActivityStreams 2.0 data format. - * Version: 5.9.0 + * Version: 7.0.1 * Author: Matthias Pfefferle & Automattic * Author URI: https://automattic.com/ * License: MIT @@ -19,7 +19,7 @@ use WP_CLI; -\define( 'ACTIVITYPUB_PLUGIN_VERSION', '5.9.0' ); +\define( 'ACTIVITYPUB_PLUGIN_VERSION', '7.0.1' ); // Plugin related constants. \define( 'ACTIVITYPUB_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); @@ -96,8 +96,10 @@ function plugin_init() { * Initialize plugin admin. */ function plugin_admin_init() { - // Menus are registered before `admin_init`, because of course they are. + // Screen Options and Menus are set before `admin_init`. + \add_filter( 'init', array( __NAMESPACE__ . '\WP_Admin\Screen_Options', 'init' ) ); \add_action( 'admin_menu', array( __NAMESPACE__ . '\WP_Admin\Menu', 'admin_menu' ) ); + \add_action( 'admin_init', array( __NAMESPACE__ . '\WP_Admin\Admin', 'init' ) ); \add_action( 'admin_init', array( __NAMESPACE__ . '\WP_Admin\Health_Check', 'init' ) ); \add_action( 'admin_init', array( __NAMESPACE__ . '\WP_Admin\Settings', 'init' ) ); @@ -151,50 +153,7 @@ function activation_redirect( $plugin ) { ) ); - -/** - * `get_plugin_data` wrapper. - * - * @deprecated 4.2.0 Use `get_plugin_data` instead. - * - * @param array $default_headers Optional. The default plugin headers. Default empty array. - * @return array The plugin metadata array. - */ -function get_plugin_meta( $default_headers = array() ) { - _deprecated_function( __FUNCTION__, '4.2.0', 'get_plugin_data' ); - - if ( ! $default_headers ) { - $default_headers = array( - 'Name' => 'Plugin Name', - 'PluginURI' => 'Plugin URI', - 'Version' => 'Version', - 'Description' => 'Description', - 'Author' => 'Author', - 'AuthorURI' => 'Author URI', - 'TextDomain' => 'Text Domain', - 'DomainPath' => 'Domain Path', - 'Network' => 'Network', - 'RequiresWP' => 'Requires at least', - 'RequiresPHP' => 'Requires PHP', - 'UpdateURI' => 'Update URI', - ); - } - - return \get_file_data( __FILE__, $default_headers, 'plugin' ); -} - -/** - * Plugin Version Number used for caching. - * - * @deprecated 4.2.0 Use constant ACTIVITYPUB_PLUGIN_VERSION directly. - */ -function get_plugin_version() { - _deprecated_function( __FUNCTION__, '4.2.0', 'ACTIVITYPUB_PLUGIN_VERSION' ); - - return ACTIVITYPUB_PLUGIN_VERSION; -} - -// Check for CLI env, to add the CLI commands. +// Check for CLI env, to add the CLI commands.Add commentMore actions. if ( defined( 'WP_CLI' ) && WP_CLI ) { WP_CLI::add_command( 'activitypub', diff --git a/build/editor-plugin/block.json b/build/blocks/editor-plugin/block.json similarity index 100% rename from build/editor-plugin/block.json rename to build/blocks/editor-plugin/block.json diff --git a/build/blocks/editor-plugin/plugin.asset.php b/build/blocks/editor-plugin/plugin.asset.php new file mode 100644 index 000000000..276924f72 --- /dev/null +++ b/build/blocks/editor-plugin/plugin.asset.php @@ -0,0 +1 @@ + array('react', 'wp-components', 'wp-core-data', 'wp-data', 'wp-edit-post', 'wp-editor', 'wp-element', 'wp-i18n', 'wp-plugins', 'wp-primitives', 'wp-url'), 'version' => '59c90d6c8ab5e740f5a1'); diff --git a/build/blocks/editor-plugin/plugin.js b/build/blocks/editor-plugin/plugin.js new file mode 100644 index 000000000..83a5cd37e --- /dev/null +++ b/build/blocks/editor-plugin/plugin.js @@ -0,0 +1 @@ +(()=>{"use strict";var e={20:(e,t,i)=>{var n=i(609),a=Symbol.for("react.element"),o=(Symbol.for("react.fragment"),Object.prototype.hasOwnProperty),l=n.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,r={key:!0,ref:!0,__self:!0,__source:!0};t.jsx=function(e,t,i){var n,c={},s=null,p=null;for(n in void 0!==i&&(s=""+i),void 0!==t.key&&(s=""+t.key),void 0!==t.ref&&(p=t.ref),t)o.call(t,n)&&!r.hasOwnProperty(n)&&(c[n]=t[n]);if(e&&e.defaultProps)for(n in t=e.defaultProps)void 0===c[n]&&(c[n]=t[n]);return{$$typeof:a,type:e,key:s,ref:p,props:c,_owner:l.current}}},609:e=>{e.exports=window.React},848:(e,t,i)=>{e.exports=i(20)}},t={};function i(n){var a=t[n];if(void 0!==a)return a.exports;var o=t[n]={exports:{}};return e[n](o,o.exports,i),o.exports}var n=i(609);const a=window.wp.editor,o=window.wp.editPost,l=window.wp.plugins,r=window.wp.components,c=window.wp.element,s=(0,c.forwardRef)((function({icon:e,size:t=24,...i},n){return(0,c.cloneElement)(e,{width:t,height:t,...i,ref:n})})),p=window.wp.primitives;var u=i(848);const v=(0,u.jsx)(p.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,u.jsx)(p.Path,{d:"M12 3.3c-4.8 0-8.8 3.9-8.8 8.8 0 4.8 3.9 8.8 8.8 8.8 4.8 0 8.8-3.9 8.8-8.8s-4-8.8-8.8-8.8zm6.5 5.5h-2.6C15.4 7.3 14.8 6 14 5c2 .6 3.6 2 4.5 3.8zm.7 3.2c0 .6-.1 1.2-.2 1.8h-2.9c.1-.6.1-1.2.1-1.8s-.1-1.2-.1-1.8H19c.2.6.2 1.2.2 1.8zM12 18.7c-1-.7-1.8-1.9-2.3-3.5h4.6c-.5 1.6-1.3 2.9-2.3 3.5zm-2.6-4.9c-.1-.6-.1-1.1-.1-1.8 0-.6.1-1.2.1-1.8h5.2c.1.6.1 1.1.1 1.8s-.1 1.2-.1 1.8H9.4zM4.8 12c0-.6.1-1.2.2-1.8h2.9c-.1.6-.1 1.2-.1 1.8 0 .6.1 1.2.1 1.8H5c-.2-.6-.2-1.2-.2-1.8zM12 5.3c1 .7 1.8 1.9 2.3 3.5H9.7c.5-1.6 1.3-2.9 2.3-3.5zM10 5c-.8 1-1.4 2.3-1.8 3.8H5.5C6.4 7 8 5.6 10 5zM5.5 15.3h2.6c.4 1.5 1 2.8 1.8 3.7-1.8-.6-3.5-2-4.4-3.7zM14 19c.8-1 1.4-2.2 1.8-3.7h2.6C17.6 17 16 18.4 14 19z"})}),w=(0,u.jsx)(p.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,u.jsx)(p.Path,{d:"M15.5 9.5a1 1 0 100-2 1 1 0 000 2zm0 1.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5zm-2.25 6v-2a2.75 2.75 0 00-2.75-2.75h-4A2.75 2.75 0 003.75 15v2h1.5v-2c0-.69.56-1.25 1.25-1.25h4c.69 0 1.25.56 1.25 1.25v2h1.5zm7-2v2h-1.5v-2c0-.69-.56-1.25-1.25-1.25H15v-1.5h2.5A2.75 2.75 0 0120.25 15zM9.5 8.5a1 1 0 11-2 0 1 1 0 012 0zm1.5 0a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z",fillRule:"evenodd"})}),_=(0,u.jsx)(p.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,u.jsx)(p.Path,{d:"M19.5 4.5h-7V6h4.44l-5.97 5.97 1.06 1.06L18 7.06v4.44h1.5v-7Zm-13 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-3H17v3a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h3V5.5h-3Z"})}),d=window.wp.data,m=window.wp.coreData,h=window.wp.url,b=window.wp.i18n;(0,l.registerPlugin)("activitypub-editor-plugin",{render:()=>{const e=(0,d.useSelect)((e=>e(a.store).getCurrentPostType()),[]),[t,i]=(0,m.useEntityProp)("postType",e,"meta");if("wp_block"===e)return null;const l=(0,n.createElement)(p.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},(0,n.createElement)(p.Path,{fillRule:"evenodd",clipRule:"evenodd",d:"M12 18.5A6.5 6.5 0 0 1 6.93 7.931l9.139 9.138A6.473 6.473 0 0 1 12 18.5Zm5.123-2.498a6.5 6.5 0 0 0-9.124-9.124l9.124 9.124ZM4 12a8 8 0 1 1 16 0 8 8 0 0 1-16 0Z"})),c={verticalAlign:"middle",gap:"4px",justifyContent:"start",display:"inline-flex",alignItems:"center"},u=(e,t,i)=>(0,n.createElement)(r.Tooltip,{text:i},(0,n.createElement)(r.__experimentalText,{style:c},(0,n.createElement)(s,{icon:e}),t)),_=a.PluginDocumentSettingPanel||o.PluginDocumentSettingPanel;return(0,n.createElement)(_,{name:"activitypub",className:"block-editor-block-inspector",title:(0,b.__)("Fediverse ⁂","activitypub")},(0,n.createElement)(r.TextControl,{label:(0,b.__)("Content Warning","activitypub"),value:t?.activitypub_content_warning,onChange:e=>{i({...t,activitypub_content_warning:e})},placeholder:(0,b.__)("Optional content warning","activitypub"),help:(0,b.__)("Content warnings do not change the content on your site, only in the fediverse.","activitypub"),__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0}),(0,n.createElement)(r.RangeControl,{label:(0,b.__)("Maximum Image Attachments","activitypub"),value:t?.activitypub_max_image_attachments,onChange:e=>{i({...t,activitypub_max_image_attachments:e})},min:0,max:10,help:(0,b.__)("Maximum number of image attachments to include when sharing to the fediverse.","activitypub"),__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0}),(0,n.createElement)(r.RadioControl,{label:(0,b.__)("Visibility","activitypub"),help:(0,b.__)("This adjusts the visibility of a post in the fediverse, but note that it won't affect how the post appears on the blog.","activitypub"),selected:t?.activitypub_content_visibility||"public",options:[{label:u(v,(0,b.__)("Public","activitypub"),(0,b.__)("Post will be visible to everyone and appear in public timelines.","activitypub")),value:"public"},{label:u(w,(0,b.__)("Quiet public","activitypub"),(0,b.__)("Post will be visible to everyone but will not appear in public timelines.","activitypub")),value:"quiet_public"},{label:u(l,(0,b.__)("Do not federate","activitypub"),(0,b.__)("Post will not be shared to the Fediverse.","activitypub")),value:"local"}],onChange:e=>{i({...t,activitypub_content_visibility:e})},className:"activitypub-visibility"}))}}),(0,l.registerPlugin)("activitypub-editor-preview",{render:()=>{const e=(0,d.useSelect)((e=>e(a.store).getCurrentPost().status),[]);return(0,n.createElement)(n.Fragment,null,a.PluginPreviewMenuItem?(0,n.createElement)(a.PluginPreviewMenuItem,{onClick:()=>{const e=(0,d.select)(a.store).getEditedPostPreviewLink(),t=(0,h.addQueryArgs)(e,{activitypub:"true"});window.open(t,"_blank")},icon:_,disabled:"auto-draft"===e},(0,b.__)("Fediverse preview ⁂","activitypub")):null)}})})(); \ No newline at end of file diff --git a/build/follow-me/block.json b/build/blocks/follow-me/block.json similarity index 61% rename from build/follow-me/block.json rename to build/blocks/follow-me/block.json index e799fbb58..36b8965b3 100644 --- a/build/follow-me/block.json +++ b/build/blocks/follow-me/block.json @@ -2,14 +2,20 @@ "$schema": "https://schemas.wp.org/trunk/block.json", "name": "activitypub/follow-me", "apiVersion": 3, - "version": "1.0.0", + "version": "2.2.0", "title": "Follow me on the Fediverse", "category": "widgets", "description": "Display your Fediverse profile so that visitors can follow you.", "textdomain": "activitypub", "icon": "groups", + "example": { + "attributes": { + "className": "is-style-default" + } + }, "supports": { "html": false, + "interactivity": true, "color": { "gradients": true, "link": true, @@ -25,34 +31,38 @@ "color": true, "style": true }, + "shadow": true, "typography": { "fontSize": true, "__experimentalDefaultControls": { "fontSize": true } + }, + "innerBlocks": { + "allowedBlocks": [ + "core/button" + ] } }, - "attributes": { - "selectedUser": { - "type": "string", - "default": "site" + "styles": [ + { + "name": "default", + "label": "Default", + "isDefault": true }, - "buttonOnly": { - "type": "boolean", - "default": false + { + "name": "button-only", + "label": "Button" }, - "buttonText": { - "type": "string", - "default": "Follow" - }, - "buttonSize": { + { + "name": "profile", + "label": "Profile" + } + ], + "attributes": { + "selectedUser": { "type": "string", - "default": "default", - "enum": [ - "small", - "default", - "compact" - ] + "default": "blog" } }, "usesContext": [ @@ -60,9 +70,8 @@ "postId" ], "editorScript": "file:./index.js", - "viewScript": "file:./view.js", - "style": [ - "file:./style-view.css", - "wp-components" - ] + "viewScriptModule": "file:./view.js", + "viewScript": "wp-api-fetch", + "style": "file:./style-index.css", + "render": "file:./render.php" } \ No newline at end of file diff --git a/build/blocks/follow-me/index.asset.php b/build/blocks/follow-me/index.asset.php new file mode 100644 index 000000000..01ddc09b2 --- /dev/null +++ b/build/blocks/follow-me/index.asset.php @@ -0,0 +1 @@ + array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-core-data', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '2f132bae6907da90c624'); diff --git a/build/blocks/follow-me/index.js b/build/blocks/follow-me/index.js new file mode 100644 index 000000000..ddb0c9341 --- /dev/null +++ b/build/blocks/follow-me/index.js @@ -0,0 +1,2 @@ +(()=>{var e,t={20:(e,t,r)=>{"use strict";var o=r(609),n=Symbol.for("react.element"),a=(Symbol.for("react.fragment"),Object.prototype.hasOwnProperty),l=o.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,s={key:!0,ref:!0,__self:!0,__source:!0};t.jsx=function(e,t,r){var o,i={},c=null,u=null;for(o in void 0!==r&&(c=""+r),void 0!==t.key&&(c=""+t.key),void 0!==t.ref&&(u=t.ref),t)a.call(t,o)&&!s.hasOwnProperty(o)&&(i[o]=t[o]);if(e&&e.defaultProps)for(o in t=e.defaultProps)void 0===i[o]&&(i[o]=t[o]);return{$$typeof:n,type:e,key:c,ref:u,props:i,_owner:l.current}}},28:(e,t,r)=>{"use strict";const o=window.wp.blocks,n=window.wp.primitives;var a=r(848);const l=(0,a.jsx)(n.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,a.jsx)(n.Path,{d:"M15.5 9.5a1 1 0 100-2 1 1 0 000 2zm0 1.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5zm-2.25 6v-2a2.75 2.75 0 00-2.75-2.75h-4A2.75 2.75 0 003.75 15v2h1.5v-2c0-.69.56-1.25 1.25-1.25h4c.69 0 1.25.56 1.25 1.25v2h1.5zm7-2v2h-1.5v-2c0-.69-.56-1.25-1.25-1.25H15v-1.5h2.5A2.75 2.75 0 0120.25 15zM9.5 8.5a1 1 0 11-2 0 1 1 0 012 0zm1.5 0a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z",fillRule:"evenodd"})});var s=r(609),i=r(942),c=r.n(i);const u=window.wp.blockEditor,p=window.wp.i18n,d={html:!1,color:{gradients:!0,link:!0,__experimentalDefaultControls:{background:!0,text:!0,link:!0}},__experimentalBorder:{radius:!0,width:!0,color:!0,style:!0},typography:{fontSize:!0,__experimentalDefaultControls:{fontSize:!0}}},f=d;function v({buttonOnly:e=!1,className:t="",...r}){return r.className=c()(t,e?"is-style-button-only":"is-style-default"),r}const b={attributes:{buttonOnly:{type:"boolean",default:!1},buttonText:{type:"string",default:"Follow"},selectedUser:{type:"string",default:"blog"}},supports:d,isEligible:({buttonText:e,buttonOnly:t})=>!!e||!!t,migrate({buttonText:e,...t}){const r=(0,o.createBlock)("core/button",{text:e});return[v(t),[r]]}},m={attributes:{selectedUser:{type:"string",default:"blog"},buttonOnly:{type:"boolean",default:!1}},supports:f,isEligible:({buttonOnly:e})=>!!e,migrate:v,save(){const e=u.useBlockProps.save(),t=u.useInnerBlocksProps.save(e);return(0,s.createElement)("div",{...t})}},y=[{attributes:{selectedUser:{type:"string",default:"blog"}},supports:f,isEligible:(e,t)=>1===t.length&&"button"===t[0].attributes.tagName,migrate(e,t){var r;const{tagName:n,...a}=t[0].attributes,l=null!==(r=t[0].originalContent.replace(/<[^>]*>/g,""))&&void 0!==r?r:(0,p.__)("Follow","activitypub");return[e,[(0,o.createBlock)("core/button",{...a,text:l})]]},save(){const e=u.useBlockProps.save(),t=u.useInnerBlocksProps.save(e);return(0,s.createElement)("div",{...t})}},m,b],w=window.wp.apiFetch;var h=r.n(w);const g=window.wp.data,_=window.wp.coreData,E=window.wp.components,k=window.wp.element;function x(){return window._activityPubOptions||{}}function O({name:e}){const{enabled:t}=x(),r=t?.blog?"":(0,p.__)("It will be empty in other non-author contexts.","activitypub"),o=(0,p.sprintf)(/* translators: %1$s: block name, %2$s: extra information for non-author context */ /* translators: %1$s: block name, %2$s: extra information for non-author context */ +(0,p.__)("This %1$s block will adapt to the page it is on, displaying the user profile associated with a post author (in a loop) or a user archive. %2$s","activitypub"),e,r).trim();return(0,s.createElement)(E.Card,null,(0,s.createElement)(E.CardBody,null,(0,k.createInterpolateElement)(o,{strong:(0,s.createElement)("strong",null)})))}const S={avatar:"https://secure.gravatar.com/avatar/default?s=120",webfinger:"@well@hello.dolly",name:(0,p.__)("Hello Dolly Fan Account","activitypub"),url:"#",image:{url:""},summary:""};function B(e){if(!e)return S;const t={...S,...e};return t.avatar=t?.icon?.url,t.webfinger&&!t.webfinger.startsWith("@")&&(t.webfinger="@"+t.webfinger),t}function N({profile:e,className:t,innerBlocksProps:r}){const{webfinger:o,avatar:n,name:a,image:l,summary:i,followersCount:c,postsCount:u}=e,d=t&&t.includes("is-style-button-only"),f={posts:u||0,followers:c||0};return(0,s.createElement)("div",{className:"activitypub-profile"},!d&&l?.url&&(0,s.createElement)("div",{className:"activitypub-profile__header",style:{backgroundImage:`url(${l.url})`}}),(0,s.createElement)("div",{className:"activitypub-profile__body"},!d&&(0,s.createElement)("img",{className:"activitypub-profile__avatar",src:n,alt:a}),(0,s.createElement)("div",{className:"activitypub-profile__content"},!d&&(0,s.createElement)("div",{className:"activitypub-profile__info"},(0,s.createElement)("div",{className:"activitypub-profile__name"},a),(0,s.createElement)("div",{className:"activitypub-profile__handle"},o)),(0,s.createElement)("div",{...r}),!d&&(0,s.createElement)("div",{className:"activitypub-profile__bio",dangerouslySetInnerHTML:{__html:i}}),!d&&(0,s.createElement)("div",{className:"activitypub-profile__stats"},Object.entries(f).map((([e,t])=>(0,s.createElement)("div",{key:e},(0,s.createElement)("strong",null,t)," ","posts"===e?(0,p._n)("post","posts",t,"activitypub"):"followers"===e?(0,p._n)("follower","followers",t,"activitypub"):(0,p._n)("following","following",t,"activitypub"))))))))}const P=JSON.parse('{"$schema":"https://schemas.wp.org/trunk/block.json","name":"activitypub/follow-me","apiVersion":3,"version":"2.2.0","title":"Follow me on the Fediverse","category":"widgets","description":"Display your Fediverse profile so that visitors can follow you.","textdomain":"activitypub","icon":"groups","example":{"attributes":{"className":"is-style-default"}},"supports":{"html":false,"interactivity":true,"color":{"gradients":true,"link":true,"__experimentalDefaultControls":{"background":true,"text":true,"link":true}},"__experimentalBorder":{"radius":true,"width":true,"color":true,"style":true},"shadow":true,"typography":{"fontSize":true,"__experimentalDefaultControls":{"fontSize":true}},"innerBlocks":{"allowedBlocks":["core/button"]}},"styles":[{"name":"default","label":"Default","isDefault":true},{"name":"button-only","label":"Button"},{"name":"profile","label":"Profile"}],"attributes":{"selectedUser":{"type":"string","default":"blog"}},"usesContext":["postType","postId"],"editorScript":"file:./index.js","viewScriptModule":"file:./view.js","viewScript":"wp-api-fetch","style":"file:./style-index.css","render":"file:./render.php"}');(0,o.registerBlockType)(P,{deprecated:y,edit:function({attributes:e,setAttributes:t,context:{postType:r,postId:o}}){const n=(0,u.useBlockProps)({className:"activitypub-follow-me-block-wrapper"}),a=function({withInherit:e=!1}){const{enabled:t}=x(),r=t?.users?(0,g.useSelect)((e=>e("core").getUsers({capabilities:"activitypub"})),[]):[];return(0,k.useMemo)((()=>{if(!r)return[];const o=[];return t?.blog&&o.push({label:(0,p.__)("Blog","activitypub"),value:"blog"}),e&&t?.users&&o.push({label:(0,p.__)("Dynamic User","activitypub"),value:"inherit"}),r.reduce(((e,t)=>(e.push({label:t.name,value:`${t.id}`}),e)),o)}),[r])}({withInherit:!0}),{namespace:l}=x(),{selectedUser:i,className:c="is-style-default"}=e,d="inherit"===i,[f,v]=(0,k.useState)(B(S)),b="blog"===i?0:i,m=[["core/button",{text:(0,p.__)("Follow","activitypub")}]],y=(0,u.useInnerBlocksProps)({},{allowedBlocks:["core/button"],template:m,templateLock:!1,renderAppender:!1}),w=(0,g.useSelect)((e=>{const{getEditedEntityRecord:t}=e(_.store),n=t("postType",r,o)?.author;return null!=n?n:null}),[r,o]);return(0,k.useEffect)((()=>{if(d&&!w)return;const e=d?w:b;(function(e){const{namespace:t}=x(),r={headers:{Accept:"application/activity+json"},path:`/${t}/actors/${e}`};return h()(r)})(e).then((t=>{if(v(B(t)),t.followers)try{const{pathname:e}=new URL(t.followers);h()({path:e.replace("wp-json/","")}).then((({totalItems:e=0})=>{v((t=>({...t,followersCount:e})))})).catch((()=>{}))}catch(e){}e?h()({path:`/wp/v2/users/${e}/?context=activitypub`}).then((({post_count:e})=>{v((t=>({...t,postsCount:e})))})).catch((()=>{})):h()({path:"/wp/v2/posts",method:"HEAD",parse:!1}).then((e=>{const t=e.headers.get("X-WP-Total");v((e=>({...e,postsCount:t})))})).catch((()=>{}))})).catch((()=>{}))}),[b,w,d]),(0,k.useEffect)((()=>{a.length&&(a.find((({value:e})=>e===i))||t({selectedUser:a[0].value}))}),[i,a]),(0,s.createElement)("div",{...n},(0,s.createElement)(u.InspectorControls,{key:"activitypub-follow-me"},a.length>1&&(0,s.createElement)(E.PanelBody,{title:(0,p.__)("Follow Me Options","activitypub")},(0,s.createElement)(E.SelectControl,{label:(0,p.__)("Select User","activitypub"),value:e.selectedUser,options:a,onChange:e=>t({selectedUser:e}),__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0}))),d&&!w?(0,s.createElement)(O,{name:(0,p.__)("Follow Me","activitypub")}):(0,s.createElement)(N,{profile:f,className:c,innerBlocksProps:y}))},icon:l,save:function(){const e=u.useBlockProps.save(),t=u.useInnerBlocksProps.save(e);return(0,s.createElement)("div",{...t})}})},609:e=>{"use strict";e.exports=window.React},848:(e,t,r)=>{"use strict";e.exports=r(20)},942:(e,t)=>{var r;!function(){"use strict";var o={}.hasOwnProperty;function n(){for(var e="",t=0;t{if(!r){var l=1/0;for(u=0;u=a)&&Object.keys(o.O).every((e=>o.O[e](r[i])))?r.splice(i--,1):(s=!1,a0&&e[u-1][2]>a;u--)e[u]=e[u-1];e[u]=[r,n,a]},o.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return o.d(t,{a:t}),t},o.d=(e,t)=>{for(var r in t)o.o(t,r)&&!o.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},o.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e={759:0,975:0};o.O.j=t=>0===e[t];var t=(t,r)=>{var n,a,[l,s,i]=r,c=0;if(l.some((t=>0!==e[t]))){for(n in s)o.o(s,n)&&(o.m[n]=s[n]);if(i)var u=i(o)}for(t&&t(r);co(28)));n=o.O(n)})(); \ No newline at end of file diff --git a/build/blocks/follow-me/render.php b/build/blocks/follow-me/render.php new file mode 100644 index 000000000..df7f3ec4c --- /dev/null +++ b/build/blocks/follow-me/render.php @@ -0,0 +1,244 @@ + ACTIVITYPUB_REST_NAMESPACE, + 'i18n' => array( + 'copy' => __( 'Copy', 'activitypub' ), + 'copied' => __( 'Copied!', 'activitypub' ), + 'emptyProfileError' => __( 'Please enter a profile URL or handle.', 'activitypub' ), + 'genericError' => __( 'An error occurred. Please try again.', 'activitypub' ), + 'invalidProfileError' => __( 'Please enter a valid profile URL or handle.', 'activitypub' ), + ), + ) +); + +// Add the block wrapper attributes. +$wrapper_attributes = array( + 'id' => $block_id, + 'class' => 'activitypub-follow-me-block-wrapper', + 'data-wp-interactive' => 'activitypub/follow-me', + 'data-wp-init' => 'callbacks.initButtonStyles', +); +if ( isset( $attributes['buttonOnly'] ) ) { + $wrapper_attributes['class'] .= ' is-style-button-only'; +} + +$wrapper_context = wp_interactivity_data_wp_context( + array( + 'backgroundColor' => $background_color, + 'blockId' => $block_id, + 'buttonStyle' => $button_style, + 'copyButtonText' => __( 'Copy', 'activitypub' ), + 'errorMessage' => '', + 'isError' => false, + 'isLoading' => false, + 'modal' => array( 'isOpen' => false ), + 'remoteProfile' => '', + 'userId' => $user_id, + 'webfinger' => '@' . $actor->get_webfinger(), + ) +); + +if ( empty( $content ) ) { + $button_text = $attributes['buttonText'] ?? __( 'Follow', 'activitypub' ); + $content = ''; +} else { + $content = implode( PHP_EOL, wp_list_pluck( $block->parsed_block['innerBlocks'], 'innerHTML' ) ); +} + +$content = Blocks::add_directions( + $content, + array( 'class_name' => 'wp-element-button' ), + array( + 'data-wp-on--click' => 'actions.toggleModal', + 'data-wp-on-async--keydown' => 'actions.onKeydown', + 'data-wp-bind--aria-expanded' => 'context.modal.isOpen', + 'aria-label' => __( 'Follow me on the Fediverse', 'activitypub' ), + 'aria-haspopup' => 'dialog', + 'aria-controls' => 'modal-heading', + 'role' => 'button', + 'tabindex' => '0', + ) +); + +$header_image = $actor->get_image(); +$has_header = ! empty( $header_image['url'] ) && str_contains( $attributes['className'] ?? '', 'is-style-profile' ); + +$stats = array( + 'posts' => $user_id ? count_user_posts( $user_id, 'post', true ) : (int) wp_count_posts()->publish, + 'followers' => Followers::count_followers( $user_id ), +); + +ob_start(); +?> +
+

+
+ +
+
+ + +
+
+
+

+
+ +
+
+ + +
+
+
+ +
+ +> +
+ +
+ + +
+ <?php echo esc_attr( $actor->get_name() ); ?> + +
+
+
get_name() ); ?>
+ +
+
+ + + + get_summary() ) : ?> +
+ get_summary() ); ?> +
+ + +
+ +
+ ' . esc_html( number_format_i18n( $stats['posts'] ) ) . '' + ); + ?> +
+ + +
+ ' . esc_html( number_format_i18n( $stats['followers'] ) ) . '' + ); + ?> +
+ +
+
+
+
+ + $modal_content, + /* translators: %s: Profile name. */ + 'title' => sprintf( esc_html__( 'Follow %s', 'activitypub' ), esc_html( $actor->get_name() ) ), + ) + ); + ?> +
diff --git a/build/blocks/follow-me/style-index-rtl.css b/build/blocks/follow-me/style-index-rtl.css new file mode 100644 index 000000000..5c9ec8c63 --- /dev/null +++ b/build/blocks/follow-me/style-index-rtl.css @@ -0,0 +1 @@ +body.modal-open{overflow:hidden}.activitypub-modal__overlay{align-items:center;background-color:rgba(0,0,0,.5);bottom:0;color:initial;display:flex;justify-content:center;right:0;padding:1rem;position:fixed;left:0;top:0;z-index:100000}.activitypub-modal__overlay.compact{align-items:flex-start;background-color:transparent;bottom:auto;justify-content:flex-start;right:auto;padding:0;position:absolute;left:auto;top:auto;z-index:100}.activitypub-modal__overlay[hidden]{display:none}.activitypub-modal__frame{animation:activitypub-modal-appear .2s ease-out;background-color:var(--wp--preset--color--white,#fff);border-radius:8px;box-shadow:0 5px 15px rgba(0,0,0,.3);display:flex;flex-direction:column;max-height:calc(100vh - 2rem);max-width:660px;overflow:hidden;width:100%}.compact .activitypub-modal__frame{box-shadow:0 2px 8px rgba(0,0,0,.1);max-height:300px;max-width:-moz-min-content;max-width:min-content;min-width:250px;width:auto}.activitypub-modal__header{align-items:center;border-bottom:1px solid var(--wp--preset--color--light-gray,#f0f0f0);display:flex;flex-shrink:0;justify-content:space-between;padding:2rem 2rem 1.5rem}.compact .activitypub-modal__header{display:none}.activitypub-modal__header .activitypub-modal__close{align-items:center;border:none;cursor:pointer;display:flex;justify-content:center;padding:.5rem;width:auto}.activitypub-modal__header .activitypub-modal__close:active{border:none;padding:.5rem}.activitypub-modal__title{font-size:130%;font-weight:600;line-height:1.4;margin:0!important}.activitypub-modal__content{overflow-y:auto}@keyframes activitypub-modal-appear{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.activitypub-follow-me-block-wrapper{display:block;margin:1rem 0;position:relative}.activitypub-follow-me-block-wrapper .activitypub-profile{padding:1rem 0}.activitypub-follow-me-block-wrapper .activitypub-profile__body{display:flex;flex-wrap:wrap}.activitypub-follow-me-block-wrapper .activitypub-profile__avatar{border-radius:50%;height:75px;margin-left:1rem;-o-object-fit:cover;object-fit:cover;width:75px}.activitypub-follow-me-block-wrapper .activitypub-profile__content{align-items:center;display:flex;flex:1;flex-wrap:wrap;justify-content:space-between;min-width:0}.activitypub-follow-me-block-wrapper .activitypub-profile__info{display:block;flex:1;min-width:0}.activitypub-follow-me-block-wrapper .activitypub-profile__name{font-size:1.25em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.activitypub-follow-me-block-wrapper .activitypub-profile__handle,.activitypub-follow-me-block-wrapper .activitypub-profile__name{color:inherit;line-height:1.2;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.activitypub-follow-me-block-wrapper .activitypub-profile div.wp-block-button{align-items:center;display:flex;margin:0 1rem 0 0}.activitypub-follow-me-block-wrapper .activitypub-profile .wp-block-button__link{margin:0}.activitypub-follow-me-block-wrapper .activitypub-profile .is-small{font-size:.8rem;padding:.25rem .5rem}.activitypub-follow-me-block-wrapper .activitypub-profile .is-compact{font-size:.9rem;padding:.4rem .8rem}.activitypub-follow-me-block-wrapper:not(.is-style-button-only):not(.is-style-profile) .activitypub-profile__bio,.activitypub-follow-me-block-wrapper:not(.is-style-button-only):not(.is-style-profile) .activitypub-profile__stats{display:none}.activitypub-follow-me-block-wrapper.is-style-button-only .activitypub-profile{padding:0}.activitypub-follow-me-block-wrapper.is-style-button-only .activitypub-profile__body{display:block;padding:0}.activitypub-follow-me-block-wrapper.is-style-button-only .activitypub-profile__content{display:inline}.activitypub-follow-me-block-wrapper.is-style-button-only div.wp-block-button{display:inline-block;margin:0}.activitypub-follow-me-block-wrapper.is-style-button-only .activitypub-profile__avatar,.activitypub-follow-me-block-wrapper.is-style-button-only .activitypub-profile__bio,.activitypub-follow-me-block-wrapper.is-style-button-only .activitypub-profile__handle,.activitypub-follow-me-block-wrapper.is-style-button-only .activitypub-profile__name,.activitypub-follow-me-block-wrapper.is-style-button-only .activitypub-profile__stats{display:none}.activitypub-follow-me-block-wrapper.is-style-profile{border-radius:8px;box-shadow:0 2px 8px rgba(0,0,0,.1);overflow:hidden}.activitypub-follow-me-block-wrapper.is-style-profile .activitypub-profile,.activitypub-follow-me-block-wrapper.is-style-profile.has-background .activitypub-profile{padding:0}.activitypub-follow-me-block-wrapper.is-style-profile .activitypub-profile__header{background-color:#ccc;background-position:50%;background-size:cover;height:120px;width:100%}.activitypub-follow-me-block-wrapper.is-style-profile .activitypub-profile__body{padding:1rem}.activitypub-follow-me-block-wrapper.is-style-profile .activitypub-profile__avatar{height:64px;width:64px}.activitypub-follow-me-block-wrapper.is-style-profile .activitypub-profile__content{flex:1;min-width:0}.activitypub-follow-me-block-wrapper.is-style-profile .activitypub-profile__name{margin-bottom:.25rem}.activitypub-follow-me-block-wrapper.is-style-profile .activitypub-profile__bio{font-size:90%;line-height:1.4;margin-top:16px;width:100%}.activitypub-follow-me-block-wrapper.is-style-profile .activitypub-profile__bio p{margin:0 0 .5rem}.activitypub-follow-me-block-wrapper.is-style-profile .activitypub-profile__bio p:last-child{margin-bottom:0}.activitypub-follow-me-block-wrapper.is-style-profile .activitypub-profile__stats{display:flex;font-size:.9em;gap:16px;margin-top:1rem;width:100%}.activitypub-follow-me-block-wrapper.has-background .activitypub-profile,.activitypub-follow-me-block-wrapper.has-border .activitypub-profile{padding-right:1rem;padding-left:1rem}.activitypub-dialog__section{border-bottom:1px solid var(--wp--preset--color--light-gray,#f0f0f0);padding:1.5rem 2rem}.activitypub-dialog__section:last-child{border-bottom:none;padding-bottom:2rem}.activitypub-dialog__section h4{font-size:110%;margin-bottom:.5rem;margin-top:0}.activitypub-dialog__description{color:inherit;font-size:95%;margin-bottom:1rem}.activitypub-dialog__button-group{display:flex;margin-bottom:.5rem;width:100%}.activitypub-dialog__button-group input[type]{border:1px solid var(--wp--preset--color--gray,#e2e4e7);border-radius:0 4px 4px 0;flex:1;line-height:1;margin:0}.activitypub-dialog__button-group input[type]::-moz-placeholder{opacity:.5}.activitypub-dialog__button-group input[type]::placeholder{opacity:.5}.activitypub-dialog__button-group input[type][aria-invalid=true]{border-color:var(--wp--preset--color--vivid-red)}.activitypub-dialog__button-group button{border-radius:4px 0 0 4px!important;margin-right:-1px!important;min-width:22.5%;width:auto}.activitypub-dialog__error{color:var(--wp--preset--color--vivid-red);font-size:90%;margin-top:.5rem} diff --git a/build/blocks/follow-me/style-index.css b/build/blocks/follow-me/style-index.css new file mode 100644 index 000000000..386060cdf --- /dev/null +++ b/build/blocks/follow-me/style-index.css @@ -0,0 +1 @@ +body.modal-open{overflow:hidden}.activitypub-modal__overlay{align-items:center;background-color:rgba(0,0,0,.5);bottom:0;color:initial;display:flex;justify-content:center;left:0;padding:1rem;position:fixed;right:0;top:0;z-index:100000}.activitypub-modal__overlay.compact{align-items:flex-start;background-color:transparent;bottom:auto;justify-content:flex-start;left:auto;padding:0;position:absolute;right:auto;top:auto;z-index:100}.activitypub-modal__overlay[hidden]{display:none}.activitypub-modal__frame{animation:activitypub-modal-appear .2s ease-out;background-color:var(--wp--preset--color--white,#fff);border-radius:8px;box-shadow:0 5px 15px rgba(0,0,0,.3);display:flex;flex-direction:column;max-height:calc(100vh - 2rem);max-width:660px;overflow:hidden;width:100%}.compact .activitypub-modal__frame{box-shadow:0 2px 8px rgba(0,0,0,.1);max-height:300px;max-width:-moz-min-content;max-width:min-content;min-width:250px;width:auto}.activitypub-modal__header{align-items:center;border-bottom:1px solid var(--wp--preset--color--light-gray,#f0f0f0);display:flex;flex-shrink:0;justify-content:space-between;padding:2rem 2rem 1.5rem}.compact .activitypub-modal__header{display:none}.activitypub-modal__header .activitypub-modal__close{align-items:center;border:none;cursor:pointer;display:flex;justify-content:center;padding:.5rem;width:auto}.activitypub-modal__header .activitypub-modal__close:active{border:none;padding:.5rem}.activitypub-modal__title{font-size:130%;font-weight:600;line-height:1.4;margin:0!important}.activitypub-modal__content{overflow-y:auto}@keyframes activitypub-modal-appear{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.activitypub-follow-me-block-wrapper{display:block;margin:1rem 0;position:relative}.activitypub-follow-me-block-wrapper .activitypub-profile{padding:1rem 0}.activitypub-follow-me-block-wrapper .activitypub-profile__body{display:flex;flex-wrap:wrap}.activitypub-follow-me-block-wrapper .activitypub-profile__avatar{border-radius:50%;height:75px;margin-right:1rem;-o-object-fit:cover;object-fit:cover;width:75px}.activitypub-follow-me-block-wrapper .activitypub-profile__content{align-items:center;display:flex;flex:1;flex-wrap:wrap;justify-content:space-between;min-width:0}.activitypub-follow-me-block-wrapper .activitypub-profile__info{display:block;flex:1;min-width:0}.activitypub-follow-me-block-wrapper .activitypub-profile__name{font-size:1.25em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.activitypub-follow-me-block-wrapper .activitypub-profile__handle,.activitypub-follow-me-block-wrapper .activitypub-profile__name{color:inherit;line-height:1.2;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.activitypub-follow-me-block-wrapper .activitypub-profile div.wp-block-button{align-items:center;display:flex;margin:0 0 0 1rem}.activitypub-follow-me-block-wrapper .activitypub-profile .wp-block-button__link{margin:0}.activitypub-follow-me-block-wrapper .activitypub-profile .is-small{font-size:.8rem;padding:.25rem .5rem}.activitypub-follow-me-block-wrapper .activitypub-profile .is-compact{font-size:.9rem;padding:.4rem .8rem}.activitypub-follow-me-block-wrapper:not(.is-style-button-only):not(.is-style-profile) .activitypub-profile__bio,.activitypub-follow-me-block-wrapper:not(.is-style-button-only):not(.is-style-profile) .activitypub-profile__stats{display:none}.activitypub-follow-me-block-wrapper.is-style-button-only .activitypub-profile{padding:0}.activitypub-follow-me-block-wrapper.is-style-button-only .activitypub-profile__body{display:block;padding:0}.activitypub-follow-me-block-wrapper.is-style-button-only .activitypub-profile__content{display:inline}.activitypub-follow-me-block-wrapper.is-style-button-only div.wp-block-button{display:inline-block;margin:0}.activitypub-follow-me-block-wrapper.is-style-button-only .activitypub-profile__avatar,.activitypub-follow-me-block-wrapper.is-style-button-only .activitypub-profile__bio,.activitypub-follow-me-block-wrapper.is-style-button-only .activitypub-profile__handle,.activitypub-follow-me-block-wrapper.is-style-button-only .activitypub-profile__name,.activitypub-follow-me-block-wrapper.is-style-button-only .activitypub-profile__stats{display:none}.activitypub-follow-me-block-wrapper.is-style-profile{border-radius:8px;box-shadow:0 2px 8px rgba(0,0,0,.1);overflow:hidden}.activitypub-follow-me-block-wrapper.is-style-profile .activitypub-profile,.activitypub-follow-me-block-wrapper.is-style-profile.has-background .activitypub-profile{padding:0}.activitypub-follow-me-block-wrapper.is-style-profile .activitypub-profile__header{background-color:#ccc;background-position:50%;background-size:cover;height:120px;width:100%}.activitypub-follow-me-block-wrapper.is-style-profile .activitypub-profile__body{padding:1rem}.activitypub-follow-me-block-wrapper.is-style-profile .activitypub-profile__avatar{height:64px;width:64px}.activitypub-follow-me-block-wrapper.is-style-profile .activitypub-profile__content{flex:1;min-width:0}.activitypub-follow-me-block-wrapper.is-style-profile .activitypub-profile__name{margin-bottom:.25rem}.activitypub-follow-me-block-wrapper.is-style-profile .activitypub-profile__bio{font-size:90%;line-height:1.4;margin-top:16px;width:100%}.activitypub-follow-me-block-wrapper.is-style-profile .activitypub-profile__bio p{margin:0 0 .5rem}.activitypub-follow-me-block-wrapper.is-style-profile .activitypub-profile__bio p:last-child{margin-bottom:0}.activitypub-follow-me-block-wrapper.is-style-profile .activitypub-profile__stats{display:flex;font-size:.9em;gap:16px;margin-top:1rem;width:100%}.activitypub-follow-me-block-wrapper.has-background .activitypub-profile,.activitypub-follow-me-block-wrapper.has-border .activitypub-profile{padding-left:1rem;padding-right:1rem}.activitypub-dialog__section{border-bottom:1px solid var(--wp--preset--color--light-gray,#f0f0f0);padding:1.5rem 2rem}.activitypub-dialog__section:last-child{border-bottom:none;padding-bottom:2rem}.activitypub-dialog__section h4{font-size:110%;margin-bottom:.5rem;margin-top:0}.activitypub-dialog__description{color:inherit;font-size:95%;margin-bottom:1rem}.activitypub-dialog__button-group{display:flex;margin-bottom:.5rem;width:100%}.activitypub-dialog__button-group input[type]{border:1px solid var(--wp--preset--color--gray,#e2e4e7);border-radius:4px 0 0 4px;flex:1;line-height:1;margin:0}.activitypub-dialog__button-group input[type]::-moz-placeholder{opacity:.5}.activitypub-dialog__button-group input[type]::placeholder{opacity:.5}.activitypub-dialog__button-group input[type][aria-invalid=true]{border-color:var(--wp--preset--color--vivid-red)}.activitypub-dialog__button-group button{border-radius:0 4px 4px 0!important;margin-left:-1px!important;min-width:22.5%;width:auto}.activitypub-dialog__error{color:var(--wp--preset--color--vivid-red);font-size:90%;margin-top:.5rem} diff --git a/build/follow-me/style-view-rtl.css b/build/blocks/follow-me/style-view-rtl.css similarity index 100% rename from build/follow-me/style-view-rtl.css rename to build/blocks/follow-me/style-view-rtl.css diff --git a/build/follow-me/style-view.css b/build/blocks/follow-me/style-view.css similarity index 100% rename from build/follow-me/style-view.css rename to build/blocks/follow-me/style-view.css diff --git a/build/blocks/follow-me/view.asset.php b/build/blocks/follow-me/view.asset.php new file mode 100644 index 000000000..7fd88c86f --- /dev/null +++ b/build/blocks/follow-me/view.asset.php @@ -0,0 +1 @@ + array('@wordpress/interactivity'), 'version' => '1bdc53d1581dc837c6e5', 'type' => 'module'); diff --git a/build/blocks/follow-me/view.js b/build/blocks/follow-me/view.js new file mode 100644 index 000000000..0af3dcf1b --- /dev/null +++ b/build/blocks/follow-me/view.js @@ -0,0 +1 @@ +import*as t from"@wordpress/interactivity";var e={d:(t,o)=>{for(var n in o)e.o(o,n)&&!e.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:o[n]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e)};const o=(r={getContext:()=>t.getContext,getElement:()=>t.getElement,store:()=>t.store},l={},e.d(l,r),l),n={computedStyles:null,variables:{}};var r,l;function c(t){if("undefined"==typeof window||!window.getComputedStyle)return!1;if(n.variables.hasOwnProperty(t))return n.variables[t];n.computedStyles||(n.computedStyles=window.getComputedStyle(document.documentElement));const e=n.computedStyles.getPropertyValue(t).trim();return n.variables[t]=""!==e,n.variables[t]}function i(t){if("string"!=typeof t)return null;if(t.match(/^#/))return t.substring(0,7);const[,,e]=t.split("|"),o=`--wp--preset--color--${e}`;return c(o)?`var(${o})`:null}function a(t,e,o=null,n=""){return o?`${t}${n} { ${e}: ${o}; }\n`:""}function s(t,e,o,n){return a(t,"background-color",e)+a(t,"color",o)+a(t,"background-color",n,":hover")+a(t,"background-color",n,":focus")}const{apiFetch:d}=window.wp;!function(){const{actions:t,callbacks:e}=(0,o.store)("activitypub/follow-me",{actions:{openModal(t){const n=(0,o.getContext)();n.modal.isOpen=!0,n.modal.isCompact?setTimeout(e.positionModal,0):setTimeout((()=>{const t=document.getElementById(n.blockId);if(t){const o=t.querySelector(".activitypub-modal__frame");o&&e.trapFocus(o)}}),50),"function"==typeof e.onModalOpen&&e.onModalOpen(t)},closeModal(t){const n=(0,o.getContext)();n.modal.isOpen=!1;const r=(0,o.getElement)();if("actions.toggleModal"===r.ref.dataset["wpOn-Click"])r.ref.focus();else{const t=document.getElementById(n.blockId);if(t){const e=t.querySelector('[data-wp-on--click="actions.toggleModal"], [data-wp-on-async--click="actions.toggleModal"]');e&&e.focus()}}"function"==typeof e.onModalClose&&e.onModalClose(t)},toggleModal(e){const{modal:n}=(0,o.getContext)();n.isOpen?t.closeModal(e):t.openModal(e)}},callbacks:{_abortController:null,handleModalEffects(){const{modal:t}=(0,o.getContext)();if(t.isOpen&&!t.isCompact?document.body.classList.add("modal-open"):document.body.classList.remove("modal-open"),e._abortController&&(e._abortController.abort(),e._abortController=null),t.isOpen){e._abortController=new AbortController;const{signal:t}=e._abortController;document.addEventListener("keydown",e.documentKeydown,{signal:t}),document.addEventListener("click",e.documentClick,{signal:t})}},documentKeydown(e){const{modal:n}=(0,o.getContext)();n.isOpen&&"Escape"===e.key&&t.closeModal()},documentClick(e){const{blockId:n,modal:r}=(0,o.getContext)();if(!r.isOpen)return;const l=document.getElementById(n);if(!l)return;const c=l.querySelector('.wp-element-button[data-wp-on--click="actions.toggleModal"]');if(c&&(c===e.target||c.contains(e.target)))return;const i=l.querySelector(".activitypub-modal__frame");i&&!i.contains(e.target)&&t.closeModal()},positionModal(){const{blockId:t}=(0,o.getContext)(),e=document.getElementById(t);if(!e)return;const n=e.querySelector(".activitypub-modal__overlay");if(!n)return;n.style.top="",n.style.left="",n.style.right="",n.style.bottom="";const r=(0,o.getElement)().ref.getBoundingClientRect(),l=window.innerWidth,c=e.getBoundingClientRect();let i={top:r.bottom-c.top+8+"px",left:r.left-c.left-2+"px"};l-r.right<250&&(i.left="auto",i.right=c.right-r.right+"px"),Object.assign(n.style,i)},trapFocus(t){const e=t.querySelectorAll('a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input[type="text"]:not([disabled]):not([readonly]), input[type="radio"]:not([disabled]), input[type="checkbox"]:not([disabled]), select:not([disabled])'),o=e[0],n=e[e.length-1];o&&o.classList.contains("activitypub-modal__close")&&e.length>1?e[1].focus():o.focus(),t.addEventListener("keydown",(function(t){"Tab"!==t.key&&9!==t.keyCode||(t.shiftKey?document.activeElement===o&&(n.focus(),t.preventDefault()):document.activeElement===n&&(o.focus(),t.preventDefault()))}))}}})}();const{actions:u,callbacks:p,state:m}=(0,o.store)("activitypub/follow-me",{actions:{copyToClipboard(){const t=(0,o.getContext)();navigator.clipboard.writeText(t.webfinger).then((()=>{t.copyButtonText=m.i18n.copied,setTimeout((()=>{t.copyButtonText=m.i18n.copy}),1e3)}),(t=>{console.error("Could not copy text: ",t)}))},updateRemoteProfile(t){const e=(0,o.getContext)();e.remoteProfile=t.target.value,e.isError=!1,e.errorMessage=""},onKeydown(t){"A"!==(0,o.getElement)().ref.tagName||"Enter"!==t.key&&" "!==t.key||(t.preventDefault(),u.toggleModal(t))},handleKeyDown(t){"Enter"===t.key&&(t.preventDefault(),u.submitRemoteProfile())},submitRemoteProfile:function*(){const t=(0,o.getContext)(),{namespace:e}=m,n=t.remoteProfile.trim();if(!n)return t.isError=!0,void(t.errorMessage=m.i18n.emptyProfileError);if(!p.isHandle(n))return t.isError=!0,void(t.errorMessage=m.i18n.invalidProfileError);t.isLoading=!0,t.isError=!1;const r=`/${e}/actors/${t.userId}/remote-follow?resource=${encodeURIComponent(n)}`;try{const e=yield d({path:r});t.isLoading=!1,window.open(e.url,"_blank"),u.closeModal(new Event("click"))}catch(e){console.error("Error submitting profile:",e),t.isLoading=!1,t.isError=!0,t.errorMessage=e.message||m.i18n.genericError}}},callbacks:{initButtonStyles:()=>{const{buttonStyle:t,backgroundColor:e,blockId:n}=(0,o.getContext)();if(n&&t){const o=document.createElement("style"),l=`#${n}`;o.textContent=function(t,e,o){const n=`${t} .wp-block-button__link`,r=function(t){if("string"==typeof t){const e=`--wp--preset--color--${t}`;return c(e)?`var(${e})`:null}return t?.color?.background||null}(o)||e?.color?.background;return s(n,i(e?.elements?.link?.color?.text),r,i(e?.elements?.link?.[":hover"]?.color?.text))}(l,t,e),document.head.appendChild(o);const a=document.createElement("style");a.textContent=(r=t,s(".activitypub-dialog__button-group .wp-block-button",i(r?.elements?.link?.color?.text)||"#111","#fff",i(r?.elements?.link?.[":hover"]?.color?.text)||"#333")),document.head.appendChild(a)}var r},isHandle(t){const e=t.replace(/^@/,"").split("@");return 2===e.length&&p.isUrl(`https://${e[1]}`)},isUrl(t){try{return new URL(t),!0}catch(t){return!1}},onModalClose(){(0,o.getContext)().isError=!1}}}); \ No newline at end of file diff --git a/build/followers/block.json b/build/blocks/followers/block.json similarity index 71% rename from build/followers/block.json rename to build/blocks/followers/block.json index 078b82f30..929cf5855 100644 --- a/build/followers/block.json +++ b/build/blocks/followers/block.json @@ -2,23 +2,20 @@ "$schema": "https://schemas.wp.org/trunk/block.json", "name": "activitypub/followers", "apiVersion": 3, - "version": "1.0.0", + "version": "2.0.1", "title": "Fediverse Followers", "category": "widgets", "description": "Display your followers from the Fediverse on your website.", "textdomain": "activitypub", "icon": "groups", "supports": { - "html": false + "html": false, + "interactivity": true }, "attributes": { - "title": { - "type": "string", - "default": "Fediverse Followers" - }, "selectedUser": { "type": "string", - "default": "site" + "default": "blog" }, "per_page": { "type": "number", @@ -40,12 +37,12 @@ "styles": [ { "name": "default", - "label": "No Lines", + "label": "Default", "isDefault": true }, { - "name": "with-lines", - "label": "Lines" + "name": "card", + "label": "Card" }, { "name": "compact", @@ -53,9 +50,11 @@ } ], "editorScript": "file:./index.js", - "viewScript": "file:./view.js", + "editorStyle": "file:./index.css", + "viewScriptModule": "file:./view.js", + "viewScript": "wp-api-fetch", "style": [ - "file:./style-view.css", - "wp-block-query-pagination" - ] + "file:./style-index.css" + ], + "render": "file:./render.php" } \ No newline at end of file diff --git a/build/followers/index.asset.php b/build/blocks/followers/index.asset.php similarity index 81% rename from build/followers/index.asset.php rename to build/blocks/followers/index.asset.php index 02a90ee07..1c4d95a5e 100644 --- a/build/followers/index.asset.php +++ b/build/blocks/followers/index.asset.php @@ -1 +1 @@ - array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-core-data', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives', 'wp-url'), 'version' => '0090f7363e3c09edb5b6'); + array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-core-data', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives', 'wp-url'), 'version' => '41a177c9fbbe060ac05f'); diff --git a/build/blocks/followers/index.js b/build/blocks/followers/index.js new file mode 100644 index 000000000..1b644db5f --- /dev/null +++ b/build/blocks/followers/index.js @@ -0,0 +1,2 @@ +(()=>{"use strict";var e,t={20:(e,t,r)=>{var a=r(609),l=Symbol.for("react.element"),o=(Symbol.for("react.fragment"),Object.prototype.hasOwnProperty),n=a.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,s={key:!0,ref:!0,__self:!0,__source:!0};t.jsx=function(e,t,r){var a,i={},c=null,p=null;for(a in void 0!==r&&(c=""+r),void 0!==t.key&&(c=""+t.key),void 0!==t.ref&&(p=t.ref),t)o.call(t,a)&&!s.hasOwnProperty(a)&&(i[a]=t[a]);if(e&&e.defaultProps)for(a in t=e.defaultProps)void 0===i[a]&&(i[a]=t[a]);return{$$typeof:l,type:e,key:c,ref:p,props:i,_owner:n.current}}},309:(e,t,r)=>{const a=window.wp.blocks,l=window.wp.primitives;var o=r(848);const n=(0,o.jsx)(l.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,o.jsx)(l.Path,{d:"M15.5 9.5a1 1 0 100-2 1 1 0 000 2zm0 1.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5zm-2.25 6v-2a2.75 2.75 0 00-2.75-2.75h-4A2.75 2.75 0 003.75 15v2h1.5v-2c0-.69.56-1.25 1.25-1.25h4c.69 0 1.25.56 1.25 1.25v2h1.5zm7-2v2h-1.5v-2c0-.69-.56-1.25-1.25-1.25H15v-1.5h2.5A2.75 2.75 0 0120.25 15zM9.5 8.5a1 1 0 11-2 0 1 1 0 012 0zm1.5 0a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z",fillRule:"evenodd"})}),s=[{attributes:{title:{type:"string",default:"Fediverse Followers"},selectedUser:{type:"string",default:"blog"},per_page:{type:"number",default:10},order:{type:"string",default:"desc",enum:["asc","desc"]}},supports:{html:!1},isEligible:({title:e})=>!!e,migrate:({title:e,...t})=>[t,[(0,a.createBlock)("core/heading",{content:e,level:3})]]}];var i=r(609);const c=window.wp.apiFetch;var p=r.n(c);const u=window.wp.components,d=window.wp.blockEditor,v=window.wp.coreData,m=window.wp.data,f=window.wp.element,w=window.wp.url,g=window.wp.i18n;function h(){return window._activityPubOptions||{}}function b({name:e}){const{enabled:t}=h(),r=t?.blog?"":(0,g.__)("It will be empty in other non-author contexts.","activitypub"),a=(0,g.sprintf)(/* translators: %1$s: block name, %2$s: extra information for non-author context */ /* translators: %1$s: block name, %2$s: extra information for non-author context */ +(0,g.__)("This %1$s block will adapt to the page it is on, displaying the user profile associated with a post author (in a loop) or a user archive. %2$s","activitypub"),e,r).trim();return(0,i.createElement)(u.Card,null,(0,i.createElement)(u.CardBody,null,(0,f.createInterpolateElement)(a,{strong:(0,i.createElement)("strong",null)})))}function _({selectedUser:e,per_page:t,order:r,page:a,setPage:l,followerData:o=!1}){const n="blog"===e?0:e,[s,c]=(0,f.useState)([]),[u,d]=(0,f.useState)(0),[v,m]=(0,f.useState)(0),[b,_]=(0,f.useState)(1),x=a||b,k=l||_,N=(e,r)=>{c(e),m(r),d(Math.ceil(r/t))};return(0,f.useEffect)((()=>{if(o&&1===x)return N(o.followers,o.total);const e=function(e,t,r,a){const{namespace:l}=h(),o=`/${l}/actors/${e}/followers`,n={per_page:t,order:r,page:a,context:"full"};return(0,w.addQueryArgs)(o,n)}(n,t,r,x);p()({path:e}).then((({orderedItems:e,totalItems:t})=>N(e,t))).catch((()=>N([],0)))}),[n,t,r,x,o]),(0,i.createElement)("div",{className:"followers-container"},s.length?(0,i.createElement)("ul",{className:"followers-list"},s.map((e=>(0,i.createElement)("li",{key:e.url,className:"follower-item"},(0,i.createElement)(E,{...e}))))):(0,i.createElement)("p",{className:"followers-placeholder"},(0,g.__)("No followers found.","activitypub")),(0,i.createElement)(y,{page:x,pages:u,setPage:k}))}function y({page:e,pages:t,setPage:r}){if(t<=1)return null;const a=e<=1,l=e>=t;return(0,i.createElement)("nav",{className:"followers-pagination",role:"navigation"},(0,i.createElement)("h1",{className:"screen-reader-text"},(0,g.__)("Follower navigation","activitypub")),(0,i.createElement)("a",{className:"pagination-previous","aria-disabled":a,"aria-label":(0,g.__)("Previous page","activitypub"),onClick:t=>{t.preventDefault(),r(e-1)}},(0,g.__)("Previous","activitypub")),(0,i.createElement)("div",{className:"pagination-info"},`${e} / ${t}`),(0,i.createElement)("a",{className:"pagination-next","aria-disabled":l,"aria-label":(0,g.__)("Next page","activitypub"),onClick:t=>{t.preventDefault(),r(e+1)}},(0,g.__)("Next","activitypub")))}function E({name:e,icon:t,url:r,preferredUsername:a}){const l=`@${a}`,{defaultAvatarUrl:o}=h(),n=t.url||o;return(0,i.createElement)("a",{className:"follower-link",href:r,title:l,onClick:e=>e.preventDefault()},(0,i.createElement)("img",{width:"48",height:"48",src:n,className:"follower-avatar",alt:e,onError:e=>{e.target.src=o}}),(0,i.createElement)("div",{className:"follower-info"},(0,i.createElement)("span",{className:"follower-name"},e),(0,i.createElement)("span",{className:"follower-username"},l)),(0,i.createElement)("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",width:"24",height:"24",className:"external-link-icon","aria-hidden":"true",focusable:"false",fill:"currentColor"},(0,i.createElement)("path",{d:"M18.2 17c0 .7-.6 1.2-1.2 1.2H7c-.7 0-1.2-.6-1.2-1.2V7c0-.7.6-1.2 1.2-1.2h3.2V4.2H7C5.5 4.2 4.2 5.5 4.2 7v10c0 1.5 1.2 2.8 2.8 2.8h10c1.5 0 2.8-1.2 2.8-2.8v-3.6h-1.5V17zM14.9 3v1.5h3.7l-6.4 6.4 1.1 1.1 6.4-6.4v3.7h1.5V3h-6.3z"})))}const x=JSON.parse('{"$schema":"https://schemas.wp.org/trunk/block.json","name":"activitypub/followers","apiVersion":3,"version":"2.0.1","title":"Fediverse Followers","category":"widgets","description":"Display your followers from the Fediverse on your website.","textdomain":"activitypub","icon":"groups","supports":{"html":false,"interactivity":true},"attributes":{"selectedUser":{"type":"string","default":"blog"},"per_page":{"type":"number","default":10},"order":{"type":"string","default":"desc","enum":["asc","desc"]}},"usesContext":["postType","postId"],"styles":[{"name":"default","label":"Default","isDefault":true},{"name":"card","label":"Card"},{"name":"compact","label":"Compact"}],"editorScript":"file:./index.js","editorStyle":"file:./index.css","viewScriptModule":"file:./view.js","viewScript":"wp-api-fetch","style":["file:./style-index.css"],"render":"file:./render.php"}');(0,a.registerBlockType)(x,{deprecated:s,edit:function({attributes:e,setAttributes:t,context:{postType:r,postId:a}}){const{className:l="",order:o,per_page:n,selectedUser:s}=e,c=(0,d.useBlockProps)(),[p,w]=(0,f.useState)(1),y=[{label:(0,g.__)("New to old","activitypub"),value:"desc"},{label:(0,g.__)("Old to new","activitypub"),value:"asc"}],E=function({withInherit:e=!1}){const{enabled:t}=h(),r=t?.users?(0,m.useSelect)((e=>e("core").getUsers({capabilities:"activitypub"})),[]):[];return(0,f.useMemo)((()=>{if(!r)return[];const a=[];return t?.blog&&a.push({label:(0,g.__)("Blog","activitypub"),value:"blog"}),e&&t?.users&&a.push({label:(0,g.__)("Dynamic User","activitypub"),value:"inherit"}),r.reduce(((e,t)=>(e.push({label:t.name,value:`${t.id}`}),e)),a)}),[r])}({withInherit:!0}),x=e=>r=>{w(1),t({[e]:r})},k=(0,m.useSelect)((e=>{const{getEditedEntityRecord:t}=e(v.store),l=t("postType",r,a)?.author;return null!=l?l:null}),[r,a]);(0,f.useEffect)((()=>{E.length&&(E.find((({value:e})=>e===s))||t({selectedUser:E[0].value}))}),[s,E]);const N=[["core/heading",{level:3,placeholder:(0,g.__)("Fediverse Followers","activitypub"),content:(0,g.__)("Fediverse Followers","activitypub")}]];return(0,i.createElement)("div",{...c},(0,i.createElement)(d.InspectorControls,{key:"setting"},(0,i.createElement)(u.PanelBody,{title:(0,g.__)("Followers Options","activitypub")},E.length>1&&(0,i.createElement)(u.SelectControl,{label:(0,g.__)("Select User","activitypub"),value:s,options:E,onChange:x("selectedUser"),__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0}),(0,i.createElement)(u.SelectControl,{label:(0,g.__)("Sort","activitypub"),value:o,options:y,onChange:x("order"),__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0}),(0,i.createElement)(u.RangeControl,{label:(0,g.__)("Number of Followers","activitypub"),value:n,onChange:x("per_page"),min:1,max:10,__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0}))),(0,i.createElement)("div",{className:"wp-block-activitypub-followers "+l},(0,i.createElement)(d.InnerBlocks,{template:N,allowedBlocks:["core/heading"],templateLock:"all",renderAppender:!1}),"inherit"===s?k?(0,i.createElement)(_,{...e,page:p,setPage:w,selectedUser:k}):(0,i.createElement)(b,{name:(0,g.__)("Followers","activitypub")}):(0,i.createElement)(_,{...e,page:p,setPage:w})))},save:function(){const e=d.useBlockProps.save(),t=d.useInnerBlocksProps.save(e);return(0,i.createElement)("div",{...t})},icon:n})},609:e=>{e.exports=window.React},848:(e,t,r)=>{e.exports=r(20)}},r={};function a(e){var l=r[e];if(void 0!==l)return l.exports;var o=r[e]={exports:{}};return t[e](o,o.exports,a),o.exports}a.m=t,e=[],a.O=(t,r,l,o)=>{if(!r){var n=1/0;for(p=0;p=o)&&Object.keys(a.O).every((e=>a.O[e](r[i])))?r.splice(i--,1):(s=!1,o0&&e[p-1][2]>o;p--)e[p]=e[p-1];e[p]=[r,l,o]},a.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return a.d(t,{a:t}),t},a.d=(e,t)=>{for(var r in t)a.o(t,r)&&!a.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},a.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e={488:0,424:0};a.O.j=t=>0===e[t];var t=(t,r)=>{var l,o,[n,s,i]=r,c=0;if(n.some((t=>0!==e[t]))){for(l in s)a.o(s,l)&&(a.m[l]=s[l]);if(i)var p=i(a)}for(t&&t(r);ca(309)));l=a.O(l)})(); \ No newline at end of file diff --git a/build/blocks/followers/render.php b/build/blocks/followers/render.php new file mode 100644 index 000000000..f9493c048 --- /dev/null +++ b/build/blocks/followers/render.php @@ -0,0 +1,168 @@ +' . esc_html( $_title ) . ''; + unset( $attributes['title'], $attributes['className'] ); +} else { + $content = implode( PHP_EOL, wp_list_pluck( $block->parsed_block['innerBlocks'], 'innerHTML' ) ); +} + +$user_id = Blocks::get_user_id( $attributes['selectedUser'] ); +if ( is_null( $user_id ) ) { + return ''; +} + +$user = Actors::get_by_id( $user_id ); +if ( is_wp_error( $user ) ) { + return ''; +} + +$_per_page = absint( $attributes['per_page'] ); +$follower_data = Followers::get_followers_with_count( $user_id, $_per_page ); + +// Prepare Followers data for the Interactivity API context. +$followers = array_map( + /** + * Prepare follower data for the Interactivity API context. + * + * @param WP_Post $follower Follower object. + * + * @return array + */ + function ( $follower ) { + $actor = Actors::get_actor( $follower ); + $username = $actor->get_preferred_username(); + + return array( + 'handle' => '@' . $username, + 'icon' => $actor->get_icon(), + 'name' => $actor->get_name() ?? $username, + 'url' => object_to_uri( $actor->get_url() ) ?? $actor->get_id(), + ); + }, + $follower_data['followers'] +); + +// Set up the Interactivity API state. +wp_interactivity_state( + 'activitypub/followers', + array( + 'defaultAvatarUrl' => ACTIVITYPUB_PLUGIN_URL . 'assets/img/mp.jpg', + 'namespace' => ACTIVITYPUB_REST_NAMESPACE, + ) +); + +// Set initial context data. +$context = array( + 'followers' => $followers, + 'isLoading' => false, + 'order' => $attributes['order'], + 'page' => 1, + 'pages' => ceil( $follower_data['total'] / $_per_page ), + 'per_page' => $_per_page, + 'total' => $follower_data['total'], + 'userId' => $user_id, +); + +// Get block wrapper attributes with the data-wp-interactive attribute. +$wrapper_attributes = get_block_wrapper_attributes( + array( + 'id' => wp_unique_id( 'activitypub-followers-block-' ), + 'data-wp-interactive' => 'activitypub/followers', + 'data-wp-context' => wp_json_encode( $context, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ), + ) +); +?> + +
> + + +
+
    + +
+ + $_per_page ) : ?> + + +
+
+
+ +
+
diff --git a/build/blocks/followers/style-index-rtl.css b/build/blocks/followers/style-index-rtl.css new file mode 100644 index 000000000..3173399f4 --- /dev/null +++ b/build/blocks/followers/style-index-rtl.css @@ -0,0 +1 @@ +button{border:none}.wp-block-activitypub-followers{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif;margin:16px 0}.wp-block-activitypub-followers .wp-block-heading{border-bottom:1px solid;margin:0 0 16px;padding:0 0 8px}.wp-block-activitypub-followers .followers-pagination,.wp-block-activitypub-followers .wp-block-heading{border-color:var(--wp--preset--color--foreground,var(--wp--preset--color--primary,#e0e0e0))}.wp-block-activitypub-followers .followers-container{position:relative}.wp-block-activitypub-followers .followers-container .followers-list{list-style:none;margin:0;padding:0}.wp-block-activitypub-followers .followers-container .follower-item{margin:0 0 8px}.wp-block-activitypub-followers .followers-container .follower-item:last-child{margin-bottom:0}.wp-block-activitypub-followers .followers-container .follower-link{align-items:center;border:none;border-radius:8px;box-shadow:none;display:flex;padding:8px;transition:background-color .2s ease}.wp-block-activitypub-followers .followers-container .follower-link:focus,.wp-block-activitypub-followers .followers-container .follower-link:hover{background-color:var(--wp--preset--color--subtle-background,var(--wp--preset--color--accent-2,var(--wp--preset--color--tertiary,var(--wp--preset--color--secondary,#f0f0f0))));box-shadow:none;outline:none}.wp-block-activitypub-followers .followers-container .follower-link:focus .external-link-icon,.wp-block-activitypub-followers .followers-container .follower-link:hover .external-link-icon{opacity:1}.wp-block-activitypub-followers .followers-container .follower-avatar{border:1px solid #e0e0e0;border-radius:50%;height:48px;margin-left:16px;-o-object-fit:cover;object-fit:cover;width:48px}.wp-block-activitypub-followers .followers-container .follower-info{display:flex;flex:1;flex-direction:column;line-height:1.3;overflow:hidden}.wp-block-activitypub-followers .followers-container .follower-name{font-weight:600;margin-bottom:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.wp-block-activitypub-followers .followers-container .follower-username{color:var(--wp--preset--color--very-dark-gray,#666);font-size:90%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.wp-block-activitypub-followers .followers-container .external-link-icon{height:16px;margin-right:8px;transition:opacity .2s ease;width:16px}.wp-block-activitypub-followers .followers-container .followers-pagination{align-items:center;border-top-style:solid;border-top-width:1px;display:grid;grid-template-columns:1fr auto 1fr;margin-top:16px;padding-top:8px!important}.wp-block-activitypub-followers .followers-container .followers-pagination .pagination-info{color:var(--wp--preset--color--very-dark-gray,#666);font-size:90%;justify-self:center}.wp-block-activitypub-followers .followers-container .followers-pagination .pagination-next,.wp-block-activitypub-followers .followers-container .followers-pagination .pagination-previous{border:none;box-shadow:none;cursor:pointer;display:inline-block;font-size:90%;min-width:60px;padding:8px 0}.wp-block-activitypub-followers .followers-container .followers-pagination .pagination-next[hidden],.wp-block-activitypub-followers .followers-container .followers-pagination .pagination-previous[hidden]{display:none!important}.wp-block-activitypub-followers .followers-container .followers-pagination .pagination-next[aria-disabled=true],.wp-block-activitypub-followers .followers-container .followers-pagination .pagination-previous[aria-disabled=true]{cursor:not-allowed;opacity:.3;pointer-events:none;text-decoration:none}.wp-block-activitypub-followers .followers-container .followers-pagination .pagination-previous{justify-self:start;padding-left:8px}.wp-block-activitypub-followers .followers-container .followers-pagination .pagination-previous:before{content:"←"}.wp-block-activitypub-followers .followers-container .followers-pagination .pagination-next{justify-self:end;padding-right:8px;text-align:left}.wp-block-activitypub-followers .followers-container .followers-pagination .pagination-next:after{content:"→"}@media(max-width:480px){.wp-block-activitypub-followers .followers-container .followers-pagination{grid-template-columns:1fr 1fr}.wp-block-activitypub-followers .followers-container .followers-pagination .pagination-info{display:none}.wp-block-activitypub-followers .followers-container .followers-pagination .pagination-next,.wp-block-activitypub-followers .followers-container .followers-pagination .pagination-previous{align-items:center;font-size:100%;min-height:44px}}.wp-block-activitypub-followers .followers-container .followers-loading{align-items:center;background-color:hsla(0,0%,100%,.5);border-radius:8px;bottom:0;display:flex;justify-content:center;right:0;position:absolute;left:0;top:0}.wp-block-activitypub-followers .followers-container .followers-loading[aria-hidden=true]{display:none}.wp-block-activitypub-followers .followers-container .loading-spinner{animation:spin 1s ease-in-out infinite;border:3px solid color-mix(in srgb,var(--wp--preset--color--primary,#0073aa) 30%,transparent);border-radius:50%;border-top-color:var(--wp--preset--color--primary,#0073aa);height:40px;width:40px}@keyframes spin{to{transform:rotate(-1turn)}}.wp-block-activitypub-followers.is-style-card:not(.block-editor-block-list__block){background-color:var(--wp--preset--color--white,#fff);border-radius:8px;box-shadow:0 2px 8px rgba(0,0,0,.1);box-sizing:border-box;padding:24px}@media(max-width:480px){.wp-block-activitypub-followers.is-style-card:not(.block-editor-block-list__block){margin-right:-12px;margin-left:-12px}}.wp-block-activitypub-followers.is-style-card:not(.block-editor-block-list__block) .wp-block-heading{border-bottom:none;margin-bottom:16px;text-align:center}.wp-block-activitypub-followers.is-style-card:not(.block-editor-block-list__block) .follower-link{border:1px solid #e0e0e0;margin-bottom:8px}.wp-block-activitypub-followers.is-style-card:not(.block-editor-block-list__block) .follower-link:focus,.wp-block-activitypub-followers.is-style-card:not(.block-editor-block-list__block) .follower-link:hover{border-color:#c7c7c7}.wp-block-activitypub-followers.is-style-card:not(.block-editor-block-list__block) .followers-pagination{border:none;padding-bottom:0!important}.wp-block-activitypub-followers.is-style-compact .follower-link{padding:4px}.wp-block-activitypub-followers.is-style-compact .follower-avatar{height:36px;margin-left:8px;width:36px}.wp-block-activitypub-followers.is-style-compact .follower-name{font-size:90%}.wp-block-activitypub-followers.is-style-compact .follower-username{font-size:80%}.wp-block-activitypub-followers.is-style-compact .followers-pagination{margin-top:8px;padding-top:4px}.wp-block-activitypub-followers.is-style-compact .followers-pagination .pagination-next,.wp-block-activitypub-followers.is-style-compact .followers-pagination .pagination-previous{font-size:80%;padding-bottom:4px;padding-top:4px}@media(max-width:480px){.wp-block-activitypub-followers.is-style-compact .followers-pagination .pagination-next,.wp-block-activitypub-followers.is-style-compact .followers-pagination .pagination-previous{font-size:100%}}.wp-block-activitypub-followers.is-style-compact .followers-pagination .pagination-info{font-size:80%} diff --git a/build/blocks/followers/style-index.css b/build/blocks/followers/style-index.css new file mode 100644 index 000000000..f1afc8a64 --- /dev/null +++ b/build/blocks/followers/style-index.css @@ -0,0 +1 @@ +button{border:none}.wp-block-activitypub-followers{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif;margin:16px 0}.wp-block-activitypub-followers .wp-block-heading{border-bottom:1px solid;margin:0 0 16px;padding:0 0 8px}.wp-block-activitypub-followers .followers-pagination,.wp-block-activitypub-followers .wp-block-heading{border-color:var(--wp--preset--color--foreground,var(--wp--preset--color--primary,#e0e0e0))}.wp-block-activitypub-followers .followers-container{position:relative}.wp-block-activitypub-followers .followers-container .followers-list{list-style:none;margin:0;padding:0}.wp-block-activitypub-followers .followers-container .follower-item{margin:0 0 8px}.wp-block-activitypub-followers .followers-container .follower-item:last-child{margin-bottom:0}.wp-block-activitypub-followers .followers-container .follower-link{align-items:center;border:none;border-radius:8px;box-shadow:none;display:flex;padding:8px;transition:background-color .2s ease}.wp-block-activitypub-followers .followers-container .follower-link:focus,.wp-block-activitypub-followers .followers-container .follower-link:hover{background-color:var(--wp--preset--color--subtle-background,var(--wp--preset--color--accent-2,var(--wp--preset--color--tertiary,var(--wp--preset--color--secondary,#f0f0f0))));box-shadow:none;outline:none}.wp-block-activitypub-followers .followers-container .follower-link:focus .external-link-icon,.wp-block-activitypub-followers .followers-container .follower-link:hover .external-link-icon{opacity:1}.wp-block-activitypub-followers .followers-container .follower-avatar{border:1px solid #e0e0e0;border-radius:50%;height:48px;margin-right:16px;-o-object-fit:cover;object-fit:cover;width:48px}.wp-block-activitypub-followers .followers-container .follower-info{display:flex;flex:1;flex-direction:column;line-height:1.3;overflow:hidden}.wp-block-activitypub-followers .followers-container .follower-name{font-weight:600;margin-bottom:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.wp-block-activitypub-followers .followers-container .follower-username{color:var(--wp--preset--color--very-dark-gray,#666);font-size:90%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.wp-block-activitypub-followers .followers-container .external-link-icon{height:16px;margin-left:8px;transition:opacity .2s ease;width:16px}.wp-block-activitypub-followers .followers-container .followers-pagination{align-items:center;border-top-style:solid;border-top-width:1px;display:grid;grid-template-columns:1fr auto 1fr;margin-top:16px;padding-top:8px!important}.wp-block-activitypub-followers .followers-container .followers-pagination .pagination-info{color:var(--wp--preset--color--very-dark-gray,#666);font-size:90%;justify-self:center}.wp-block-activitypub-followers .followers-container .followers-pagination .pagination-next,.wp-block-activitypub-followers .followers-container .followers-pagination .pagination-previous{border:none;box-shadow:none;cursor:pointer;display:inline-block;font-size:90%;min-width:60px;padding:8px 0}.wp-block-activitypub-followers .followers-container .followers-pagination .pagination-next[hidden],.wp-block-activitypub-followers .followers-container .followers-pagination .pagination-previous[hidden]{display:none!important}.wp-block-activitypub-followers .followers-container .followers-pagination .pagination-next[aria-disabled=true],.wp-block-activitypub-followers .followers-container .followers-pagination .pagination-previous[aria-disabled=true]{cursor:not-allowed;opacity:.3;pointer-events:none;text-decoration:none}.wp-block-activitypub-followers .followers-container .followers-pagination .pagination-previous{justify-self:start;padding-right:8px}.wp-block-activitypub-followers .followers-container .followers-pagination .pagination-previous:before{content:"←"}.wp-block-activitypub-followers .followers-container .followers-pagination .pagination-next{justify-self:end;padding-left:8px;text-align:right}.wp-block-activitypub-followers .followers-container .followers-pagination .pagination-next:after{content:"→"}@media(max-width:480px){.wp-block-activitypub-followers .followers-container .followers-pagination{grid-template-columns:1fr 1fr}.wp-block-activitypub-followers .followers-container .followers-pagination .pagination-info{display:none}.wp-block-activitypub-followers .followers-container .followers-pagination .pagination-next,.wp-block-activitypub-followers .followers-container .followers-pagination .pagination-previous{align-items:center;font-size:100%;min-height:44px}}.wp-block-activitypub-followers .followers-container .followers-loading{align-items:center;background-color:hsla(0,0%,100%,.5);border-radius:8px;bottom:0;display:flex;justify-content:center;left:0;position:absolute;right:0;top:0}.wp-block-activitypub-followers .followers-container .followers-loading[aria-hidden=true]{display:none}.wp-block-activitypub-followers .followers-container .loading-spinner{animation:spin 1s ease-in-out infinite;border:3px solid color-mix(in srgb,var(--wp--preset--color--primary,#0073aa) 30%,transparent);border-radius:50%;border-top-color:var(--wp--preset--color--primary,#0073aa);height:40px;width:40px}@keyframes spin{to{transform:rotate(1turn)}}.wp-block-activitypub-followers.is-style-card:not(.block-editor-block-list__block){background-color:var(--wp--preset--color--white,#fff);border-radius:8px;box-shadow:0 2px 8px rgba(0,0,0,.1);box-sizing:border-box;padding:24px}@media(max-width:480px){.wp-block-activitypub-followers.is-style-card:not(.block-editor-block-list__block){margin-left:-12px;margin-right:-12px}}.wp-block-activitypub-followers.is-style-card:not(.block-editor-block-list__block) .wp-block-heading{border-bottom:none;margin-bottom:16px;text-align:center}.wp-block-activitypub-followers.is-style-card:not(.block-editor-block-list__block) .follower-link{border:1px solid #e0e0e0;margin-bottom:8px}.wp-block-activitypub-followers.is-style-card:not(.block-editor-block-list__block) .follower-link:focus,.wp-block-activitypub-followers.is-style-card:not(.block-editor-block-list__block) .follower-link:hover{border-color:#c7c7c7}.wp-block-activitypub-followers.is-style-card:not(.block-editor-block-list__block) .followers-pagination{border:none;padding-bottom:0!important}.wp-block-activitypub-followers.is-style-compact .follower-link{padding:4px}.wp-block-activitypub-followers.is-style-compact .follower-avatar{height:36px;margin-right:8px;width:36px}.wp-block-activitypub-followers.is-style-compact .follower-name{font-size:90%}.wp-block-activitypub-followers.is-style-compact .follower-username{font-size:80%}.wp-block-activitypub-followers.is-style-compact .followers-pagination{margin-top:8px;padding-top:4px}.wp-block-activitypub-followers.is-style-compact .followers-pagination .pagination-next,.wp-block-activitypub-followers.is-style-compact .followers-pagination .pagination-previous{font-size:80%;padding-bottom:4px;padding-top:4px}@media(max-width:480px){.wp-block-activitypub-followers.is-style-compact .followers-pagination .pagination-next,.wp-block-activitypub-followers.is-style-compact .followers-pagination .pagination-previous{font-size:100%}}.wp-block-activitypub-followers.is-style-compact .followers-pagination .pagination-info{font-size:80%} diff --git a/build/followers/style-view-rtl.css b/build/blocks/followers/style-view-rtl.css similarity index 100% rename from build/followers/style-view-rtl.css rename to build/blocks/followers/style-view-rtl.css diff --git a/build/followers/style-view.css b/build/blocks/followers/style-view.css similarity index 100% rename from build/followers/style-view.css rename to build/blocks/followers/style-view.css diff --git a/build/blocks/followers/view.asset.php b/build/blocks/followers/view.asset.php new file mode 100644 index 000000000..3859fc72c --- /dev/null +++ b/build/blocks/followers/view.asset.php @@ -0,0 +1 @@ + array('@wordpress/interactivity'), 'version' => '1ee127d6b44f5125a728', 'type' => 'module'); diff --git a/build/blocks/followers/view.js b/build/blocks/followers/view.js new file mode 100644 index 000000000..deb5ecd21 --- /dev/null +++ b/build/blocks/followers/view.js @@ -0,0 +1 @@ +import*as e from"@wordpress/interactivity";var t={d:(e,r)=>{for(var o in r)t.o(r,o)&&!t.o(e,o)&&Object.defineProperty(e,o,{enumerable:!0,get:r[o]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)};const r=(l={getContext:()=>e.getContext,store:()=>e.store},c={},t.d(c,l),c),{apiFetch:o,url:a}=window.wp,{actions:n,state:s}=(0,r.store)("activitypub/followers",{state:{get paginationText(){const{page:e,pages:t}=(0,r.getContext)();return`${e} / ${t}`},get disablePreviousLink(){const{page:e}=(0,r.getContext)();return e<=1},get disableNextLink(){const{page:e,pages:t}=(0,r.getContext)();return e>=t}},actions:{async fetchFollowers(){const e=(0,r.getContext)(),{userId:t,page:n,per_page:l,order:c}=e;e.isLoading=!0;try{const r=a.addQueryArgs(`/${s.namespace}/actors/${t}/followers`,{context:"full",per_page:l,order:c,page:n}),{orderedItems:g,totalItems:p}=await o({path:r});e.followers=g.map((e=>({handle:"@"+e.preferredUsername,icon:e.icon,name:e.name||e.preferredUsername,url:e.url||e.id}))),e.total=p,e.pages=Math.ceil(p/l)}catch(e){console.error("Error fetching followers:",e)}finally{e.isLoading=!1}},previousPage(e){e.preventDefault();const t=(0,r.getContext)();t.page>1&&(t.page--,n.fetchFollowers().catch((e=>{console.error("Error fetching followers:",e)})))},nextPage(e){e.preventDefault();const t=(0,r.getContext)();t.page{console.error("Error fetching followers:",e)})))}},callbacks:{setDefaultAvatar(e){e.target.src=s.defaultAvatarUrl}}});var l,c; \ No newline at end of file diff --git a/build/blocks/reactions/block.json b/build/blocks/reactions/block.json new file mode 100644 index 000000000..23bbea380 --- /dev/null +++ b/build/blocks/reactions/block.json @@ -0,0 +1,51 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "name": "activitypub/reactions", + "apiVersion": 3, + "version": "3.0.3", + "title": "Fediverse Reactions", + "category": "widgets", + "icon": "heart", + "description": "Display Fediverse likes and reposts", + "supports": { + "align": [ + "wide", + "full" + ], + "color": { + "gradients": true + }, + "__experimentalBorder": { + "radius": true, + "width": true, + "color": true, + "style": true + }, + "html": false, + "interactivity": true, + "layout": { + "default": { + "type": "constrained", + "orientation": "vertical", + "justifyContent": "center" + }, + "allowEditing": false + }, + "shadow": true, + "typography": { + "fontSize": true, + "__experimentalDefaultControls": { + "fontSize": true + } + } + }, + "blockHooks": { + "core/post-content": "after" + }, + "textdomain": "activitypub", + "editorScript": "file:./index.js", + "style": "file:./style-index.css", + "viewScriptModule": "file:./view.js", + "viewScript": "wp-api-fetch", + "render": "file:./render.php" +} \ No newline at end of file diff --git a/build/blocks/reactions/index.asset.php b/build/blocks/reactions/index.asset.php new file mode 100644 index 000000000..15b3bd542 --- /dev/null +++ b/build/blocks/reactions/index.asset.php @@ -0,0 +1 @@ + array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-element', 'wp-i18n'), 'version' => '66d2e9cbcff2fdd6bad4'); diff --git a/build/blocks/reactions/index.js b/build/blocks/reactions/index.js new file mode 100644 index 000000000..7cec4f28e --- /dev/null +++ b/build/blocks/reactions/index.js @@ -0,0 +1,3 @@ +(()=>{"use strict";var e,t={321:(e,t,r)=>{const a=window.wp.blocks,n=[{attributes:{title:{type:"string",default:"Fediverse reactions"}},supports:{html:!1,color:{gradients:!0,link:!0,__experimentalDefaultControls:{background:!0,text:!0,link:!0}},__experimentalBorder:{radius:!0,width:!0,color:!0,style:!0},typography:{fontSize:!0,__experimentalDefaultControls:{fontSize:!0}}},isEligible:({title:e})=>!!e,migrate:({title:e,...t})=>[t,[(0,a.createBlock)("core/heading",{content:e,level:6})]]}],l=window.React,i=window.wp.blockEditor,o=window.wp.i18n,s=window.wp.data,c=window.wp.element,u=window.wp.components,p=window.wp.apiFetch;var m=r.n(p);function d(){return window._activityPubOptions||{}}const f=({reactions:e})=>{const{defaultAvatarUrl:t}=d();return(0,l.createElement)("ul",{className:"reaction-avatars"},e.map(((e,r)=>{const a=["reaction-avatar"].filter(Boolean).join(" "),n=e.avatar||t;return(0,l.createElement)("li",{key:r},(0,l.createElement)("a",{href:e.url,target:"_blank",rel:"noopener noreferrer"},(0,l.createElement)("img",{src:n,alt:e.name,className:a,width:"32",height:"32",onError:e=>{e.target.src=t}})))})))},v=({reactions:e})=>{const{defaultAvatarUrl:t}=d();return(0,l.createElement)("ul",{className:"reactions-list"},e.map(((e,r)=>{const a=e.avatar||t;return(0,l.createElement)("li",{key:r,className:"reaction-item"},(0,l.createElement)("a",{href:e.url,className:"reaction-item",target:"_blank",rel:"noopener noreferrer"},(0,l.createElement)("img",{src:a,alt:e.name,width:"32",height:"32",onError:e=>{e.target.src=t}}),(0,l.createElement)("span",{className:"reaction-name"},e.name)))})))},h=({items:e,label:t})=>{const[r,a]=(0,c.useState)(!1),[n,i]=(0,c.useState)(null),o=(0,c.useRef)(null),s=e.slice(0,20);return(0,l.createElement)("div",{className:"reaction-group",ref:o},(0,l.createElement)(f,{reactions:s}),(0,l.createElement)(u.Button,{ref:i,className:"reaction-label is-link",onClick:()=>a(!r),"aria-expanded":r},t),r&&n&&(0,l.createElement)(u.Popover,{anchor:n,onClose:()=>a(!1)},(0,l.createElement)(v,{reactions:e})))};function w({postId:e=null,reactions:t=null,fallbackReactions:r=null}){const{namespace:a}=d(),[n,i]=(0,c.useState)(t),[o,s]=(0,c.useState)(!t),u=()=>{r&&i(r),s(!1)};return(0,c.useEffect)((()=>{if(t)return i(t),void s(!1);e&&"number"==typeof e?(s(!0),m()({path:`/${a}/posts/${e}/reactions`}).then((e=>{const t=Object.values(e).some((e=>e.items?.length>0));i(!t&&r?r:e),s(!1)})).catch(u)):u()}),[e,t,r,a]),o?null:n&&Object.values(n).some((e=>e.items?.length>0))?(0,l.createElement)(l.Fragment,null,Object.entries(n).map((([e,t])=>t.items?.length?(0,l.createElement)(h,{key:e,items:t.items,label:t.label}):null))):null}const g=(e,t,r,a)=>Array.from({length:e},((e,n)=>({name:`${t} ${n+1}`,url:"#",avatar:`data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Ccircle cx='32' cy='32' r='32' fill='%23${a[n%a.length]}'/%3E%3Ctext x='32' y='38' font-family='sans-serif' font-size='24' fill='white' text-anchor='middle'%3E${String.fromCharCode(r+n)}%3C/text%3E%3C/svg%3E`}))),b=["FF6B6B","4ECDC4","45B7D1","96CEB4","D4A5A5","9B59B6","3498DB","E67E22"],y={likes:{label:(0,o.sprintf)(/* translators: %d: Number of likes */ /* translators: %d: Number of likes */ +(0,o._x)("%d likes","number of likes","activitypub"),9),items:g(9,"User",65,b)},reposts:{label:(0,o.sprintf)(/* translators: %d: Number of reposts */ /* translators: %d: Number of reposts */ +(0,o._x)("%d reposts","number of reposts","activitypub"),6),items:g(6,"Reposter",82,b)}},E=JSON.parse('{"$schema":"https://schemas.wp.org/trunk/block.json","name":"activitypub/reactions","apiVersion":3,"version":"3.0.3","title":"Fediverse Reactions","category":"widgets","icon":"heart","description":"Display Fediverse likes and reposts","supports":{"align":["wide","full"],"color":{"gradients":true},"__experimentalBorder":{"radius":true,"width":true,"color":true,"style":true},"html":false,"interactivity":true,"layout":{"default":{"type":"constrained","orientation":"vertical","justifyContent":"center"},"allowEditing":false},"shadow":true,"typography":{"fontSize":true,"__experimentalDefaultControls":{"fontSize":true}}},"blockHooks":{"core/post-content":"after"},"textdomain":"activitypub","editorScript":"file:./index.js","style":"file:./style-index.css","viewScriptModule":"file:./view.js","viewScript":"wp-api-fetch","render":"file:./render.php"}');(0,a.registerBlockType)(E,{deprecated:n,edit:function({__unstableLayoutClassNames:e}){const t=(0,i.useBlockProps)({className:e}),{getCurrentPostId:r}=(0,s.select)("core/editor"),a=[["core/heading",{level:6,placeholder:(0,o.__)("Fediverse Reactions","activitypub"),content:(0,o.__)("Fediverse Reactions","activitypub")}]];return(0,l.createElement)("div",{...t},(0,l.createElement)(i.InnerBlocks,{template:a,allowedBlocks:["core/heading"],templateLock:"all",renderAppender:!1}),(0,l.createElement)(w,{postId:r(),fallbackReactions:y}))},save:function(){return(0,l.createElement)("div",{...i.useBlockProps.save()},(0,l.createElement)(i.InnerBlocks.Content,null))}})}},r={};function a(e){var n=r[e];if(void 0!==n)return n.exports;var l=r[e]={exports:{}};return t[e](l,l.exports,a),l.exports}a.m=t,e=[],a.O=(t,r,n,l)=>{if(!r){var i=1/0;for(u=0;u=l)&&Object.keys(a.O).every((e=>a.O[e](r[s])))?r.splice(s--,1):(o=!1,l0&&e[u-1][2]>l;u--)e[u]=e[u-1];e[u]=[r,n,l]},a.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return a.d(t,{a:t}),t},a.d=(e,t)=>{for(var r in t)a.o(t,r)&&!a.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},a.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e={229:0,345:0};a.O.j=t=>0===e[t];var t=(t,r)=>{var n,l,[i,o,s]=r,c=0;if(i.some((t=>0!==e[t]))){for(n in o)a.o(o,n)&&(a.m[n]=o[n]);if(s)var u=s(a)}for(t&&t(r);ca(321)));n=a.O(n)})(); \ No newline at end of file diff --git a/build/blocks/reactions/render.php b/build/blocks/reactions/render.php new file mode 100644 index 000000000..67992c7e0 --- /dev/null +++ b/build/blocks/reactions/render.php @@ -0,0 +1,218 @@ + null ) ); + +/* @var \WP_Block $block Current block. */ +$block = $block ?? ''; + +/* @var string $content Block content. */ +$content = $content ?? ''; + +if ( empty( $content ) ) { + // Fallback for v1.0.0 blocks. + $_title = $attributes['title'] ?? __( 'Fediverse Reactions', 'activitypub' ); + $content = '
' . esc_html( $_title ) . '
'; + unset( $attributes['title'], $attributes['className'] ); +} else { + $content = implode( PHP_EOL, wp_list_pluck( $block->parsed_block['innerBlocks'], 'innerHTML' ) ); +} + +// Get the Post ID from attributes or use the current post. +$_post_id = $attributes['postId'] ?? get_the_ID(); + +// Generate a unique ID for the block. +$block_id = 'activitypub-reactions-block-' . wp_unique_id(); + +$reactions = array(); + +foreach ( Comment::get_comment_types() as $_type => $type_object ) { + $_comments = get_comments( + array( + 'post_id' => $_post_id, + 'type' => $_type, + 'status' => 'approve', + 'parent' => 0, + ) + ); + + if ( empty( $_comments ) ) { + continue; + } + + $count = count( $_comments ); + // phpcs:disable WordPress.WP.I18n + $label = sprintf( + _n( + $type_object['count_single'], + $type_object['count_plural'], + $count, + 'activitypub' + ), + number_format_i18n( $count ) + ); + // phpcs:enable WordPress.WP.I18n + + $reactions[ $_type ] = array( + 'label' => $label, + 'count' => $count, + 'items' => array_map( + function ( $comment ) { + return array( + 'name' => html_entity_decode( $comment->comment_author ), + 'url' => $comment->comment_author_url, + 'avatar' => get_avatar_url( $comment ), + ); + }, + $_comments + ), + ); +} + +if ( empty( $reactions ) ) { + echo ''; + return; +} + +// Set up the Interactivity API state. +wp_interactivity_state( + 'activitypub/reactions', + array( + 'defaultAvatarUrl' => ACTIVITYPUB_PLUGIN_URL . 'assets/img/mp.jpg', + 'namespace' => ACTIVITYPUB_REST_NAMESPACE, + 'reactions' => array( + $_post_id => $reactions, + ), + ) +); + +// Render a subset of the most recent reactions. +$reactions = array_map( + function ( $reaction ) use ( $attributes ) { + $count = 20; + if ( 'wide' === $attributes['align'] ) { + $count = 40; + } elseif ( 'full' === $attributes['align'] ) { + $count = 60; + } + + $reaction['items'] = array_slice( array_reverse( $reaction['items'] ), 0, $count ); + + return $reaction; + }, + $reactions +); + +// Initialize the context for the block. +$context = array( + 'blockId' => $block_id, + 'modal' => array( + 'isCompact' => true, + 'isOpen' => false, + 'items' => array(), + ), + 'postId' => $_post_id, + 'reactions' => $reactions, +); + +// Add the block wrapper attributes. +$wrapper_attributes = get_block_wrapper_attributes( + array( + 'id' => $block_id, + 'data-wp-interactive' => 'activitypub/reactions', + 'data-wp-context' => wp_json_encode( $context, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ), + 'data-wp-init' => 'callbacks.initReactions', + ) +); + +ob_start(); +?> +
    + +
+ + +
> + + +
+ $reaction ) : + /* translators: %s: reaction type. */ + $aria_label = sprintf( __( 'View all %s', 'activitypub' ), Comment::get_comment_type_attr( $_type, 'label' ) ); + ?> +
+
    + +
+ +
+ +
+ + true, + 'content' => $modal_content, + ) + ); + ?> +
diff --git a/build/blocks/reactions/style-index-rtl.css b/build/blocks/reactions/style-index-rtl.css new file mode 100644 index 000000000..54a50cff2 --- /dev/null +++ b/build/blocks/reactions/style-index-rtl.css @@ -0,0 +1 @@ +body.modal-open{overflow:hidden}.activitypub-modal__overlay{align-items:center;background-color:rgba(0,0,0,.5);bottom:0;color:initial;display:flex;justify-content:center;right:0;padding:1rem;position:fixed;left:0;top:0;z-index:100000}.activitypub-modal__overlay.compact{align-items:flex-start;background-color:transparent;bottom:auto;justify-content:flex-start;right:auto;padding:0;position:absolute;left:auto;top:auto;z-index:100}.activitypub-modal__overlay[hidden]{display:none}.activitypub-modal__frame{animation:activitypub-modal-appear .2s ease-out;background-color:var(--wp--preset--color--white,#fff);border-radius:8px;box-shadow:0 5px 15px rgba(0,0,0,.3);display:flex;flex-direction:column;max-height:calc(100vh - 2rem);max-width:660px;overflow:hidden;width:100%}.compact .activitypub-modal__frame{box-shadow:0 2px 8px rgba(0,0,0,.1);max-height:300px;max-width:-moz-min-content;max-width:min-content;min-width:250px;width:auto}.activitypub-modal__header{align-items:center;border-bottom:1px solid var(--wp--preset--color--light-gray,#f0f0f0);display:flex;flex-shrink:0;justify-content:space-between;padding:2rem 2rem 1.5rem}.compact .activitypub-modal__header{display:none}.activitypub-modal__header .activitypub-modal__close{align-items:center;border:none;cursor:pointer;display:flex;justify-content:center;padding:.5rem;width:auto}.activitypub-modal__header .activitypub-modal__close:active{border:none;padding:.5rem}.activitypub-modal__title{font-size:130%;font-weight:600;line-height:1.4;margin:0!important}.activitypub-modal__content{overflow-y:auto}@keyframes activitypub-modal-appear{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.wp-block-activitypub-reactions{margin-bottom:2rem;margin-top:2rem;position:relative}.wp-block-activitypub-reactions.has-background,.wp-block-activitypub-reactions.has-border{box-sizing:border-box;padding:2rem}.wp-block-activitypub-reactions .activitypub-reactions{display:flex;flex-direction:column;flex-wrap:wrap}.wp-block-activitypub-reactions .reaction-group{align-items:center;display:flex;gap:.75rem;justify-content:flex-start;margin:.5em 0;position:relative;width:100%}@media(max-width:782px){.wp-block-activitypub-reactions .reaction-group:has(.reaction-avatars:not(:empty)){justify-content:space-between}}.wp-block-activitypub-reactions .reaction-group .reaction-avatars{align-items:center;display:flex;flex-direction:row;list-style:none;margin:0!important;padding:0}.wp-block-activitypub-reactions .reaction-group .reaction-avatars li{margin:0 0 0 -10px;padding:0;transition:transform .2s ease}.wp-block-activitypub-reactions .reaction-group .reaction-avatars li:not([hidden]):not(:has(~li:not([hidden]))){margin-left:0}.wp-block-activitypub-reactions .reaction-group .reaction-avatars li:hover{transform:translateY(-2px);z-index:2}.wp-block-activitypub-reactions .reaction-group .reaction-avatars li a{border-radius:50%;box-shadow:none;display:block;line-height:1;text-decoration:none}.wp-block-activitypub-reactions .reaction-group .reaction-avatar{max-height:32px;max-width:32px;overflow:hidden;-moz-force-broken-image-icon:1;border:.5px solid var(--wp--preset--color--contrast,hsla(0,0%,100%,.8));border-radius:50%;box-shadow:0 0 0 .5px hsla(0,0%,100%,.8),0 1px 3px rgba(0,0,0,.2);transition:transform .6s cubic-bezier(.34,1.56,.64,1);will-change:transform}.wp-block-activitypub-reactions .reaction-group .reaction-avatar:focus-visible,.wp-block-activitypub-reactions .reaction-group .reaction-avatar:hover{position:relative;transform:translateY(-5px);z-index:1}.wp-block-activitypub-reactions .reaction-group .reaction-label{align-items:center;background:none;border:none;border-radius:4px;color:currentColor;display:flex;flex:0 0 auto;font-size:70%;gap:.25rem;padding:.25rem .5rem;text-decoration:none;transition:background-color .2s ease;white-space:nowrap}.wp-block-activitypub-reactions .reaction-group .reaction-label:hover{background-color:rgba(0,0,0,.05);color:currentColor}.wp-block-activitypub-reactions .reaction-group .reaction-label:focus:not(:disabled){box-shadow:none;outline:1px solid currentColor;outline-offset:2px}.reactions-list{list-style:none;margin:0!important;padding:.5rem}.components-popover__content>.reactions-list{padding:0}.reactions-list .reaction-item{margin:0 0 .5rem}.reactions-list .reaction-item:last-child{margin-bottom:0}.reactions-list .reaction-item a{align-items:center;border-radius:4px;box-shadow:none;color:inherit;display:flex;gap:.75rem;padding:.5rem;text-decoration:none;transition:background-color .2s ease}.reactions-list .reaction-item a:hover{background-color:rgba(0,0,0,.03)}.reactions-list .reaction-item img{border:1px solid var(--wp--preset--color--light-gray,#f0f0f0);border-radius:50%;box-shadow:none;height:36px;width:36px}.reactions-list .reaction-item .reaction-name{font-size:75%}.components-popover__content{box-shadow:0 2px 8px rgba(0,0,0,.1);max-height:300px;max-width:-moz-min-content;max-width:min-content;min-width:250px;padding:.5rem;width:auto} diff --git a/build/blocks/reactions/style-index.css b/build/blocks/reactions/style-index.css new file mode 100644 index 000000000..5b2fbf3f5 --- /dev/null +++ b/build/blocks/reactions/style-index.css @@ -0,0 +1 @@ +body.modal-open{overflow:hidden}.activitypub-modal__overlay{align-items:center;background-color:rgba(0,0,0,.5);bottom:0;color:initial;display:flex;justify-content:center;left:0;padding:1rem;position:fixed;right:0;top:0;z-index:100000}.activitypub-modal__overlay.compact{align-items:flex-start;background-color:transparent;bottom:auto;justify-content:flex-start;left:auto;padding:0;position:absolute;right:auto;top:auto;z-index:100}.activitypub-modal__overlay[hidden]{display:none}.activitypub-modal__frame{animation:activitypub-modal-appear .2s ease-out;background-color:var(--wp--preset--color--white,#fff);border-radius:8px;box-shadow:0 5px 15px rgba(0,0,0,.3);display:flex;flex-direction:column;max-height:calc(100vh - 2rem);max-width:660px;overflow:hidden;width:100%}.compact .activitypub-modal__frame{box-shadow:0 2px 8px rgba(0,0,0,.1);max-height:300px;max-width:-moz-min-content;max-width:min-content;min-width:250px;width:auto}.activitypub-modal__header{align-items:center;border-bottom:1px solid var(--wp--preset--color--light-gray,#f0f0f0);display:flex;flex-shrink:0;justify-content:space-between;padding:2rem 2rem 1.5rem}.compact .activitypub-modal__header{display:none}.activitypub-modal__header .activitypub-modal__close{align-items:center;border:none;cursor:pointer;display:flex;justify-content:center;padding:.5rem;width:auto}.activitypub-modal__header .activitypub-modal__close:active{border:none;padding:.5rem}.activitypub-modal__title{font-size:130%;font-weight:600;line-height:1.4;margin:0!important}.activitypub-modal__content{overflow-y:auto}@keyframes activitypub-modal-appear{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.wp-block-activitypub-reactions{margin-bottom:2rem;margin-top:2rem;position:relative}.wp-block-activitypub-reactions.has-background,.wp-block-activitypub-reactions.has-border{box-sizing:border-box;padding:2rem}.wp-block-activitypub-reactions .activitypub-reactions{display:flex;flex-direction:column;flex-wrap:wrap}.wp-block-activitypub-reactions .reaction-group{align-items:center;display:flex;gap:.75rem;justify-content:flex-start;margin:.5em 0;position:relative;width:100%}@media(max-width:782px){.wp-block-activitypub-reactions .reaction-group:has(.reaction-avatars:not(:empty)){justify-content:space-between}}.wp-block-activitypub-reactions .reaction-group .reaction-avatars{align-items:center;display:flex;flex-direction:row;list-style:none;margin:0!important;padding:0}.wp-block-activitypub-reactions .reaction-group .reaction-avatars li{margin:0 -10px 0 0;padding:0;transition:transform .2s ease}.wp-block-activitypub-reactions .reaction-group .reaction-avatars li:not([hidden]):not(:has(~li:not([hidden]))){margin-right:0}.wp-block-activitypub-reactions .reaction-group .reaction-avatars li:hover{transform:translateY(-2px);z-index:2}.wp-block-activitypub-reactions .reaction-group .reaction-avatars li a{border-radius:50%;box-shadow:none;display:block;line-height:1;text-decoration:none}.wp-block-activitypub-reactions .reaction-group .reaction-avatar{max-height:32px;max-width:32px;overflow:hidden;-moz-force-broken-image-icon:1;border:.5px solid var(--wp--preset--color--contrast,hsla(0,0%,100%,.8));border-radius:50%;box-shadow:0 0 0 .5px hsla(0,0%,100%,.8),0 1px 3px rgba(0,0,0,.2);transition:transform .6s cubic-bezier(.34,1.56,.64,1);will-change:transform}.wp-block-activitypub-reactions .reaction-group .reaction-avatar:focus-visible,.wp-block-activitypub-reactions .reaction-group .reaction-avatar:hover{position:relative;transform:translateY(-5px);z-index:1}.wp-block-activitypub-reactions .reaction-group .reaction-label{align-items:center;background:none;border:none;border-radius:4px;color:currentColor;display:flex;flex:0 0 auto;font-size:70%;gap:.25rem;padding:.25rem .5rem;text-decoration:none;transition:background-color .2s ease;white-space:nowrap}.wp-block-activitypub-reactions .reaction-group .reaction-label:hover{background-color:rgba(0,0,0,.05);color:currentColor}.wp-block-activitypub-reactions .reaction-group .reaction-label:focus:not(:disabled){box-shadow:none;outline:1px solid currentColor;outline-offset:2px}.reactions-list{list-style:none;margin:0!important;padding:.5rem}.components-popover__content>.reactions-list{padding:0}.reactions-list .reaction-item{margin:0 0 .5rem}.reactions-list .reaction-item:last-child{margin-bottom:0}.reactions-list .reaction-item a{align-items:center;border-radius:4px;box-shadow:none;color:inherit;display:flex;gap:.75rem;padding:.5rem;text-decoration:none;transition:background-color .2s ease}.reactions-list .reaction-item a:hover{background-color:rgba(0,0,0,.03)}.reactions-list .reaction-item img{border:1px solid var(--wp--preset--color--light-gray,#f0f0f0);border-radius:50%;box-shadow:none;height:36px;width:36px}.reactions-list .reaction-item .reaction-name{font-size:75%}.components-popover__content{box-shadow:0 2px 8px rgba(0,0,0,.1);max-height:300px;max-width:-moz-min-content;max-width:min-content;min-width:250px;padding:.5rem;width:auto} diff --git a/build/blocks/reactions/view.asset.php b/build/blocks/reactions/view.asset.php new file mode 100644 index 000000000..3c0e9f451 --- /dev/null +++ b/build/blocks/reactions/view.asset.php @@ -0,0 +1 @@ + array('@wordpress/interactivity'), 'version' => '4067078b710ef29f00a0', 'type' => 'module'); diff --git a/build/blocks/reactions/view.js b/build/blocks/reactions/view.js new file mode 100644 index 000000000..0363abe94 --- /dev/null +++ b/build/blocks/reactions/view.js @@ -0,0 +1 @@ +import*as t from"@wordpress/interactivity";var e={d:(t,o)=>{for(var n in o)e.o(o,n)&&!e.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:o[n]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e)};const o=(n={getContext:()=>t.getContext,getElement:()=>t.getElement,store:()=>t.store,withScope:()=>t.withScope},a={},e.d(a,n),a);var n,a;const{apiFetch:c}=window.wp;!function(){const{actions:t,callbacks:e}=(0,o.store)("activitypub/reactions",{actions:{openModal(t){const n=(0,o.getContext)();n.modal.isOpen=!0,n.modal.isCompact?setTimeout(e.positionModal,0):setTimeout((()=>{const t=document.getElementById(n.blockId);if(t){const o=t.querySelector(".activitypub-modal__frame");o&&e.trapFocus(o)}}),50),"function"==typeof e.onModalOpen&&e.onModalOpen(t)},closeModal(t){const n=(0,o.getContext)();n.modal.isOpen=!1;const a=(0,o.getElement)();if("actions.toggleModal"===a.ref.dataset["wpOn-Click"])a.ref.focus();else{const t=document.getElementById(n.blockId);if(t){const e=t.querySelector('[data-wp-on--click="actions.toggleModal"], [data-wp-on-async--click="actions.toggleModal"]');e&&e.focus()}}"function"==typeof e.onModalClose&&e.onModalClose(t)},toggleModal(e){const{modal:n}=(0,o.getContext)();n.isOpen?t.closeModal(e):t.openModal(e)}},callbacks:{_abortController:null,handleModalEffects(){const{modal:t}=(0,o.getContext)();if(t.isOpen&&!t.isCompact?document.body.classList.add("modal-open"):document.body.classList.remove("modal-open"),e._abortController&&(e._abortController.abort(),e._abortController=null),t.isOpen){e._abortController=new AbortController;const{signal:t}=e._abortController;document.addEventListener("keydown",e.documentKeydown,{signal:t}),document.addEventListener("click",e.documentClick,{signal:t})}},documentKeydown(e){const{modal:n}=(0,o.getContext)();n.isOpen&&"Escape"===e.key&&t.closeModal()},documentClick(e){const{blockId:n,modal:a}=(0,o.getContext)();if(!a.isOpen)return;const c=document.getElementById(n);if(!c)return;const l=c.querySelector('.wp-element-button[data-wp-on--click="actions.toggleModal"]');if(l&&(l===e.target||l.contains(e.target)))return;const s=c.querySelector(".activitypub-modal__frame");s&&!s.contains(e.target)&&t.closeModal()},positionModal(){const{blockId:t}=(0,o.getContext)(),e=document.getElementById(t);if(!e)return;const n=e.querySelector(".activitypub-modal__overlay");if(!n)return;n.style.top="",n.style.left="",n.style.right="",n.style.bottom="";const a=(0,o.getElement)().ref.getBoundingClientRect(),c=window.innerWidth,l=e.getBoundingClientRect();let s={top:a.bottom-l.top+8+"px",left:a.left-l.left-2+"px"};c-a.right<250&&(s.left="auto",s.right=l.right-a.right+"px"),Object.assign(n.style,s)},trapFocus(t){const e=t.querySelectorAll('a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input[type="text"]:not([disabled]):not([readonly]), input[type="radio"]:not([disabled]), input[type="checkbox"]:not([disabled]), select:not([disabled])'),o=e[0],n=e[e.length-1];o&&o.classList.contains("activitypub-modal__close")&&e.length>1?e[1].focus():o.focus(),t.addEventListener("keydown",(function(t){"Tab"!==t.key&&9!==t.keyCode||(t.shiftKey?document.activeElement===o&&(n.focus(),t.preventDefault()):document.activeElement===n&&(o.focus(),t.preventDefault()))}))}}})}();const{callbacks:l,state:s}=(0,o.store)("activitypub/reactions",{actions:{async fetchReactions(){const t=(0,o.getContext)(),{namespace:e}=s;if(t.postId)try{t.reactions=await c({path:`/${e}/posts/${t.postId}/reactions`})}catch(t){console.error("Error fetching reactions:",t)}}},callbacks:{initReactions(){const t=new ResizeObserver((0,o.withScope)(l.calculateVisibleAvatars));return(0,o.getElement)().ref.querySelectorAll(".reaction-group").forEach((e=>{t.observe(e)})),()=>{t.disconnect()}},calculateVisibleAvatars(){const{postId:t}=(0,o.getContext)();(s.reactions&&s.reactions[t]?Object.keys(s.reactions[t]):[]).forEach((e=>{s.reactions?.[t][e]?.items?.length&&(0,o.getElement)().ref.querySelectorAll(`.reaction-group[data-reaction-type="${e}"]`).forEach((o=>{const n=o.querySelector(".reaction-label").offsetWidth||0,a=o.offsetWidth-n-12;let c=1;a>32&&(c+=Math.floor((a-32)/22));const l=s.reactions[t][e].items,r=Math.min(c,l.length),i=o.querySelector(".reaction-avatars");i&&i.querySelectorAll("li").forEach(((t,e)=>{e array('react', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '5a6a24d679d205a8f709'); + array('react', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '57bbe00f94168db2cc91'); diff --git a/build/remote-reply/index.js b/build/blocks/remote-reply/index.js similarity index 93% rename from build/remote-reply/index.js rename to build/blocks/remote-reply/index.js index 601228ce4..083ee5d33 100644 --- a/build/remote-reply/index.js +++ b/build/blocks/remote-reply/index.js @@ -1,2 +1,2 @@ -(()=>{"use strict";var e,t={20:(e,t,r)=>{var o=r(609),a=Symbol.for("react.element"),i=(Symbol.for("react.fragment"),Object.prototype.hasOwnProperty),l=o.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,n={key:!0,ref:!0,__self:!0,__source:!0};t.jsx=function(e,t,r){var o,c={},s=null,m=null;for(o in void 0!==r&&(s=""+r),void 0!==t.key&&(s=""+t.key),void 0!==t.ref&&(m=t.ref),t)i.call(t,o)&&!n.hasOwnProperty(o)&&(c[o]=t[o]);if(e&&e.defaultProps)for(o in t=e.defaultProps)void 0===c[o]&&(c[o]=t[o]);return{$$typeof:a,type:e,key:s,ref:m,props:c,_owner:l.current}}},170:(e,t,r)=>{var o=r(609);const a=window.wp.element,i=window.wp.domReady;var l=r.n(i);const n=window.wp.components,c=window.wp.i18n,s=(0,a.forwardRef)((function({icon:e,size:t=24,...r},o){return(0,a.cloneElement)(e,{width:t,height:t,...r,ref:o})})),m=window.wp.primitives;var p=r(848);const u=(0,p.jsx)(m.SVG,{viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg",children:(0,p.jsx)(m.Path,{d:"M12 21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21ZM15.5303 8.46967C15.8232 8.76256 15.8232 9.23744 15.5303 9.53033L13.0607 12L15.5303 14.4697C15.8232 14.7626 15.8232 15.2374 15.5303 15.5303C15.2374 15.8232 14.7626 15.8232 14.4697 15.5303L12 13.0607L9.53033 15.5303C9.23744 15.8232 8.76256 15.8232 8.46967 15.5303C8.17678 15.2374 8.17678 14.7626 8.46967 14.4697L10.9393 12L8.46967 9.53033C8.17678 9.23744 8.17678 8.76256 8.46967 8.46967C8.76256 8.17678 9.23744 8.17678 9.53033 8.46967L12 10.9393L14.4697 8.46967C14.7626 8.17678 15.2374 8.17678 15.5303 8.46967Z"})}),d=window.wp.apiFetch;var v=r.n(d);const y=(0,p.jsx)(m.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(m.Path,{fillRule:"evenodd",clipRule:"evenodd",d:"M5 4.5h11a.5.5 0 0 1 .5.5v11a.5.5 0 0 1-.5.5H5a.5.5 0 0 1-.5-.5V5a.5.5 0 0 1 .5-.5ZM3 5a2 2 0 0 1 2-2h11a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5Zm17 3v10.75c0 .69-.56 1.25-1.25 1.25H6v1.5h12.75a2.75 2.75 0 0 0 2.75-2.75V8H20Z"})}),_=(0,p.jsx)(m.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(m.Path,{d:"M16.7 7.1l-6.3 8.5-3.3-2.5-.9 1.2 4.5 3.4L17.9 8z"})}),f=(0,p.jsx)(m.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(m.Path,{d:"M15.5 9.5a1 1 0 100-2 1 1 0 000 2zm0 1.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5zm-2.25 6v-2a2.75 2.75 0 00-2.75-2.75h-4A2.75 2.75 0 003.75 15v2h1.5v-2c0-.69.56-1.25 1.25-1.25h4c.69 0 1.25.56 1.25 1.25v2h1.5zm7-2v2h-1.5v-2c0-.69-.56-1.25-1.25-1.25H15v-1.5h2.5A2.75 2.75 0 0120.25 15zM9.5 8.5a1 1 0 11-2 0 1 1 0 012 0zm1.5 0a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z",fillRule:"evenodd"})}),b=window.wp.compose,w="fediverse-remote-user";function h(){const[e,t]=(0,a.useState)(function(){const e=localStorage.getItem(w);return e?JSON.parse(e):{}}()),r=(0,a.useCallback)((e=>{!function(e){localStorage.setItem(w,JSON.stringify(e))}(e),t(e)}),[]),o=(0,a.useCallback)((()=>{localStorage.removeItem(w),t({})}),[]);return{template:e?.template||!1,profileURL:e?.profileURL||!1,setRemoteUser:r,deleteRemoteUser:o}}function g(e){try{return new URL(e),!0}catch(e){return!1}}function E({actionText:e,copyDescription:t,handle:r,resourceUrl:i,myProfile:l="",rememberProfile:m=!1}){const p=(0,c.__)("Loading...","activitypub"),u=(0,c.__)("Opening...","activitypub"),d=(0,c.__)("Error","activitypub"),w=(0,c.__)("Invalid","activitypub"),E=l||(0,c.__)("My Profile","activitypub"),[C,R]=(0,a.useState)(e),[x,O]=(0,a.useState)(y),k=(0,b.useCopyToClipboard)(r,(()=>{O(_),setTimeout((()=>O(y)),1e3)})),[L,S]=(0,a.useState)(""),[U,N]=(0,a.useState)(!0),{setRemoteUser:P}=h(),j=(0,a.useCallback)((()=>{let t;if(!g(L)&&!function(e){const t=e.replace(/^@/,"").split("@");return 2===t.length&&g(`https://${t[1]}`)}(L))return R(w),t=setTimeout((()=>R(e)),2e3),()=>clearTimeout(t);const r=i+L;R(p),v()({path:r}).then((({url:t,template:r})=>{U&&P({profileURL:L,template:r}),R(u),setTimeout((()=>{window.open(t,"_blank"),R(e)}),200)})).catch((()=>{R(d),setTimeout((()=>R(e)),2e3)}))}),[L]);return(0,o.createElement)("div",{className:"activitypub__dialog",role:"dialog","aria-labelledby":"dialog-title"},(0,o.createElement)("div",{className:"activitypub-dialog__section"},(0,o.createElement)("h4",{id:"dialog-title"},E),(0,o.createElement)("div",{className:"activitypub-dialog__description",id:"copy-description"},t),(0,o.createElement)("div",{className:"activitypub-dialog__button-group"},(0,o.createElement)("label",{htmlFor:"profile-handle",className:"screen-reader-text"},t),(0,o.createElement)("input",{type:"text",id:"profile-handle",value:r,readOnly:!0}),(0,o.createElement)(n.Button,{ref:k,"aria-label":(0,c.__)("Copy handle to clipboard","activitypub")},(0,o.createElement)(s,{icon:x}),(0,c.__)("Copy","activitypub")))),(0,o.createElement)("div",{className:"activitypub-dialog__section"},(0,o.createElement)("h4",{id:"remote-profile-title"},(0,c.__)("Your Profile","activitypub")),(0,o.createElement)("div",{className:"activitypub-dialog__description",id:"remote-profile-description"},(0,a.createInterpolateElement)((0,c.__)("Or, if you know your own profile, we can start things that way! (eg @yourusername@example.com)","activitypub"),{code:(0,o.createElement)("code",null)})),(0,o.createElement)("div",{className:"activitypub-dialog__button-group"},(0,o.createElement)("label",{htmlFor:"remote-profile",className:"screen-reader-text"},(0,c.__)("Enter your ActivityPub profile","activitypub")),(0,o.createElement)("input",{type:"text",id:"remote-profile",value:L,onKeyDown:e=>{"Enter"===e?.code&&j()},onChange:e=>S(e.target.value),"aria-invalid":C===w}),(0,o.createElement)(n.Button,{onClick:j,"aria-label":(0,c.__)("Submit profile","activitypub")},(0,o.createElement)(s,{icon:f}),C)),m&&(0,o.createElement)("div",{className:"activitypub-dialog__remember"},(0,o.createElement)(n.CheckboxControl,{checked:U,label:(0,c.__)("Remember me for easier comments","activitypub"),onChange:()=>{N(!U)}}))))}function C({selectedComment:e,commentId:t}){const{namespace:r}=window._activityPubOptions||{},a=(0,c.__)("Reply","activitypub"),i=`/${r}/comments/${t}/remote-reply?resource=`,l=(0,c.__)("Copy and paste the Comment URL into the search field of your favorite fediverse app or server.","activitypub");return(0,o.createElement)(E,{actionText:a,copyDescription:l,handle:e,resourceUrl:i,myProfile:(0,c.__)("Original Comment URL","activitypub"),rememberProfile:!0})}function R({profileURL:e,template:t,commentURL:r,deleteRemoteUser:a}){return(0,o.createElement)(o.Fragment,null,(0,o.createElement)(n.Button,{variant:"link",className:"comment-reply-link activitypub-remote-reply__button",onClick:()=>{const e=t.replace("{uri}",r);window.open(e,"_blank")}},/* translators: %s: profile name */ /* translators: %s: profile name */ -(0,c.sprintf)((0,c.__)("Reply as %s","activitypub"),e)),(0,o.createElement)(n.Button,{className:"activitypub-remote-profile-delete",onClick:a,title:(0,c.__)("Delete Remote Profile","activitypub")},(0,o.createElement)(s,{icon:u,size:18})))}function x({selectedComment:e,commentId:t}){const[r,i]=(0,a.useState)(!1),l=(0,c.__)("Remote Reply","activitypub"),{profileURL:s,template:m,deleteRemoteUser:p}=h(),u=s&&m;return(0,o.createElement)(o.Fragment,null,u?(0,o.createElement)(R,{profileURL:s,template:m,commentURL:e,deleteRemoteUser:p}):(0,o.createElement)(n.Button,{variant:"link",className:"comment-reply-link activitypub-remote-reply__button",onClick:()=>i(!0)},(0,c.__)("Reply on the Fediverse","activitypub")),r&&(0,o.createElement)(n.Modal,{className:"activitypub-remote-reply__modal activitypub__modal",onRequestClose:()=>i(!1),title:l},(0,o.createElement)(C,{selectedComment:e,commentId:t})))}let O=1;l()((()=>{[].forEach.call(document.querySelectorAll(".activitypub-remote-reply"),(e=>{const t=JSON.parse(e.dataset.attrs);(0,a.createRoot)(e).render((0,o.createElement)(x,{...t,id:"activitypub-remote-reply-link-"+O++,useId:!0}))}))}))},609:e=>{e.exports=window.React},848:(e,t,r)=>{e.exports=r(20)}},r={};function o(e){var a=r[e];if(void 0!==a)return a.exports;var i=r[e]={exports:{}};return t[e](i,i.exports,o),i.exports}o.m=t,e=[],o.O=(t,r,a,i)=>{if(!r){var l=1/0;for(m=0;m=i)&&Object.keys(o.O).every((e=>o.O[e](r[c])))?r.splice(c--,1):(n=!1,i0&&e[m-1][2]>i;m--)e[m]=e[m-1];e[m]=[r,a,i]},o.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return o.d(t,{a:t}),t},o.d=(e,t)=>{for(var r in t)o.o(t,r)&&!o.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},o.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e={227:0,739:0};o.O.j=t=>0===e[t];var t=(t,r)=>{var a,i,[l,n,c]=r,s=0;if(l.some((t=>0!==e[t]))){for(a in n)o.o(n,a)&&(o.m[a]=n[a]);if(c)var m=c(o)}for(t&&t(r);so(170)));a=o.O(a)})(); \ No newline at end of file +(()=>{"use strict";var e,t={20:(e,t,r)=>{var o=r(609),a=Symbol.for("react.element"),i=(Symbol.for("react.fragment"),Object.prototype.hasOwnProperty),l=o.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,n={key:!0,ref:!0,__self:!0,__source:!0};t.jsx=function(e,t,r){var o,c={},s=null,m=null;for(o in void 0!==r&&(s=""+r),void 0!==t.key&&(s=""+t.key),void 0!==t.ref&&(m=t.ref),t)i.call(t,o)&&!n.hasOwnProperty(o)&&(c[o]=t[o]);if(e&&e.defaultProps)for(o in t=e.defaultProps)void 0===c[o]&&(c[o]=t[o]);return{$$typeof:a,type:e,key:s,ref:m,props:c,_owner:l.current}}},146:(e,t,r)=>{var o=r(609);const a=window.wp.element,i=window.wp.domReady;var l=r.n(i);const n=window.wp.components,c=window.wp.i18n,s=(0,a.forwardRef)((function({icon:e,size:t=24,...r},o){return(0,a.cloneElement)(e,{width:t,height:t,...r,ref:o})})),m=window.wp.primitives;var p=r(848);const u=(0,p.jsx)(m.SVG,{viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg",children:(0,p.jsx)(m.Path,{d:"M12 21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21ZM15.5303 8.46967C15.8232 8.76256 15.8232 9.23744 15.5303 9.53033L13.0607 12L15.5303 14.4697C15.8232 14.7626 15.8232 15.2374 15.5303 15.5303C15.2374 15.8232 14.7626 15.8232 14.4697 15.5303L12 13.0607L9.53033 15.5303C9.23744 15.8232 8.76256 15.8232 8.46967 15.5303C8.17678 15.2374 8.17678 14.7626 8.46967 14.4697L10.9393 12L8.46967 9.53033C8.17678 9.23744 8.17678 8.76256 8.46967 8.46967C8.76256 8.17678 9.23744 8.17678 9.53033 8.46967L12 10.9393L14.4697 8.46967C14.7626 8.17678 15.2374 8.17678 15.5303 8.46967Z"})}),d=window.wp.apiFetch;var v=r.n(d);const y=(0,p.jsx)(m.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(m.Path,{fillRule:"evenodd",clipRule:"evenodd",d:"M5 4.5h11a.5.5 0 0 1 .5.5v11a.5.5 0 0 1-.5.5H5a.5.5 0 0 1-.5-.5V5a.5.5 0 0 1 .5-.5ZM3 5a2 2 0 0 1 2-2h11a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5Zm17 3v10.75c0 .69-.56 1.25-1.25 1.25H6v1.5h12.75a2.75 2.75 0 0 0 2.75-2.75V8H20Z"})}),_=(0,p.jsx)(m.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(m.Path,{d:"M16.7 7.1l-6.3 8.5-3.3-2.5-.9 1.2 4.5 3.4L17.9 8z"})}),f=(0,p.jsx)(m.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(m.Path,{d:"M15.5 9.5a1 1 0 100-2 1 1 0 000 2zm0 1.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5zm-2.25 6v-2a2.75 2.75 0 00-2.75-2.75h-4A2.75 2.75 0 003.75 15v2h1.5v-2c0-.69.56-1.25 1.25-1.25h4c.69 0 1.25.56 1.25 1.25v2h1.5zm7-2v2h-1.5v-2c0-.69-.56-1.25-1.25-1.25H15v-1.5h2.5A2.75 2.75 0 0120.25 15zM9.5 8.5a1 1 0 11-2 0 1 1 0 012 0zm1.5 0a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z",fillRule:"evenodd"})}),b=window.wp.compose,w="fediverse-remote-user";function h(){const[e,t]=(0,a.useState)(function(){const e=localStorage.getItem(w);return e?JSON.parse(e):{}}()),r=(0,a.useCallback)((e=>{!function(e){localStorage.setItem(w,JSON.stringify(e))}(e),t(e)}),[]),o=(0,a.useCallback)((()=>{localStorage.removeItem(w),t({})}),[]);return{template:e?.template||!1,profileURL:e?.profileURL||!1,setRemoteUser:r,deleteRemoteUser:o}}function g(e){try{return new URL(e),!0}catch(e){return!1}}function E({actionText:e,copyDescription:t,handle:r,resourceUrl:i,myProfile:l="",rememberProfile:m=!1}){const p=(0,c.__)("Loading...","activitypub"),u=(0,c.__)("Opening...","activitypub"),d=(0,c.__)("Error","activitypub"),w=(0,c.__)("Invalid","activitypub"),E=l||(0,c.__)("My Profile","activitypub"),[C,R]=(0,a.useState)(e),[x,O]=(0,a.useState)(y),k=(0,b.useCopyToClipboard)(r,(()=>{O(_),setTimeout((()=>O(y)),1e3)})),[L,S]=(0,a.useState)(""),[U,N]=(0,a.useState)(!0),{setRemoteUser:P}=h(),j=(0,a.useCallback)((()=>{let t;if(!g(L)&&!function(e){const t=e.replace(/^@/,"").split("@");return 2===t.length&&g(`https://${t[1]}`)}(L))return R(w),t=setTimeout((()=>R(e)),2e3),()=>clearTimeout(t);const r=i+L;R(p),v()({path:r}).then((({url:t,template:r})=>{U&&P({profileURL:L,template:r}),R(u),setTimeout((()=>{window.open(t,"_blank"),R(e)}),200)})).catch((()=>{R(d),setTimeout((()=>R(e)),2e3)}))}),[L]);return(0,o.createElement)("div",{className:"activitypub__dialog",role:"dialog","aria-labelledby":"dialog-title"},(0,o.createElement)("div",{className:"activitypub-dialog__section"},(0,o.createElement)("h4",{id:"dialog-title"},E),(0,o.createElement)("div",{className:"activitypub-dialog__description",id:"copy-description"},t),(0,o.createElement)("div",{className:"activitypub-dialog__button-group"},(0,o.createElement)("label",{htmlFor:"profile-handle",className:"screen-reader-text"},t),(0,o.createElement)("input",{type:"text",id:"profile-handle",value:r,readOnly:!0}),(0,o.createElement)(n.Button,{ref:k,"aria-label":(0,c.__)("Copy handle to clipboard","activitypub")},(0,o.createElement)(s,{icon:x}),(0,c.__)("Copy","activitypub")))),(0,o.createElement)("div",{className:"activitypub-dialog__section"},(0,o.createElement)("h4",{id:"remote-profile-title"},(0,c.__)("Your Profile","activitypub")),(0,o.createElement)("div",{className:"activitypub-dialog__description",id:"remote-profile-description"},(0,a.createInterpolateElement)((0,c.__)("Or, if you know your own profile, we can start things that way! (eg @yourusername@example.com)","activitypub"),{code:(0,o.createElement)("code",null)})),(0,o.createElement)("div",{className:"activitypub-dialog__button-group"},(0,o.createElement)("label",{htmlFor:"remote-profile",className:"screen-reader-text"},(0,c.__)("Enter your ActivityPub profile","activitypub")),(0,o.createElement)("input",{type:"text",id:"remote-profile",value:L,onKeyDown:e=>{"Enter"===e?.code&&j()},onChange:e=>S(e.target.value),"aria-invalid":C===w}),(0,o.createElement)(n.Button,{onClick:j,"aria-label":(0,c.__)("Submit profile","activitypub")},(0,o.createElement)(s,{icon:f}),C)),m&&(0,o.createElement)("div",{className:"activitypub-dialog__remember"},(0,o.createElement)(n.CheckboxControl,{checked:U,label:(0,c.__)("Remember me for easier comments","activitypub"),onChange:()=>{N(!U)}}))))}function C({selectedComment:e,commentId:t}){const{namespace:r}=window._activityPubOptions||{},a=(0,c.__)("Reply","activitypub"),i=`/${r}/comments/${t}/remote-reply?resource=`,l=(0,c.__)("Copy and paste the Comment URL into the search field of your favorite fediverse app or server.","activitypub");return(0,o.createElement)(E,{actionText:a,copyDescription:l,handle:e,resourceUrl:i,myProfile:(0,c.__)("Original Comment URL","activitypub"),rememberProfile:!0})}function R({profileURL:e,template:t,commentURL:r,deleteRemoteUser:a}){return(0,o.createElement)(o.Fragment,null,(0,o.createElement)(n.Button,{variant:"link",className:"comment-reply-link activitypub-remote-reply__button",onClick:()=>{const e=t.replace("{uri}",r);window.open(e,"_blank")}},/* translators: %s: profile name */ /* translators: %s: profile name */ +(0,c.sprintf)((0,c.__)("Reply as %s","activitypub"),e)),(0,o.createElement)(n.Button,{className:"activitypub-remote-profile-delete",onClick:a,title:(0,c.__)("Delete Remote Profile","activitypub")},(0,o.createElement)(s,{icon:u,size:18})))}function x({selectedComment:e,commentId:t}){const[r,i]=(0,a.useState)(!1),l=(0,c.__)("Remote Reply","activitypub"),{profileURL:s,template:m,deleteRemoteUser:p}=h(),u=s&&m;return(0,o.createElement)(o.Fragment,null,u?(0,o.createElement)(R,{profileURL:s,template:m,commentURL:e,deleteRemoteUser:p}):(0,o.createElement)(n.Button,{variant:"link",className:"comment-reply-link activitypub-remote-reply__button",onClick:()=>i(!0)},(0,c.__)("Reply on the Fediverse","activitypub")),r&&(0,o.createElement)(n.Modal,{className:"activitypub-remote-reply__modal activitypub__modal",onRequestClose:()=>i(!1),title:l},(0,o.createElement)(C,{selectedComment:e,commentId:t})))}let O=1;l()((()=>{[].forEach.call(document.querySelectorAll(".activitypub-remote-reply"),(e=>{const t=JSON.parse(e.dataset.attrs);(0,a.createRoot)(e).render((0,o.createElement)(x,{...t,id:"activitypub-remote-reply-link-"+O++,useId:!0}))}))}))},609:e=>{e.exports=window.React},848:(e,t,r)=>{e.exports=r(20)}},r={};function o(e){var a=r[e];if(void 0!==a)return a.exports;var i=r[e]={exports:{}};return t[e](i,i.exports,o),i.exports}o.m=t,e=[],o.O=(t,r,a,i)=>{if(!r){var l=1/0;for(m=0;m=i)&&Object.keys(o.O).every((e=>o.O[e](r[c])))?r.splice(c--,1):(n=!1,i0&&e[m-1][2]>i;m--)e[m]=e[m-1];e[m]=[r,a,i]},o.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return o.d(t,{a:t}),t},o.d=(e,t)=>{for(var r in t)o.o(t,r)&&!o.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},o.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e={284:0,4:0};o.O.j=t=>0===e[t];var t=(t,r)=>{var a,i,[l,n,c]=r,s=0;if(l.some((t=>0!==e[t]))){for(a in n)o.o(n,a)&&(o.m[a]=n[a]);if(c)var m=c(o)}for(t&&t(r);so(146)));a=o.O(a)})(); \ No newline at end of file diff --git a/build/blocks/remote-reply/render.php b/build/blocks/remote-reply/render.php new file mode 100644 index 000000000..2e7026c90 --- /dev/null +++ b/build/blocks/remote-reply/render.php @@ -0,0 +1,192 @@ + ACTIVITYPUB_REST_NAMESPACE, + 'i18n' => array( + 'copied' => __( 'Copied!', 'activitypub' ), + 'copy' => __( 'Copy', 'activitypub' ), + 'emptyProfileError' => __( 'Please enter a profile URL or handle.', 'activitypub' ), + 'genericError' => __( 'An error occurred. Please try again.', 'activitypub' ), + 'invalidProfileError' => __( 'Please enter a valid URL or handle.', 'activitypub' ), + ), + ) +); + +// Add the block wrapper attributes. +$wrapper_attributes = get_block_wrapper_attributes( + array( + 'id' => $block_id, + 'class' => 'activitypub-remote-reply reply', + 'data-wp-interactive' => 'activitypub/remote-reply', + 'data-wp-init' => 'callbacks.init', + ) +); + +$wrapper_context = wp_interactivity_data_wp_context( + array( + 'blockId' => $block_id, + 'commentId' => $comment_id, + 'commentURL' => $selected_comment, + 'copyButtonText' => $state['i18n']['copy'], + 'errorMessage' => '', + 'hasRemoteUser' => false, + 'isError' => false, + 'isLoading' => false, + 'modal' => array( 'isOpen' => false ), + 'profileURL' => '', + 'remoteProfile' => '', + 'shouldSaveProfile' => true, + 'template' => '', + ) +); + +ob_start(); +?> +
+

+
+ +
+
+ + +
+
+
+

+
+ +
+
+ + +
+
+
+ +
+
+ +
+ +> + + + + + __( 'Remote Reply', 'activitypub' ), + 'content' => $modal_content, + ) + ); + ?> +
+ array('@wordpress/interactivity'), 'version' => '95d693d7cf19305bfcbb', 'type' => 'module'); diff --git a/build/blocks/remote-reply/view.js b/build/blocks/remote-reply/view.js new file mode 100644 index 000000000..aa72b8d31 --- /dev/null +++ b/build/blocks/remote-reply/view.js @@ -0,0 +1 @@ +import*as e from"@wordpress/interactivity";var t,o,r={580:(t,o,r)=>{const n=(l={getContext:()=>e.getContext,getElement:()=>e.getElement,store:()=>e.store},s={},r.d(s,l),s);var l,s;const{apiFetch:a}=window.wp;!function(){const{actions:e,callbacks:t}=(0,n.store)("activitypub/remote-reply",{actions:{openModal(e){const o=(0,n.getContext)();o.modal.isOpen=!0,o.modal.isCompact?setTimeout(t.positionModal,0):setTimeout((()=>{const e=document.getElementById(o.blockId);if(e){const o=e.querySelector(".activitypub-modal__frame");o&&t.trapFocus(o)}}),50),"function"==typeof t.onModalOpen&&t.onModalOpen(e)},closeModal(e){const o=(0,n.getContext)();o.modal.isOpen=!1;const r=(0,n.getElement)();if("actions.toggleModal"===r.ref.dataset["wpOn-Click"])r.ref.focus();else{const e=document.getElementById(o.blockId);if(e){const t=e.querySelector('[data-wp-on--click="actions.toggleModal"], [data-wp-on-async--click="actions.toggleModal"]');t&&t.focus()}}"function"==typeof t.onModalClose&&t.onModalClose(e)},toggleModal(t){const{modal:o}=(0,n.getContext)();o.isOpen?e.closeModal(t):e.openModal(t)}},callbacks:{_abortController:null,handleModalEffects(){const{modal:e}=(0,n.getContext)();if(e.isOpen&&!e.isCompact?document.body.classList.add("modal-open"):document.body.classList.remove("modal-open"),t._abortController&&(t._abortController.abort(),t._abortController=null),e.isOpen){t._abortController=new AbortController;const{signal:e}=t._abortController;document.addEventListener("keydown",t.documentKeydown,{signal:e}),document.addEventListener("click",t.documentClick,{signal:e})}},documentKeydown(t){const{modal:o}=(0,n.getContext)();o.isOpen&&"Escape"===t.key&&e.closeModal()},documentClick(t){const{blockId:o,modal:r}=(0,n.getContext)();if(!r.isOpen)return;const l=document.getElementById(o);if(!l)return;const s=l.querySelector('.wp-element-button[data-wp-on--click="actions.toggleModal"]');if(s&&(s===t.target||s.contains(t.target)))return;const a=l.querySelector(".activitypub-modal__frame");a&&!a.contains(t.target)&&e.closeModal()},positionModal(){const{blockId:e}=(0,n.getContext)(),t=document.getElementById(e);if(!t)return;const o=t.querySelector(".activitypub-modal__overlay");if(!o)return;o.style.top="",o.style.left="",o.style.right="",o.style.bottom="";const r=(0,n.getElement)().ref.getBoundingClientRect(),l=window.innerWidth,s=t.getBoundingClientRect();let a={top:r.bottom-s.top+8+"px",left:r.left-s.left-2+"px"};l-r.right<250&&(a.left="auto",a.right=s.right-r.right+"px"),Object.assign(o.style,a)},trapFocus(e){const t=e.querySelectorAll('a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input[type="text"]:not([disabled]):not([readonly]), input[type="radio"]:not([disabled]), input[type="checkbox"]:not([disabled]), select:not([disabled])'),o=t[0],r=t[t.length-1];o&&o.classList.contains("activitypub-modal__close")&&t.length>1?t[1].focus():o.focus(),e.addEventListener("keydown",(function(e){"Tab"!==e.key&&9!==e.keyCode||(e.shiftKey?document.activeElement===o&&(r.focus(),e.preventDefault()):document.activeElement===r&&(o.focus(),e.preventDefault()))}))}}})}();const{state:i,actions:c,callbacks:d}=(0,n.store)("activitypub/remote-reply",{state:{get remoteProfileUrl(){const{commentURL:e,template:t}=(0,n.getContext)();return t.replace("{uri}",encodeURIComponent(e))}},actions:{onReplyLinkKeydown(e){"Enter"!==e.key&&" "!==e.key||(e.preventDefault(),c.toggleModal(e))},copyToClipboard(){const e=(0,n.getContext)();navigator.clipboard.writeText(e.commentURL).then((()=>{e.copyButtonText=i.i18n.copied,setTimeout((()=>{e.copyButtonText=i.i18n.copy}),1e3)}),(e=>{console.error("Could not copy text: ",e)}))},updateRemoteProfile(e){const t=(0,n.getContext)();t.remoteProfile=e.target.value,t.isError=!1,t.errorMessage=""},onInputKeydown(e){if("Enter"===e.key)return e.preventDefault(),c.submitRemoteProfile()},*submitRemoteProfile(){const e=(0,n.getContext)(),{namespace:t,i18n:o}=i,r=e.remoteProfile.trim();if(!r)return e.isError=!0,void(e.errorMessage=o.emptyProfileError);if(!d.isHandle(r)&&!d.isUrl(r))return e.isError=!0,void(e.errorMessage=o.invalidProfileError);e.isLoading=!0,e.isError=!1,e.errorMessage="";const l=`/${t}/comments/${e.commentId}/remote-reply?resource=${encodeURIComponent(r)}`;try{const{template:t,url:o}=yield a({path:l});e.isLoading=!1,window.open(o,"_blank"),c.closeModal(),e.shouldSaveProfile&&(d.setStore({profileURL:r,template:t}),Object.assign(e,{hasRemoteUser:!0,profileURL:r,template:t}))}catch(t){console.error("Error submitting profile:",t),e.isLoading=!1,e.isError=!0,e.errorMessage=t.message||o.genericError}},toggleRememberProfile(){const e=(0,n.getContext)();e.shouldSaveProfile=!e.shouldSaveProfile},deleteRemoteUser(){const e=(0,n.getContext)();d.deleteStore(),e.hasRemoteUser=!1,e.profileURL="",e.template=""}},callbacks:{storageKey:"fediverse-remote-user",init(){const e=(0,n.getContext)(),{profileURL:t,template:o}=d.getStore();t&&o&&Object.assign(e,{hasRemoteUser:!0,profileURL:t,template:o})},getStore(){const e=localStorage.getItem(d.storageKey);return e?JSON.parse(e):{}},setStore(e){localStorage.setItem(d.storageKey,JSON.stringify(e))},deleteStore(){localStorage.removeItem(d.storageKey)},isHandle(e){const t=e.replace(/^@/,"").split("@");return 2===t.length&&d.isUrl(`https://${t[1]}`)},isUrl(e){try{return new URL(e),!0}catch(e){return!1}}}})}},n={};function l(e){var t=n[e];if(void 0!==t)return t.exports;var o=n[e]={exports:{}};return r[e](o,o.exports,l),o.exports}l.m=r,t=[],l.O=(e,o,r,n)=>{if(!o){var s=1/0;for(d=0;d=n)&&Object.keys(l.O).every((e=>l.O[e](o[i])))?o.splice(i--,1):(a=!1,n0&&t[d-1][2]>n;d--)t[d]=t[d-1];t[d]=[o,r,n]},l.d=(e,t)=>{for(var o in t)l.o(t,o)&&!l.o(e,o)&&Object.defineProperty(e,o,{enumerable:!0,get:t[o]})},l.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),o={915:0,771:0},l.O.j=e=>0===o[e];var s=l.O(void 0,[771],(()=>l(580)));s=l.O(s); \ No newline at end of file diff --git a/build/reply-intent/block.json b/build/blocks/reply-intent/block.json similarity index 100% rename from build/reply-intent/block.json rename to build/blocks/reply-intent/block.json diff --git a/build/reply-intent/plugin.asset.php b/build/blocks/reply-intent/plugin.asset.php similarity index 100% rename from build/reply-intent/plugin.asset.php rename to build/blocks/reply-intent/plugin.asset.php diff --git a/build/reply-intent/plugin.js b/build/blocks/reply-intent/plugin.js similarity index 100% rename from build/reply-intent/plugin.js rename to build/blocks/reply-intent/plugin.js diff --git a/build/reply/block.json b/build/blocks/reply/block.json similarity index 100% rename from build/reply/block.json rename to build/blocks/reply/block.json diff --git a/build/reply/index-rtl.css b/build/blocks/reply/index-rtl.css similarity index 75% rename from build/reply/index-rtl.css rename to build/blocks/reply/index-rtl.css index 44552c85b..72e7d4505 100644 --- a/build/reply/index-rtl.css +++ b/build/blocks/reply/index-rtl.css @@ -1 +1 @@ -.activitypub-embed{background:#fff;border:1px solid #e6e6e6;border-radius:12px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;max-width:100%;padding:0}.activitypub-reply-block .activitypub-embed{margin:1em 0}.activitypub-embed-header{align-items:center;display:flex;gap:10px;padding:15px}.activitypub-embed-header img{border-radius:50%;height:48px;width:48px}.activitypub-embed-header-text{flex-grow:1}.activitypub-embed-header-text h2{color:#000;font-size:15px;font-weight:600;margin:0;padding:0}.activitypub-embed-header-text .ap-account{color:#687684;font-size:14px;text-decoration:none}.activitypub-embed-content{padding:0 15px 15px}.activitypub-embed-content .ap-title{color:#000;font-size:23px;font-weight:600;margin:0 0 10px;padding:0}.activitypub-embed-content .ap-subtitle{color:#000;font-size:15px;margin:0 0 15px}.activitypub-embed-content .ap-preview{border:1px solid #e6e6e6}.activitypub-embed-content .ap-preview img{display:block;height:auto}.activitypub-embed-content .ap-preview{border-radius:8px;box-sizing:border-box;display:grid;gap:2px;grid-template-columns:1fr 1fr;grid-template-rows:1fr 1fr;margin:1em 0 0;min-height:64px;overflow:hidden;position:relative;width:100%}.activitypub-embed-content .ap-preview.layout-1{grid-template-columns:1fr;grid-template-rows:1fr}.activitypub-embed-content .ap-preview.layout-2{aspect-ratio:auto;grid-template-rows:1fr;height:auto}.activitypub-embed-content .ap-preview.layout-3>img:first-child{grid-row:span 2}.activitypub-embed-content .ap-preview img{border:0;box-sizing:border-box;display:inline-block;height:100%;-o-object-fit:cover;object-fit:cover;overflow:hidden;position:relative;width:100%}.activitypub-embed-content .ap-preview audio,.activitypub-embed-content .ap-preview video{display:block;grid-column:1/span 2;max-width:100%}.activitypub-embed-content .ap-preview audio{width:100%}.activitypub-embed-content .ap-preview-text{padding:15px}.activitypub-embed-meta{border-top:1px solid #e6e6e6;color:#687684;display:flex;font-size:13px;gap:15px;padding:15px}.activitypub-embed-meta .ap-stat{align-items:center;display:flex;gap:5px}@media only screen and (max-width:399px){.activitypub-embed-meta span.ap-stat{display:none!important}}.activitypub-embed-meta a.ap-stat{color:inherit;text-decoration:none}.activitypub-embed-meta strong{color:#000;font-weight:600}.activitypub-embed-meta .ap-stat-label{color:#687684}.wp-block-activitypub-reply .components-spinner{height:12px;margin-bottom:0;margin-top:0;width:12px} +.activitypub-embed{background:#fff;border:1px solid #e6e6e6;border-radius:12px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;max-width:100%;padding:0}.activitypub-reply-block .activitypub-embed{margin:1em 0}.activitypub-embed-header{align-items:center;display:flex;gap:10px;padding:15px}.activitypub-embed-header img{border-radius:50%;height:48px;width:48px}.activitypub-embed-header-text{flex-grow:1}.activitypub-embed-header-text h2{color:#000;font-size:15px;font-weight:600;margin:0;padding:0}.activitypub-embed-header-text .ap-account{color:#687684;font-size:14px;text-decoration:none}.activitypub-embed-content{padding:0 15px 15px}.activitypub-embed-content .ap-title{color:#000;font-size:23px;font-weight:600;margin:0 0 10px;padding:0}.activitypub-embed-content .ap-subtitle{color:#000;font-size:15px;margin:0 0 15px}.activitypub-embed-content .ap-preview{border:1px solid #e6e6e6}.activitypub-embed-content .ap-preview img{display:block;height:auto}.activitypub-embed-content .ap-preview{border-radius:8px;box-sizing:border-box;display:grid;gap:2px;grid-template-columns:1fr 1fr;grid-template-rows:1fr 1fr;margin:1em 0 0;min-height:64px;overflow:hidden;position:relative;width:100%}.activitypub-embed-content .ap-preview.layout-1{grid-template-columns:1fr;grid-template-rows:1fr}.activitypub-embed-content .ap-preview.layout-2{aspect-ratio:auto;grid-template-rows:1fr;height:auto}.activitypub-embed-content .ap-preview.layout-3>img:first-child{grid-row:span 2}.activitypub-embed-content .ap-preview img{border:0;box-sizing:border-box;display:inline-block;height:100%;-o-object-fit:cover;object-fit:cover;overflow:hidden;position:relative;width:100%}.activitypub-embed-content .ap-preview audio,.activitypub-embed-content .ap-preview video{display:block;grid-column:1/span 2;max-width:100%}.activitypub-embed-content .ap-preview audio{width:100%}.activitypub-embed-content .ap-preview-text{padding:15px}.activitypub-embed-meta{border-top:1px solid #e6e6e6;color:#687684;display:flex;font-size:13px;gap:15px;padding:15px}.activitypub-embed-meta .ap-stat{align-items:center;display:flex;gap:5px}@media only screen and (max-width:399px){.activitypub-embed-meta span.ap-stat{display:none!important}}.activitypub-embed-meta a.ap-stat{color:inherit;text-decoration:none}.activitypub-embed-meta strong{color:#000;font-weight:600}.activitypub-embed-meta .ap-stat-label{color:#687684}.activitypub-embed-container{margin-top:1em;min-height:100px;pointer-events:none;position:relative;-webkit-user-select:none;-moz-user-select:none;user-select:none}.activitypub-embed-loading{align-items:center;display:flex;justify-content:center}.activitypub-embed-container .wp-block-embed{pointer-events:none!important}.activitypub-embed-preview,.activitypub-embed-preview iframe{pointer-events:none}.activitypub-reply-display{margin:1em 0}.activitypub-reply-display p{margin:0}.activitypub-reply-display a{color:#2271b1;text-decoration:none}.activitypub-reply-display a:hover{color:#135e96;text-decoration:underline}.wp-block-activitypub-reply .components-spinner{height:12px;margin-bottom:0;margin-top:0;width:12px} diff --git a/build/reply/index.asset.php b/build/blocks/reply/index.asset.php similarity index 82% rename from build/reply/index.asset.php rename to build/blocks/reply/index.asset.php index 9f4117fee..9f743df2d 100644 --- a/build/reply/index.asset.php +++ b/build/blocks/reply/index.asset.php @@ -1 +1 @@ - array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives', 'wp-url'), 'version' => '78dbc26b5e405051df4a'); + array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives', 'wp-url'), 'version' => '4b89612674860b9394e5'); diff --git a/build/reply/index.css b/build/blocks/reply/index.css similarity index 75% rename from build/reply/index.css rename to build/blocks/reply/index.css index 44552c85b..72e7d4505 100644 --- a/build/reply/index.css +++ b/build/blocks/reply/index.css @@ -1 +1 @@ -.activitypub-embed{background:#fff;border:1px solid #e6e6e6;border-radius:12px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;max-width:100%;padding:0}.activitypub-reply-block .activitypub-embed{margin:1em 0}.activitypub-embed-header{align-items:center;display:flex;gap:10px;padding:15px}.activitypub-embed-header img{border-radius:50%;height:48px;width:48px}.activitypub-embed-header-text{flex-grow:1}.activitypub-embed-header-text h2{color:#000;font-size:15px;font-weight:600;margin:0;padding:0}.activitypub-embed-header-text .ap-account{color:#687684;font-size:14px;text-decoration:none}.activitypub-embed-content{padding:0 15px 15px}.activitypub-embed-content .ap-title{color:#000;font-size:23px;font-weight:600;margin:0 0 10px;padding:0}.activitypub-embed-content .ap-subtitle{color:#000;font-size:15px;margin:0 0 15px}.activitypub-embed-content .ap-preview{border:1px solid #e6e6e6}.activitypub-embed-content .ap-preview img{display:block;height:auto}.activitypub-embed-content .ap-preview{border-radius:8px;box-sizing:border-box;display:grid;gap:2px;grid-template-columns:1fr 1fr;grid-template-rows:1fr 1fr;margin:1em 0 0;min-height:64px;overflow:hidden;position:relative;width:100%}.activitypub-embed-content .ap-preview.layout-1{grid-template-columns:1fr;grid-template-rows:1fr}.activitypub-embed-content .ap-preview.layout-2{aspect-ratio:auto;grid-template-rows:1fr;height:auto}.activitypub-embed-content .ap-preview.layout-3>img:first-child{grid-row:span 2}.activitypub-embed-content .ap-preview img{border:0;box-sizing:border-box;display:inline-block;height:100%;-o-object-fit:cover;object-fit:cover;overflow:hidden;position:relative;width:100%}.activitypub-embed-content .ap-preview audio,.activitypub-embed-content .ap-preview video{display:block;grid-column:1/span 2;max-width:100%}.activitypub-embed-content .ap-preview audio{width:100%}.activitypub-embed-content .ap-preview-text{padding:15px}.activitypub-embed-meta{border-top:1px solid #e6e6e6;color:#687684;display:flex;font-size:13px;gap:15px;padding:15px}.activitypub-embed-meta .ap-stat{align-items:center;display:flex;gap:5px}@media only screen and (max-width:399px){.activitypub-embed-meta span.ap-stat{display:none!important}}.activitypub-embed-meta a.ap-stat{color:inherit;text-decoration:none}.activitypub-embed-meta strong{color:#000;font-weight:600}.activitypub-embed-meta .ap-stat-label{color:#687684}.wp-block-activitypub-reply .components-spinner{height:12px;margin-bottom:0;margin-top:0;width:12px} +.activitypub-embed{background:#fff;border:1px solid #e6e6e6;border-radius:12px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;max-width:100%;padding:0}.activitypub-reply-block .activitypub-embed{margin:1em 0}.activitypub-embed-header{align-items:center;display:flex;gap:10px;padding:15px}.activitypub-embed-header img{border-radius:50%;height:48px;width:48px}.activitypub-embed-header-text{flex-grow:1}.activitypub-embed-header-text h2{color:#000;font-size:15px;font-weight:600;margin:0;padding:0}.activitypub-embed-header-text .ap-account{color:#687684;font-size:14px;text-decoration:none}.activitypub-embed-content{padding:0 15px 15px}.activitypub-embed-content .ap-title{color:#000;font-size:23px;font-weight:600;margin:0 0 10px;padding:0}.activitypub-embed-content .ap-subtitle{color:#000;font-size:15px;margin:0 0 15px}.activitypub-embed-content .ap-preview{border:1px solid #e6e6e6}.activitypub-embed-content .ap-preview img{display:block;height:auto}.activitypub-embed-content .ap-preview{border-radius:8px;box-sizing:border-box;display:grid;gap:2px;grid-template-columns:1fr 1fr;grid-template-rows:1fr 1fr;margin:1em 0 0;min-height:64px;overflow:hidden;position:relative;width:100%}.activitypub-embed-content .ap-preview.layout-1{grid-template-columns:1fr;grid-template-rows:1fr}.activitypub-embed-content .ap-preview.layout-2{aspect-ratio:auto;grid-template-rows:1fr;height:auto}.activitypub-embed-content .ap-preview.layout-3>img:first-child{grid-row:span 2}.activitypub-embed-content .ap-preview img{border:0;box-sizing:border-box;display:inline-block;height:100%;-o-object-fit:cover;object-fit:cover;overflow:hidden;position:relative;width:100%}.activitypub-embed-content .ap-preview audio,.activitypub-embed-content .ap-preview video{display:block;grid-column:1/span 2;max-width:100%}.activitypub-embed-content .ap-preview audio{width:100%}.activitypub-embed-content .ap-preview-text{padding:15px}.activitypub-embed-meta{border-top:1px solid #e6e6e6;color:#687684;display:flex;font-size:13px;gap:15px;padding:15px}.activitypub-embed-meta .ap-stat{align-items:center;display:flex;gap:5px}@media only screen and (max-width:399px){.activitypub-embed-meta span.ap-stat{display:none!important}}.activitypub-embed-meta a.ap-stat{color:inherit;text-decoration:none}.activitypub-embed-meta strong{color:#000;font-weight:600}.activitypub-embed-meta .ap-stat-label{color:#687684}.activitypub-embed-container{margin-top:1em;min-height:100px;pointer-events:none;position:relative;-webkit-user-select:none;-moz-user-select:none;user-select:none}.activitypub-embed-loading{align-items:center;display:flex;justify-content:center}.activitypub-embed-container .wp-block-embed{pointer-events:none!important}.activitypub-embed-preview,.activitypub-embed-preview iframe{pointer-events:none}.activitypub-reply-display{margin:1em 0}.activitypub-reply-display p{margin:0}.activitypub-reply-display a{color:#2271b1;text-decoration:none}.activitypub-reply-display a:hover{color:#135e96;text-decoration:underline}.wp-block-activitypub-reply .components-spinner{height:12px;margin-bottom:0;margin-top:0;width:12px} diff --git a/build/blocks/reply/index.js b/build/blocks/reply/index.js new file mode 100644 index 000000000..e9f5749b6 --- /dev/null +++ b/build/blocks/reply/index.js @@ -0,0 +1 @@ +(()=>{"use strict";var e={20:(e,t,r)=>{var n=r(609),o=Symbol.for("react.element"),a=(Symbol.for("react.fragment"),Object.prototype.hasOwnProperty),l=n.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,i={key:!0,ref:!0,__self:!0,__source:!0};t.jsx=function(e,t,r){var n,c={},s=null,d=null;for(n in void 0!==r&&(s=""+r),void 0!==t.key&&(s=""+t.key),void 0!==t.ref&&(d=t.ref),t)a.call(t,n)&&!i.hasOwnProperty(n)&&(c[n]=t[n]);if(e&&e.defaultProps)for(n in t=e.defaultProps)void 0===c[n]&&(c[n]=t[n]);return{$$typeof:o,type:e,key:s,ref:d,props:c,_owner:l.current}}},609:e=>{e.exports=window.React},848:(e,t,r)=>{e.exports=r(20)}},t={};function r(n){var o=t[n];if(void 0!==o)return o.exports;var a=t[n]={exports:{}};return e[n](a,a.exports,r),a.exports}r.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return r.d(t,{a:t}),t},r.d=(e,t)=>{for(var n in t)r.o(t,n)&&!r.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},r.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t);const n=window.wp.blocks,o=window.wp.primitives;var a=r(848);const l=(0,a.jsx)(o.SVG,{viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg",children:(0,a.jsx)(o.Path,{d:"M6.68822 10.625L6.24878 11.0649L5.5 11.8145L5.5 5.5L12.5 5.5V8L14 6.5V5C14 4.44772 13.5523 4 13 4H5C4.44772 4 4 4.44771 4 5V13.5247C4 13.8173 4.16123 14.086 4.41935 14.2237C4.72711 14.3878 5.10601 14.3313 5.35252 14.0845L7.31 12.125H8.375L9.875 10.625H7.31H6.68822ZM14.5605 10.4983L11.6701 13.75H16.9975C17.9963 13.75 18.7796 14.1104 19.3553 14.7048C19.9095 15.2771 20.2299 16.0224 20.4224 16.7443C20.7645 18.0276 20.7543 19.4618 20.7487 20.2544C20.7481 20.345 20.7475 20.4272 20.7475 20.4999L19.2475 20.5001C19.2475 20.4191 19.248 20.3319 19.2484 20.2394V20.2394C19.2526 19.4274 19.259 18.2035 18.973 17.1307C18.8156 16.5401 18.586 16.0666 18.2778 15.7483C17.9909 15.4521 17.5991 15.25 16.9975 15.25H11.8106L14.5303 17.9697L13.4696 19.0303L8.96956 14.5303L13.4394 9.50171L14.5605 10.4983Z"})});var i=r(609);const c=window.wp.blockEditor,s=window.wp.components,d=window.wp.i18n,u=window.wp.element,m=window.wp.compose,p=window.wp.apiFetch;var f=r.n(p);const h=window.wp.url,w=window.wp.data;function b({html:e}){const t=(0,u.useRef)(null),[r,n]=(0,u.useState)(300),o=(0,u.useRef)(300),a=(0,u.useCallback)((()=>{if(t.current)try{const e=t.current;let r=300;try{e.contentDocument&&e.contentDocument.body?r=e.contentDocument.body.scrollHeight:e.contentWindow&&e.contentWindow.document&&e.contentWindow.document.body&&(r=e.contentWindow.document.body.scrollHeight)}catch(e){console.log("Could not access iframe content document:",e)}r+=5,Math.abs(r-o.current)>5&&(o.current=r,n(r))}catch(e){console.error("Error adjusting iframe height:",e)}}),[]),l=(0,u.useCallback)((()=>{if(t.current)try{a()}catch(e){console.error("Error setting up iframe height adjustment:",e)}}),[a]);return(0,u.useEffect)((()=>{t.current&&t.current.addEventListener("load",l);const e=setInterval(a,1e3);return()=>{clearInterval(e),t.current&&t.current.removeEventListener("load",l)}}),[l,a]),(0,u.useEffect)((()=>{if(t.current){const e=setTimeout((()=>{a()}),100);return()=>clearTimeout(e)}}),[e,a]),{iframeRef:t,iframeHeight:r,adjustIframeHeight:a,handleIframeLoad:l}}const y={class:"className",frameborder:"frameBorder",allowfullscreen:"allowFullScreen",allowtransparency:"allowTransparency",marginheight:"marginHeight",marginwidth:"marginWidth"};function v({onClick:e}){return(0,i.createElement)("div",{className:"activitypub-embed-overlay",onClick:e,style:{position:"absolute",top:0,left:0,width:"100%",height:"100%",cursor:"pointer",zIndex:1}})}function _({html:e,onSelectBlock:t}){const r=(0,u.useRef)(),[n,o]=(0,u.useState)(282),[a,l]=(0,u.useState)(!1),c=(0,u.useCallback)((()=>{const t=(new window.DOMParser).parseFromString(e,"text/html").querySelector("iframe"),r={};return t?(Array.from(t.attributes).forEach((({name:e,value:t})=>{"style"!==e&&(r[y[e]||e]=t)})),r):r}),[e]),s=c();return(0,u.useEffect)((()=>{if(!r.current)return;const{ownerDocument:e}=r.current,{defaultView:t}=e;function n({data:{secret:e,message:t,value:r}={}}){"height"===t&&e===s["data-secret"]&&o(r)}return t.addEventListener("message",n),()=>{t.removeEventListener("message",n)}}),[s]),s.src?(0,i.createElement)("div",{className:"wp-block-embed__wrapper",style:{position:"relative"}},(0,i.createElement)("iframe",{ref:r,title:s.title||(0,d.__)("Embedded WordPress content","activitypub"),...s,height:n,style:{width:"100%",maxWidth:"100%"}}),!a&&(0,i.createElement)(v,{onClick:t})):(0,i.createElement)("div",{className:"wp-block-embed__wrapper",style:{position:"relative"}},(0,i.createElement)("div",{dangerouslySetInnerHTML:{__html:e}}),(0,i.createElement)(v,{onClick:t}))}function g({html:e,onClick:t,isSelected:r}){const{iframeRef:n,iframeHeight:o,adjustIframeHeight:a,handleIframeLoad:l}=b({html:e}),c=(0,u.useCallback)((()=>`\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\t${e}\n\t\t\t\n\t\t\t\n\t\t`),[e]);return(0,i.createElement)("div",{className:"wp-block-embed__wrapper",style:{position:"relative"}},(0,i.createElement)("iframe",{ref:n,srcDoc:c(),sandbox:"allow-scripts allow-same-origin allow-popups allow-forms",style:{width:"100%",height:`${o}px`,border:"none",overflow:"hidden"},onLoad:l}),r&&(0,i.createElement)("div",{onClick:t,style:{position:"absolute",top:0,left:0,width:"100%",height:"100%",cursor:"pointer",zIndex:1,display:r?"block":"none"}}))}const E={default:(0,d.__)("Enter the URL of a post from the Fediverse (Mastodon, Pixelfed, etc.) that you want to reply to.","activitypub"),checking:()=>(0,i.createElement)(i.Fragment,null,(0,i.createElement)(s.Spinner,null)," "+(0,d.__)("Checking if this URL supports ActivityPub replies...","activitypub")),valid:(0,d.__)("The author will be notified of your response.","activitypub"),error:(0,d.__)("This URL probably won’t receive your reply. We’ll still try.","activitypub")},k={valid:(0,d.__)("This post can be embedded with your reply.","activitypub"),invalid:(0,d.__)("This post cannot be embedded.","activitypub")};(0,n.registerBlockType)("activitypub/reply",{edit:function({attributes:e,setAttributes:t,clientId:r,isSelected:n}){const{url:o}=e,{namespace:a}=window._activityPubOptions||{},[l,p]=(0,u.useState)(E.default),[y,v]=(0,u.useState)(!1),[C,L]=(0,u.useState)(!1),[S,P]=(0,u.useState)(!1),[x,R]=(0,u.useState)(!0===e.embedPost||!o),[T,H]=(0,u.useState)(null),{iframeRef:I,iframeHeight:O,adjustIframeHeight:D,handleIframeLoad:j}=b({html:T}),{insertAfterBlock:B,removeBlock:N}=(0,w.useDispatch)("core/block-editor"),W=(0,c.useBlockProps)(),F=(0,u.useRef)(),M=((0,u.useRef)(),(0,u.useRef)(x)),U=()=>{setTimeout((()=>F.current?.focus()),50)};(0,u.useEffect)((()=>{M.current=x}),[x]);const A=(0,u.useCallback)((e=>{v(e),M.current&&e&&t({embedPost:!0})}),[t]),V=(e=!1)=>{P(e),v(!1),L(!1),H("")},$=(0,m.useDebounce)((async e=>{if(e)try{V(!0),p(E.checking());const t=await f()({path:(0,h.addQueryArgs)(`${a}/url/validate`,{url:e})});A(t.is_activitypub),L(t.is_real_oembed),H(t.html||""),p(E.valid)}catch(e){V(),p(E.error)}finally{P(!1)}else V()}),250);return(0,u.useEffect)((()=>{o&&$(o)}),[o]),(0,i.createElement)(i.Fragment,null,(0,i.createElement)(c.InspectorControls,null,(0,i.createElement)(s.PanelBody,{title:(0,d.__)("Settings","activitypub")},(0,i.createElement)(s.ToggleControl,{label:(0,d.__)("Embed Post","activitypub"),checked:e.embedPost,onChange:e=>{t({embedPost:e}),R(e)},disabled:!y,help:y?k.valid:k.invalid}))),(0,i.createElement)("div",{...W},n&&(0,i.createElement)(s.TextControl,{label:(0,d.__)("Your post is a reply to the following URL","activitypub"),value:o,onChange:e=>t({url:e}),help:l,onKeyDown:t=>{"Enter"===t.key&&B(r),!e.url&&["Backspace","Delete"].includes(t.key)&&N(r)},ref:F}),y&&e.embedPost&&T&&(0,i.createElement)("div",{className:"activitypub-embed-container"},C&&(Y=T)&&(Y.includes("wp-embedded-content")||Y.includes("wp-embed/")||Y.includes('class="wp-embed"'))?(0,i.createElement)(_,{html:T,onSelectBlock:U}):(0,i.createElement)(g,{html:T,onClick:U,isSelected:n})),o&&(!e.embedPost||!T)&&(0,i.createElement)("div",{className:"activitypub-reply-block-editor__preview",contentEditable:!1,onClick:U,style:{cursor:"pointer"}},(0,i.createElement)("a",{href:o,className:"u-in-reply-to",target:"_blank",rel:"noreferrer"},"↬"+o.replace(/^https?:\/\//,"")))));var Y},save:()=>null,icon:l})})(); \ No newline at end of file diff --git a/build/editor-plugin/plugin.asset.php b/build/editor-plugin/plugin.asset.php deleted file mode 100644 index 35f04faca..000000000 --- a/build/editor-plugin/plugin.asset.php +++ /dev/null @@ -1 +0,0 @@ - array('react', 'wp-components', 'wp-core-data', 'wp-data', 'wp-editor', 'wp-element', 'wp-i18n', 'wp-plugins', 'wp-primitives', 'wp-url'), 'version' => 'c4ec9c3a1f0d32bd9118'); diff --git a/build/editor-plugin/plugin.js b/build/editor-plugin/plugin.js deleted file mode 100644 index a282eff0b..000000000 --- a/build/editor-plugin/plugin.js +++ /dev/null @@ -1 +0,0 @@ -(()=>{"use strict";var e={20:(e,t,i)=>{var n=i(609),a=Symbol.for("react.element"),o=(Symbol.for("react.fragment"),Object.prototype.hasOwnProperty),l=n.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,r={key:!0,ref:!0,__self:!0,__source:!0};t.jsx=function(e,t,i){var n,c={},s=null,u=null;for(n in void 0!==i&&(s=""+i),void 0!==t.key&&(s=""+t.key),void 0!==t.ref&&(u=t.ref),t)o.call(t,n)&&!r.hasOwnProperty(n)&&(c[n]=t[n]);if(e&&e.defaultProps)for(n in t=e.defaultProps)void 0===c[n]&&(c[n]=t[n]);return{$$typeof:a,type:e,key:s,ref:u,props:c,_owner:l.current}}},609:e=>{e.exports=window.React},848:(e,t,i)=>{e.exports=i(20)}},t={};function i(n){var a=t[n];if(void 0!==a)return a.exports;var o=t[n]={exports:{}};return e[n](o,o.exports,i),o.exports}var n=i(609);const a=window.wp.editor,o=window.wp.plugins,l=window.wp.components,r=window.wp.element,c=(0,r.forwardRef)((function({icon:e,size:t=24,...i},n){return(0,r.cloneElement)(e,{width:t,height:t,...i,ref:n})})),s=window.wp.primitives;var u=i(848);const p=(0,u.jsx)(s.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,u.jsx)(s.Path,{d:"M12 3.3c-4.8 0-8.8 3.9-8.8 8.8 0 4.8 3.9 8.8 8.8 8.8 4.8 0 8.8-3.9 8.8-8.8s-4-8.8-8.8-8.8zm6.5 5.5h-2.6C15.4 7.3 14.8 6 14 5c2 .6 3.6 2 4.5 3.8zm.7 3.2c0 .6-.1 1.2-.2 1.8h-2.9c.1-.6.1-1.2.1-1.8s-.1-1.2-.1-1.8H19c.2.6.2 1.2.2 1.8zM12 18.7c-1-.7-1.8-1.9-2.3-3.5h4.6c-.5 1.6-1.3 2.9-2.3 3.5zm-2.6-4.9c-.1-.6-.1-1.1-.1-1.8 0-.6.1-1.2.1-1.8h5.2c.1.6.1 1.1.1 1.8s-.1 1.2-.1 1.8H9.4zM4.8 12c0-.6.1-1.2.2-1.8h2.9c-.1.6-.1 1.2-.1 1.8 0 .6.1 1.2.1 1.8H5c-.2-.6-.2-1.2-.2-1.8zM12 5.3c1 .7 1.8 1.9 2.3 3.5H9.7c.5-1.6 1.3-2.9 2.3-3.5zM10 5c-.8 1-1.4 2.3-1.8 3.8H5.5C6.4 7 8 5.6 10 5zM5.5 15.3h2.6c.4 1.5 1 2.8 1.8 3.7-1.8-.6-3.5-2-4.4-3.7zM14 19c.8-1 1.4-2.2 1.8-3.7h2.6C17.6 17 16 18.4 14 19z"})}),v=(0,u.jsx)(s.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,u.jsx)(s.Path,{d:"M15.5 9.5a1 1 0 100-2 1 1 0 000 2zm0 1.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5zm-2.25 6v-2a2.75 2.75 0 00-2.75-2.75h-4A2.75 2.75 0 003.75 15v2h1.5v-2c0-.69.56-1.25 1.25-1.25h4c.69 0 1.25.56 1.25 1.25v2h1.5zm7-2v2h-1.5v-2c0-.69-.56-1.25-1.25-1.25H15v-1.5h2.5A2.75 2.75 0 0120.25 15zM9.5 8.5a1 1 0 11-2 0 1 1 0 012 0zm1.5 0a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z",fillRule:"evenodd"})}),w=(0,u.jsx)(s.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,u.jsx)(s.Path,{d:"M19.5 4.5h-7V6h4.44l-5.97 5.97 1.06 1.06L18 7.06v4.44h1.5v-7Zm-13 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-3H17v3a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h3V5.5h-3Z"})}),d=window.wp.data,_=window.wp.coreData,m=window.wp.url,h=window.wp.i18n,b=(0,n.createElement)(s.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},(0,n.createElement)(s.Path,{fillRule:"evenodd",clipRule:"evenodd",d:"M12 18.5A6.5 6.5 0 0 1 6.93 7.931l9.139 9.138A6.473 6.473 0 0 1 12 18.5Zm5.123-2.498a6.5 6.5 0 0 0-9.124-9.124l9.124 9.124ZM4 12a8 8 0 1 1 16 0 8 8 0 0 1-16 0Z"}));(0,o.registerPlugin)("activitypub-editor-plugin",{render:()=>{var e,t;const i=(0,d.useSelect)((e=>e("core/editor").getCurrentPostType()),[]),[o,r]=(0,_.useEntityProp)("postType",i,"meta"),s={verticalAlign:"middle",gap:"4px",justifyContent:"start",display:"inline-flex",alignItems:"center"},u=(e,t,i)=>(0,n.createElement)(l.Tooltip,{text:i},(0,n.createElement)(l.__experimentalText,{style:s},(0,n.createElement)(c,{icon:e}),t));return"wp_block"===i?null:(0,n.createElement)(a.PluginDocumentSettingPanel,{name:"activitypub",title:(0,h.__)("Fediverse ⁂","activitypub")},(0,n.createElement)(l.TextControl,{label:(0,h.__)("Content Warning","activitypub"),value:o?.activitypub_content_warning,onChange:e=>{r({...o,activitypub_content_warning:e})},placeholder:(0,h.__)("Optional content warning","activitypub"),help:(0,h.__)("Content warnings do not change the content on your site, only in the fediverse.","activitypub")}),(0,n.createElement)(l.RangeControl,{label:(0,h.__)("Maximum Image Attachments","activitypub"),value:null!==(e=null!==(t=o?.activitypub_max_image_attachments)&&void 0!==t?t:window._activityPubOptions?.maxImageAttachments)&&void 0!==e?e:4,onChange:e=>{r({...o,activitypub_max_image_attachments:e})},min:0,max:10,help:(0,h.__)("Maximum number of image attachments to include when sharing to the fediverse.","activitypub")}),(0,n.createElement)(l.RadioControl,{label:(0,h.__)("Visibility","activitypub"),help:(0,h.__)("This adjusts the visibility of a post in the fediverse, but note that it won't affect how the post appears on the blog.","activitypub"),selected:o?.activitypub_content_visibility||"public",options:[{label:u(p,(0,h.__)("Public","activitypub"),(0,h.__)("Post will be visible to everyone and appear in public timelines.","activitypub")),value:"public"},{label:u(v,(0,h.__)("Quiet public","activitypub"),(0,h.__)("Post will be visible to everyone but will not appear in public timelines.","activitypub")),value:"quiet_public"},{label:u(b,(0,h.__)("Do not federate","activitypub"),(0,h.__)("Post will not be shared to the Fediverse.","activitypub")),value:"local"}],onChange:e=>{r({...o,activitypub_content_visibility:e})},className:"activitypub-visibility"}))}}),(0,o.registerPlugin)("activitypub-editor-preview",{render:()=>{const e=(0,d.useSelect)((e=>e("core/editor").getCurrentPost().status));return(0,n.createElement)(n.Fragment,null,a.PluginPreviewMenuItem?(0,n.createElement)(a.PluginPreviewMenuItem,{onClick:()=>function(){const e=(0,d.select)("core/editor").getEditedPostPreviewLink(),t=(0,m.addQueryArgs)(e,{activitypub:"true"});window.open(t,"_blank")}(),icon:w,disabled:"auto-draft"===e},(0,h.__)("Fediverse preview ⁂","activitypub")):null)}})})(); \ No newline at end of file diff --git a/build/embed/embed-rtl.css b/build/embed/embed-rtl.css new file mode 100644 index 000000000..703b8b72f --- /dev/null +++ b/build/embed/embed-rtl.css @@ -0,0 +1 @@ +.activitypub-embed{background:#fff;border:1px solid #e6e6e6;border-radius:12px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;max-width:100%;padding:0}.activitypub-reply-block .activitypub-embed{margin:1em 0}.activitypub-embed-header{align-items:center;display:flex;gap:10px;padding:15px}.activitypub-embed-header img{border-radius:50%;height:48px;width:48px}.activitypub-embed-header-text{flex-grow:1}.activitypub-embed-header-text h2{color:#000;font-size:15px;font-weight:600;margin:0;padding:0}.activitypub-embed-header-text .ap-account{color:#687684;font-size:14px;text-decoration:none}.activitypub-embed-content{padding:0 15px 15px}.activitypub-embed-content .ap-title{color:#000;font-size:23px;font-weight:600;margin:0 0 10px;padding:0}.activitypub-embed-content .ap-subtitle{color:#000;font-size:15px;margin:0 0 15px}.activitypub-embed-content .ap-preview{border:1px solid #e6e6e6}.activitypub-embed-content .ap-preview img{display:block;height:auto}.activitypub-embed-content .ap-preview{border-radius:8px;box-sizing:border-box;display:grid;gap:2px;grid-template-columns:1fr 1fr;grid-template-rows:1fr 1fr;margin:1em 0 0;min-height:64px;overflow:hidden;position:relative;width:100%}.activitypub-embed-content .ap-preview.layout-1{grid-template-columns:1fr;grid-template-rows:1fr}.activitypub-embed-content .ap-preview.layout-2{aspect-ratio:auto;grid-template-rows:1fr;height:auto}.activitypub-embed-content .ap-preview.layout-3>img:first-child{grid-row:span 2}.activitypub-embed-content .ap-preview img{border:0;box-sizing:border-box;display:inline-block;height:100%;-o-object-fit:cover;object-fit:cover;overflow:hidden;position:relative;width:100%}.activitypub-embed-content .ap-preview audio,.activitypub-embed-content .ap-preview video{display:block;grid-column:1/span 2;max-width:100%}.activitypub-embed-content .ap-preview audio{width:100%}.activitypub-embed-content .ap-preview-text{padding:15px}.activitypub-embed-meta{border-top:1px solid #e6e6e6;color:#687684;display:flex;font-size:13px;gap:15px;padding:15px}.activitypub-embed-meta .ap-stat{align-items:center;display:flex;gap:5px}@media only screen and (max-width:399px){.activitypub-embed-meta span.ap-stat{display:none!important}}.activitypub-embed-meta a.ap-stat{color:inherit;text-decoration:none}.activitypub-embed-meta strong{color:#000;font-weight:600}.activitypub-embed-meta .ap-stat-label{color:#687684} diff --git a/build/embed/embed.css b/build/embed/embed.css new file mode 100644 index 000000000..703b8b72f --- /dev/null +++ b/build/embed/embed.css @@ -0,0 +1 @@ +.activitypub-embed{background:#fff;border:1px solid #e6e6e6;border-radius:12px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;max-width:100%;padding:0}.activitypub-reply-block .activitypub-embed{margin:1em 0}.activitypub-embed-header{align-items:center;display:flex;gap:10px;padding:15px}.activitypub-embed-header img{border-radius:50%;height:48px;width:48px}.activitypub-embed-header-text{flex-grow:1}.activitypub-embed-header-text h2{color:#000;font-size:15px;font-weight:600;margin:0;padding:0}.activitypub-embed-header-text .ap-account{color:#687684;font-size:14px;text-decoration:none}.activitypub-embed-content{padding:0 15px 15px}.activitypub-embed-content .ap-title{color:#000;font-size:23px;font-weight:600;margin:0 0 10px;padding:0}.activitypub-embed-content .ap-subtitle{color:#000;font-size:15px;margin:0 0 15px}.activitypub-embed-content .ap-preview{border:1px solid #e6e6e6}.activitypub-embed-content .ap-preview img{display:block;height:auto}.activitypub-embed-content .ap-preview{border-radius:8px;box-sizing:border-box;display:grid;gap:2px;grid-template-columns:1fr 1fr;grid-template-rows:1fr 1fr;margin:1em 0 0;min-height:64px;overflow:hidden;position:relative;width:100%}.activitypub-embed-content .ap-preview.layout-1{grid-template-columns:1fr;grid-template-rows:1fr}.activitypub-embed-content .ap-preview.layout-2{aspect-ratio:auto;grid-template-rows:1fr;height:auto}.activitypub-embed-content .ap-preview.layout-3>img:first-child{grid-row:span 2}.activitypub-embed-content .ap-preview img{border:0;box-sizing:border-box;display:inline-block;height:100%;-o-object-fit:cover;object-fit:cover;overflow:hidden;position:relative;width:100%}.activitypub-embed-content .ap-preview audio,.activitypub-embed-content .ap-preview video{display:block;grid-column:1/span 2;max-width:100%}.activitypub-embed-content .ap-preview audio{width:100%}.activitypub-embed-content .ap-preview-text{padding:15px}.activitypub-embed-meta{border-top:1px solid #e6e6e6;color:#687684;display:flex;font-size:13px;gap:15px;padding:15px}.activitypub-embed-meta .ap-stat{align-items:center;display:flex;gap:5px}@media only screen and (max-width:399px){.activitypub-embed-meta span.ap-stat{display:none!important}}.activitypub-embed-meta a.ap-stat{color:inherit;text-decoration:none}.activitypub-embed-meta strong{color:#000;font-weight:600}.activitypub-embed-meta .ap-stat-label{color:#687684} diff --git a/build/follow-me/index.asset.php b/build/follow-me/index.asset.php deleted file mode 100644 index 59e5f6f9f..000000000 --- a/build/follow-me/index.asset.php +++ /dev/null @@ -1 +0,0 @@ - array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => 'd69f8905fbe5ea6410fa'); diff --git a/build/follow-me/index.js b/build/follow-me/index.js deleted file mode 100644 index a0131f1b8..000000000 --- a/build/follow-me/index.js +++ /dev/null @@ -1,3 +0,0 @@ -(()=>{"use strict";var e,t={20:(e,t,o)=>{var r=o(609),l=Symbol.for("react.element"),n=(Symbol.for("react.fragment"),Object.prototype.hasOwnProperty),a=r.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,i={key:!0,ref:!0,__self:!0,__source:!0};t.jsx=function(e,t,o){var r,c={},u=null,s=null;for(r in void 0!==o&&(u=""+o),void 0!==t.key&&(u=""+t.key),void 0!==t.ref&&(s=t.ref),t)n.call(t,r)&&!i.hasOwnProperty(r)&&(c[r]=t[r]);if(e&&e.defaultProps)for(r in t=e.defaultProps)void 0===c[r]&&(c[r]=t[r]);return{$$typeof:l,type:e,key:u,ref:s,props:c,_owner:a.current}}},609:e=>{e.exports=window.React},848:(e,t,o)=>{e.exports=o(20)},919:(e,t,o)=>{const r=window.wp.blocks,l=window.wp.primitives;var n=o(848);const a=(0,n.jsx)(l.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,n.jsx)(l.Path,{d:"M15.5 9.5a1 1 0 100-2 1 1 0 000 2zm0 1.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5zm-2.25 6v-2a2.75 2.75 0 00-2.75-2.75h-4A2.75 2.75 0 003.75 15v2h1.5v-2c0-.69.56-1.25 1.25-1.25h4c.69 0 1.25.56 1.25 1.25v2h1.5zm7-2v2h-1.5v-2c0-.69-.56-1.25-1.25-1.25H15v-1.5h2.5A2.75 2.75 0 0120.25 15zM9.5 8.5a1 1 0 11-2 0 1 1 0 012 0zm1.5 0a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z",fillRule:"evenodd"})});var i=o(609);const c=window.wp.blockEditor,u=window.wp.i18n,s=window.wp.data,p=window.wp.coreData,d=window.wp.components,m=window.wp.element;function v(){return window._activityPubOptions||{}}const b=window.wp.apiFetch;var f=o.n(b);function y(e){return`var(--wp--preset--color--${e})`}function _(e){if("string"!=typeof e)return null;if(e.match(/^#/))return e.substring(0,7);const[,,t]=e.split("|");return y(t)}function h(e,t,o=null,r=""){return o?`${e}${r} { ${t}: ${o}; }\n`:""}function w(e,t,o,r){return h(e,"background-color",t)+h(e,"color",o)+h(e,"background-color",r,":hover")+h(e,"background-color",r,":focus")}function g({selector:e,style:t,backgroundColor:o}){const r=function(e,t,o){const r=`${e} .components-button`,l=("string"==typeof(n=o)?y(n):n?.color?.background||null)||t?.color?.background;var n;return w(r,_(t?.elements?.link?.color?.text),l,_(t?.elements?.link?.[":hover"]?.color?.text))}(e,t,o);return(0,i.createElement)("style",null,r)}const E=(0,n.jsx)(l.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,n.jsx)(l.Path,{fillRule:"evenodd",clipRule:"evenodd",d:"M5 4.5h11a.5.5 0 0 1 .5.5v11a.5.5 0 0 1-.5.5H5a.5.5 0 0 1-.5-.5V5a.5.5 0 0 1 .5-.5ZM3 5a2 2 0 0 1 2-2h11a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5Zm17 3v10.75c0 .69-.56 1.25-1.25 1.25H6v1.5h12.75a2.75 2.75 0 0 0 2.75-2.75V8H20Z"})}),x=(0,n.jsx)(l.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,n.jsx)(l.Path,{d:"M16.7 7.1l-6.3 8.5-3.3-2.5-.9 1.2 4.5 3.4L17.9 8z"})}),S=(0,m.forwardRef)((function({icon:e,size:t=24,...o},r){return(0,m.cloneElement)(e,{width:t,height:t,...o,ref:r})})),k=window.wp.compose,C="fediverse-remote-user";function O(e){try{return new URL(e),!0}catch(e){return!1}}function T({actionText:e,copyDescription:t,handle:o,resourceUrl:r,myProfile:l="",rememberProfile:n=!1}){const c=(0,u.__)("Loading...","activitypub"),s=(0,u.__)("Opening...","activitypub"),p=(0,u.__)("Error","activitypub"),v=(0,u.__)("Invalid","activitypub"),b=l||(0,u.__)("My Profile","activitypub"),[y,_]=(0,m.useState)(e),[h,w]=(0,m.useState)(E),g=(0,k.useCopyToClipboard)(o,(()=>{w(x),setTimeout((()=>w(E)),1e3)})),[T,N]=(0,m.useState)(""),[I,R]=(0,m.useState)(!0),{setRemoteUser:U}=function(){const[e,t]=(0,m.useState)(function(){const e=localStorage.getItem(C);return e?JSON.parse(e):{}}()),o=(0,m.useCallback)((e=>{!function(e){localStorage.setItem(C,JSON.stringify(e))}(e),t(e)}),[]),r=(0,m.useCallback)((()=>{localStorage.removeItem(C),t({})}),[]);return{template:e?.template||!1,profileURL:e?.profileURL||!1,setRemoteUser:o,deleteRemoteUser:r}}(),z=(0,m.useCallback)((()=>{let t;if(!O(T)&&!function(e){const t=e.replace(/^@/,"").split("@");return 2===t.length&&O(`https://${t[1]}`)}(T))return _(v),t=setTimeout((()=>_(e)),2e3),()=>clearTimeout(t);const o=r+T;_(c),f()({path:o}).then((({url:t,template:o})=>{I&&U({profileURL:T,template:o}),_(s),setTimeout((()=>{window.open(t,"_blank"),_(e)}),200)})).catch((()=>{_(p),setTimeout((()=>_(e)),2e3)}))}),[T]);return(0,i.createElement)("div",{className:"activitypub__dialog",role:"dialog","aria-labelledby":"dialog-title"},(0,i.createElement)("div",{className:"activitypub-dialog__section"},(0,i.createElement)("h4",{id:"dialog-title"},b),(0,i.createElement)("div",{className:"activitypub-dialog__description",id:"copy-description"},t),(0,i.createElement)("div",{className:"activitypub-dialog__button-group"},(0,i.createElement)("label",{htmlFor:"profile-handle",className:"screen-reader-text"},t),(0,i.createElement)("input",{type:"text",id:"profile-handle",value:o,readOnly:!0}),(0,i.createElement)(d.Button,{ref:g,"aria-label":(0,u.__)("Copy handle to clipboard","activitypub")},(0,i.createElement)(S,{icon:h}),(0,u.__)("Copy","activitypub")))),(0,i.createElement)("div",{className:"activitypub-dialog__section"},(0,i.createElement)("h4",{id:"remote-profile-title"},(0,u.__)("Your Profile","activitypub")),(0,i.createElement)("div",{className:"activitypub-dialog__description",id:"remote-profile-description"},(0,m.createInterpolateElement)((0,u.__)("Or, if you know your own profile, we can start things that way! (eg @yourusername@example.com)","activitypub"),{code:(0,i.createElement)("code",null)})),(0,i.createElement)("div",{className:"activitypub-dialog__button-group"},(0,i.createElement)("label",{htmlFor:"remote-profile",className:"screen-reader-text"},(0,u.__)("Enter your ActivityPub profile","activitypub")),(0,i.createElement)("input",{type:"text",id:"remote-profile",value:T,onKeyDown:e=>{"Enter"===e?.code&&z()},onChange:e=>N(e.target.value),"aria-invalid":y===v}),(0,i.createElement)(d.Button,{onClick:z,"aria-label":(0,u.__)("Submit profile","activitypub")},(0,i.createElement)(S,{icon:a}),y)),n&&(0,i.createElement)("div",{className:"activitypub-dialog__remember"},(0,i.createElement)(d.CheckboxControl,{checked:I,label:(0,u.__)("Remember me for easier comments","activitypub"),onChange:()=>{R(!I)}}))))}const N={avatar:"",webfinger:"@well@hello.dolly",name:(0,u.__)("Hello Dolly Fan Account","activitypub"),url:"#"};function I(e){if(!e)return N;const t={...N,...e};return t.avatar=t?.icon?.url,t}function R({profile:e,popupStyles:t,userId:o,buttonText:r,buttonOnly:l,buttonSize:n}){const{webfinger:a,avatar:c,name:u}=e,s=a.startsWith("@")?a:`@${a}`;return l?(0,i.createElement)("div",{className:"activitypub-profile"},(0,i.createElement)(U,{profile:e,popupStyles:t,userId:o,buttonText:r,buttonSize:n})):(0,i.createElement)("div",{className:"activitypub-profile"},(0,i.createElement)("img",{className:"activitypub-profile__avatar",src:c,alt:u}),(0,i.createElement)("div",{className:"activitypub-profile__content"},(0,i.createElement)("div",{className:"activitypub-profile__name"},u),(0,i.createElement)("div",{className:"activitypub-profile__handle",title:s},s)),(0,i.createElement)(U,{profile:e,popupStyles:t,userId:o,buttonText:r,buttonSize:n}))}function U({profile:e,popupStyles:t,userId:o,buttonText:r,buttonSize:l}){const[n,a]=(0,m.useState)(!1),c=(0,u.sprintf)(/* translators: %s: profile name */ /* translators: %s: profile name */ -(0,u.__)("Follow %s","activitypub"),e?.name);return(0,i.createElement)(i.Fragment,null,(0,i.createElement)(d.Button,{className:"activitypub-profile__follow",onClick:()=>a(!0),"aria-haspopup":"dialog","aria-expanded":n,"aria-label":(0,u.__)("Follow me on the Fediverse","activitypub"),size:l},r),n&&(0,i.createElement)(d.Modal,{className:"activitypub-profile__confirm activitypub__modal",onRequestClose:()=>a(!1),title:c,"aria-label":c,role:"dialog"},(0,i.createElement)(z,{profile:e,userId:o}),(0,i.createElement)("style",null,t)))}function z({profile:e,userId:t}){const{namespace:o}=v(),{webfinger:r}=e,l=(0,u.__)("Follow","activitypub"),n=`/${o}/actors/${t}/remote-follow?resource=`,a=(0,u.__)("Copy and paste my profile into the search field of your favorite fediverse app or server.","activitypub"),c=r.startsWith("@")?r:`@${r}`;return(0,i.createElement)(T,{actionText:l,copyDescription:a,handle:c,resourceUrl:n})}function $({selectedUser:e,style:t,backgroundColor:o,id:r,useId:l=!1,profileData:n=!1,buttonOnly:a=!1,buttonText:c=(0,u.__)("Follow","activitypub"),buttonSize:s="default"}){const[p,d]=(0,m.useState)(I()),b="site"===e?0:e,y=function(e){return w(".apfmd__button-group .components-button",_(e?.elements?.link?.color?.text)||"#111","#fff",_(e?.elements?.link?.[":hover"]?.color?.text)||"#333")}(t),h=l?{id:r}:{};return(0,m.useEffect)((()=>{n?d(I(n)):function(e){const{namespace:t}=v(),o={headers:{Accept:"application/activity+json"},path:`/${t}/actors/${e}`};return f()(o)}(b).then((e=>{d(I(e))}))}),[b,n]),(0,i.createElement)("div",{...h,className:"activitypub-follow-me-block-wrapper"},(0,i.createElement)(g,{selector:`#${r}`,style:t,backgroundColor:o}),(0,i.createElement)(R,{profile:p,userId:b,popupStyles:y,buttonText:c,buttonOnly:a,buttonSize:s}))}function P({name:e}){const{enabled:t}=v(),o=t?.site?"":(0,u.__)("It will be empty in other non-author contexts.","activitypub"),r=(0,u.sprintf)(/* translators: %1$s: block name, %2$s: extra information for non-author context */ /* translators: %1$s: block name, %2$s: extra information for non-author context */ -(0,u.__)("This %1$s block will adapt to the page it is on, displaying the user profile associated with a post author (in a loop) or a user archive. %2$s","activitypub"),e,o).trim();return(0,i.createElement)(d.Card,null,(0,i.createElement)(d.CardBody,null,(0,m.createInterpolateElement)(r,{strong:(0,i.createElement)("strong",null)})))}(0,r.registerBlockType)("activitypub/follow-me",{edit:function({attributes:e,setAttributes:t,context:{postType:o,postId:r}}){const l=(0,c.useBlockProps)({className:"activitypub-follow-me-block-wrapper"}),n=function({withInherit:e=!1}){const{enabled:t}=v(),o=t?.users?(0,s.useSelect)((e=>e("core").getUsers({who:"authors"}))):[];return(0,m.useMemo)((()=>{if(!o)return[];const r=[];return t?.site&&r.push({label:(0,u.__)("Site","activitypub"),value:"site"}),e&&t?.users&&r.push({label:(0,u.__)("Dynamic User","activitypub"),value:"inherit"}),o.reduce(((e,t)=>(e.push({label:t.name,value:`${t.id}`}),e)),r)}),[o])}({withInherit:!0}),{selectedUser:a,buttonOnly:b,buttonText:f,buttonSize:y}=e,_="inherit"===a,h=(0,s.useSelect)((e=>{const{getEditedEntityRecord:t}=e(p.store),l=t("postType",o,r)?.author;return null!=l?l:null}),[o,r]);return(0,m.useEffect)((()=>{n.length&&(n.find((({value:e})=>e===a))||t({selectedUser:n[0].value}))}),[a,n]),(0,i.createElement)("div",{...l},(0,i.createElement)(c.InspectorControls,{key:"activitypub-follow-me"},(0,i.createElement)(d.PanelBody,{title:(0,u.__)("Follow Me Options","activitypub")},n.length>1&&(0,i.createElement)(d.SelectControl,{label:(0,u.__)("Select User","activitypub"),value:e.selectedUser,options:n,onChange:e=>t({selectedUser:e})}),(0,i.createElement)(d.ToggleControl,{label:(0,u.__)("Button Only Mode","activitypub"),checked:b,onChange:e=>t({buttonOnly:e}),help:(0,u.__)("Only show the follow button without profile information","activitypub")}),(0,i.createElement)(d.TextControl,{label:(0,u.__)("Button Text","activitypub"),value:f,onChange:e=>t({buttonText:e})}),(0,i.createElement)(d.SelectControl,{label:(0,u.__)("Button Size","activitypub"),value:y,options:[{label:(0,u.__)("Default","activitypub"),value:"default"},{label:(0,u.__)("Compact","activitypub"),value:"compact"},{label:(0,u.__)("Small","activitypub"),value:"small"}],onChange:e=>t({buttonSize:e}),help:(0,u.__)("Choose the size of the follow button","activitypub")}))),_?h?(0,i.createElement)($,{...e,id:l.id,selectedUser:h}):(0,i.createElement)(P,{name:(0,u.__)("Follow Me","activitypub")}):(0,i.createElement)($,{...e,id:l.id}))},save:()=>null,icon:a})}},o={};function r(e){var l=o[e];if(void 0!==l)return l.exports;var n=o[e]={exports:{}};return t[e](n,n.exports,r),n.exports}r.m=t,e=[],r.O=(t,o,l,n)=>{if(!o){var a=1/0;for(s=0;s=n)&&Object.keys(r.O).every((e=>r.O[e](o[c])))?o.splice(c--,1):(i=!1,n0&&e[s-1][2]>n;s--)e[s]=e[s-1];e[s]=[o,l,n]},r.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return r.d(t,{a:t}),t},r.d=(e,t)=>{for(var o in t)r.o(t,o)&&!r.o(e,o)&&Object.defineProperty(e,o,{enumerable:!0,get:t[o]})},r.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e={338:0,301:0};r.O.j=t=>0===e[t];var t=(t,o)=>{var l,n,[a,i,c]=o,u=0;if(a.some((t=>0!==e[t]))){for(l in i)r.o(i,l)&&(r.m[l]=i[l]);if(c)var s=c(r)}for(t&&t(o);ur(919)));l=r.O(l)})(); \ No newline at end of file diff --git a/build/follow-me/view.asset.php b/build/follow-me/view.asset.php deleted file mode 100644 index b2a00fcd5..000000000 --- a/build/follow-me/view.asset.php +++ /dev/null @@ -1 +0,0 @@ - array('react', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '4e430690e282cfe013db'); diff --git a/build/follow-me/view.js b/build/follow-me/view.js deleted file mode 100644 index 7d4de0582..000000000 --- a/build/follow-me/view.js +++ /dev/null @@ -1,2 +0,0 @@ -(()=>{"use strict";var e,t={5:(e,t,r)=>{var o=r(609);const a=window.wp.element,n=window.wp.domReady;var i=r.n(n);const l=window.wp.apiFetch;var c=r.n(l);const u=window.wp.components,s=window.wp.i18n;function p(e){return`var(--wp--preset--color--${e})`}function m(e){if("string"!=typeof e)return null;if(e.match(/^#/))return e.substring(0,7);const[,,t]=e.split("|");return p(t)}function d(e,t,r=null,o=""){return r?`${e}${o} { ${t}: ${r}; }\n`:""}function v(e,t,r,o){return d(e,"background-color",t)+d(e,"color",r)+d(e,"background-color",o,":hover")+d(e,"background-color",o,":focus")}function f({selector:e,style:t,backgroundColor:r}){const a=function(e,t,r){const o=`${e} .components-button`,a=("string"==typeof(n=r)?p(n):n?.color?.background||null)||t?.color?.background;var n;return v(o,m(t?.elements?.link?.color?.text),a,m(t?.elements?.link?.[":hover"]?.color?.text))}(e,t,r);return(0,o.createElement)("style",null,a)}const b=window.wp.primitives;var y=r(848);const _=(0,y.jsx)(b.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,y.jsx)(b.Path,{fillRule:"evenodd",clipRule:"evenodd",d:"M5 4.5h11a.5.5 0 0 1 .5.5v11a.5.5 0 0 1-.5.5H5a.5.5 0 0 1-.5-.5V5a.5.5 0 0 1 .5-.5ZM3 5a2 2 0 0 1 2-2h11a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5Zm17 3v10.75c0 .69-.56 1.25-1.25 1.25H6v1.5h12.75a2.75 2.75 0 0 0 2.75-2.75V8H20Z"})}),w=(0,y.jsx)(b.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,y.jsx)(b.Path,{d:"M16.7 7.1l-6.3 8.5-3.3-2.5-.9 1.2 4.5 3.4L17.9 8z"})}),h=(0,a.forwardRef)((function({icon:e,size:t=24,...r},o){return(0,a.cloneElement)(e,{width:t,height:t,...r,ref:o})})),g=(0,y.jsx)(b.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,y.jsx)(b.Path,{d:"M15.5 9.5a1 1 0 100-2 1 1 0 000 2zm0 1.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5zm-2.25 6v-2a2.75 2.75 0 00-2.75-2.75h-4A2.75 2.75 0 003.75 15v2h1.5v-2c0-.69.56-1.25 1.25-1.25h4c.69 0 1.25.56 1.25 1.25v2h1.5zm7-2v2h-1.5v-2c0-.69-.56-1.25-1.25-1.25H15v-1.5h2.5A2.75 2.75 0 0120.25 15zM9.5 8.5a1 1 0 11-2 0 1 1 0 012 0zm1.5 0a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z",fillRule:"evenodd"})}),E=window.wp.compose,x="fediverse-remote-user";function S(e){try{return new URL(e),!0}catch(e){return!1}}function k({actionText:e,copyDescription:t,handle:r,resourceUrl:n,myProfile:i="",rememberProfile:l=!1}){const p=(0,s.__)("Loading...","activitypub"),m=(0,s.__)("Opening...","activitypub"),d=(0,s.__)("Error","activitypub"),v=(0,s.__)("Invalid","activitypub"),f=i||(0,s.__)("My Profile","activitypub"),[b,y]=(0,a.useState)(e),[k,O]=(0,a.useState)(_),N=(0,E.useCopyToClipboard)(r,(()=>{O(w),setTimeout((()=>O(_)),1e3)})),[C,R]=(0,a.useState)(""),[T,I]=(0,a.useState)(!0),{setRemoteUser:$}=function(){const[e,t]=(0,a.useState)(function(){const e=localStorage.getItem(x);return e?JSON.parse(e):{}}()),r=(0,a.useCallback)((e=>{!function(e){localStorage.setItem(x,JSON.stringify(e))}(e),t(e)}),[]),o=(0,a.useCallback)((()=>{localStorage.removeItem(x),t({})}),[]);return{template:e?.template||!1,profileURL:e?.profileURL||!1,setRemoteUser:r,deleteRemoteUser:o}}(),z=(0,a.useCallback)((()=>{let t;if(!S(C)&&!function(e){const t=e.replace(/^@/,"").split("@");return 2===t.length&&S(`https://${t[1]}`)}(C))return y(v),t=setTimeout((()=>y(e)),2e3),()=>clearTimeout(t);const r=n+C;y(p),c()({path:r}).then((({url:t,template:r})=>{T&&$({profileURL:C,template:r}),y(m),setTimeout((()=>{window.open(t,"_blank"),y(e)}),200)})).catch((()=>{y(d),setTimeout((()=>y(e)),2e3)}))}),[C]);return(0,o.createElement)("div",{className:"activitypub__dialog",role:"dialog","aria-labelledby":"dialog-title"},(0,o.createElement)("div",{className:"activitypub-dialog__section"},(0,o.createElement)("h4",{id:"dialog-title"},f),(0,o.createElement)("div",{className:"activitypub-dialog__description",id:"copy-description"},t),(0,o.createElement)("div",{className:"activitypub-dialog__button-group"},(0,o.createElement)("label",{htmlFor:"profile-handle",className:"screen-reader-text"},t),(0,o.createElement)("input",{type:"text",id:"profile-handle",value:r,readOnly:!0}),(0,o.createElement)(u.Button,{ref:N,"aria-label":(0,s.__)("Copy handle to clipboard","activitypub")},(0,o.createElement)(h,{icon:k}),(0,s.__)("Copy","activitypub")))),(0,o.createElement)("div",{className:"activitypub-dialog__section"},(0,o.createElement)("h4",{id:"remote-profile-title"},(0,s.__)("Your Profile","activitypub")),(0,o.createElement)("div",{className:"activitypub-dialog__description",id:"remote-profile-description"},(0,a.createInterpolateElement)((0,s.__)("Or, if you know your own profile, we can start things that way! (eg @yourusername@example.com)","activitypub"),{code:(0,o.createElement)("code",null)})),(0,o.createElement)("div",{className:"activitypub-dialog__button-group"},(0,o.createElement)("label",{htmlFor:"remote-profile",className:"screen-reader-text"},(0,s.__)("Enter your ActivityPub profile","activitypub")),(0,o.createElement)("input",{type:"text",id:"remote-profile",value:C,onKeyDown:e=>{"Enter"===e?.code&&z()},onChange:e=>R(e.target.value),"aria-invalid":b===v}),(0,o.createElement)(u.Button,{onClick:z,"aria-label":(0,s.__)("Submit profile","activitypub")},(0,o.createElement)(h,{icon:g}),b)),l&&(0,o.createElement)("div",{className:"activitypub-dialog__remember"},(0,o.createElement)(u.CheckboxControl,{checked:T,label:(0,s.__)("Remember me for easier comments","activitypub"),onChange:()=>{I(!T)}}))))}function O(){return window._activityPubOptions||{}}const N={avatar:"",webfinger:"@well@hello.dolly",name:(0,s.__)("Hello Dolly Fan Account","activitypub"),url:"#"};function C(e){if(!e)return N;const t={...N,...e};return t.avatar=t?.icon?.url,t}function R({profile:e,popupStyles:t,userId:r,buttonText:a,buttonOnly:n,buttonSize:i}){const{webfinger:l,avatar:c,name:u}=e,s=l.startsWith("@")?l:`@${l}`;return n?(0,o.createElement)("div",{className:"activitypub-profile"},(0,o.createElement)(T,{profile:e,popupStyles:t,userId:r,buttonText:a,buttonSize:i})):(0,o.createElement)("div",{className:"activitypub-profile"},(0,o.createElement)("img",{className:"activitypub-profile__avatar",src:c,alt:u}),(0,o.createElement)("div",{className:"activitypub-profile__content"},(0,o.createElement)("div",{className:"activitypub-profile__name"},u),(0,o.createElement)("div",{className:"activitypub-profile__handle",title:s},s)),(0,o.createElement)(T,{profile:e,popupStyles:t,userId:r,buttonText:a,buttonSize:i}))}function T({profile:e,popupStyles:t,userId:r,buttonText:n,buttonSize:i}){const[l,c]=(0,a.useState)(!1),p=(0,s.sprintf)(/* translators: %s: profile name */ /* translators: %s: profile name */ -(0,s.__)("Follow %s","activitypub"),e?.name);return(0,o.createElement)(o.Fragment,null,(0,o.createElement)(u.Button,{className:"activitypub-profile__follow",onClick:()=>c(!0),"aria-haspopup":"dialog","aria-expanded":l,"aria-label":(0,s.__)("Follow me on the Fediverse","activitypub"),size:i},n),l&&(0,o.createElement)(u.Modal,{className:"activitypub-profile__confirm activitypub__modal",onRequestClose:()=>c(!1),title:p,"aria-label":p,role:"dialog"},(0,o.createElement)(I,{profile:e,userId:r}),(0,o.createElement)("style",null,t)))}function I({profile:e,userId:t}){const{namespace:r}=O(),{webfinger:a}=e,n=(0,s.__)("Follow","activitypub"),i=`/${r}/actors/${t}/remote-follow?resource=`,l=(0,s.__)("Copy and paste my profile into the search field of your favorite fediverse app or server.","activitypub"),c=a.startsWith("@")?a:`@${a}`;return(0,o.createElement)(k,{actionText:n,copyDescription:l,handle:c,resourceUrl:i})}function $({selectedUser:e,style:t,backgroundColor:r,id:n,useId:i=!1,profileData:l=!1,buttonOnly:u=!1,buttonText:p=(0,s.__)("Follow","activitypub"),buttonSize:d="default"}){const[b,y]=(0,a.useState)(C()),_="site"===e?0:e,w=function(e){return v(".apfmd__button-group .components-button",m(e?.elements?.link?.color?.text)||"#111","#fff",m(e?.elements?.link?.[":hover"]?.color?.text)||"#333")}(t),h=i?{id:n}:{};return(0,a.useEffect)((()=>{l?y(C(l)):function(e){const{namespace:t}=O(),r={headers:{Accept:"application/activity+json"},path:`/${t}/actors/${e}`};return c()(r)}(_).then((e=>{y(C(e))}))}),[_,l]),(0,o.createElement)("div",{...h,className:"activitypub-follow-me-block-wrapper"},(0,o.createElement)(f,{selector:`#${n}`,style:t,backgroundColor:r}),(0,o.createElement)(R,{profile:b,userId:_,popupStyles:w,buttonText:p,buttonOnly:u,buttonSize:d}))}let z=1;i()((()=>{[].forEach.call(document.querySelectorAll(".activitypub-follow-me-block-wrapper"),(e=>{const t=JSON.parse(e.dataset.attrs);(0,a.createRoot)(e).render((0,o.createElement)($,{...t,id:"activitypub-follow-me-block-"+z++,useId:!0}))}))}))},20:(e,t,r)=>{var o=r(609),a=Symbol.for("react.element"),n=(Symbol.for("react.fragment"),Object.prototype.hasOwnProperty),i=o.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,l={key:!0,ref:!0,__self:!0,__source:!0};t.jsx=function(e,t,r){var o,c={},u=null,s=null;for(o in void 0!==r&&(u=""+r),void 0!==t.key&&(u=""+t.key),void 0!==t.ref&&(s=t.ref),t)n.call(t,o)&&!l.hasOwnProperty(o)&&(c[o]=t[o]);if(e&&e.defaultProps)for(o in t=e.defaultProps)void 0===c[o]&&(c[o]=t[o]);return{$$typeof:a,type:e,key:u,ref:s,props:c,_owner:i.current}}},609:e=>{e.exports=window.React},848:(e,t,r)=>{e.exports=r(20)}},r={};function o(e){var a=r[e];if(void 0!==a)return a.exports;var n=r[e]={exports:{}};return t[e](n,n.exports,o),n.exports}o.m=t,e=[],o.O=(t,r,a,n)=>{if(!r){var i=1/0;for(s=0;s=n)&&Object.keys(o.O).every((e=>o.O[e](r[c])))?r.splice(c--,1):(l=!1,n0&&e[s-1][2]>n;s--)e[s]=e[s-1];e[s]=[r,a,n]},o.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return o.d(t,{a:t}),t},o.d=(e,t)=>{for(var r in t)o.o(t,r)&&!o.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},o.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e={41:0,301:0};o.O.j=t=>0===e[t];var t=(t,r)=>{var a,n,[i,l,c]=r,u=0;if(i.some((t=>0!==e[t]))){for(a in l)o.o(l,a)&&(o.m[a]=l[a]);if(c)var s=c(o)}for(t&&t(r);uo(5)));a=o.O(a)})(); \ No newline at end of file diff --git a/build/followers/index.js b/build/followers/index.js deleted file mode 100644 index f05ec0f8a..000000000 --- a/build/followers/index.js +++ /dev/null @@ -1,4 +0,0 @@ -(()=>{var e={20:(e,t,a)=>{"use strict";var r=a(609),n=Symbol.for("react.element"),l=(Symbol.for("react.fragment"),Object.prototype.hasOwnProperty),o=r.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,i={key:!0,ref:!0,__self:!0,__source:!0};t.jsx=function(e,t,a){var r,c={},s=null,p=null;for(r in void 0!==a&&(s=""+a),void 0!==t.key&&(s=""+t.key),void 0!==t.ref&&(p=t.ref),t)l.call(t,r)&&!i.hasOwnProperty(r)&&(c[r]=t[r]);if(e&&e.defaultProps)for(r in t=e.defaultProps)void 0===c[r]&&(c[r]=t[r]);return{$$typeof:n,type:e,key:s,ref:p,props:c,_owner:o.current}}},609:e=>{"use strict";e.exports=window.React},848:(e,t,a)=>{"use strict";e.exports=a(20)},942:(e,t)=>{var a;!function(){"use strict";var r={}.hasOwnProperty;function n(){for(var e="",t=0;t{var t=e&&e.__esModule?()=>e.default:()=>e;return a.d(t,{a:t}),t},a.d=(e,t)=>{for(var r in t)a.o(t,r)&&!a.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},a.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{"use strict";const e=window.wp.blocks,t=window.wp.primitives;var r=a(848);const n=(0,r.jsx)(t.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,r.jsx)(t.Path,{d:"M15.5 9.5a1 1 0 100-2 1 1 0 000 2zm0 1.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5zm-2.25 6v-2a2.75 2.75 0 00-2.75-2.75h-4A2.75 2.75 0 003.75 15v2h1.5v-2c0-.69.56-1.25 1.25-1.25h4c.69 0 1.25.56 1.25 1.25v2h1.5zm7-2v2h-1.5v-2c0-.69-.56-1.25-1.25-1.25H15v-1.5h2.5A2.75 2.75 0 0120.25 15zM9.5 8.5a1 1 0 11-2 0 1 1 0 012 0zm1.5 0a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z",fillRule:"evenodd"})});var l=a(609);const o=window.wp.components,i=window.wp.element,c=window.wp.blockEditor,s=window.wp.data,p=window.wp.coreData,u=window.wp.i18n,m=window.wp.apiFetch;var v=a.n(m);const d=window.wp.url;var w=a(942),f=a.n(w);function b({active:e,children:t,page:a,pageClick:r,className:n}){const o=f()("wp-block activitypub-pager",n,{current:e});return(0,l.createElement)("a",{className:o,onClick:t=>{t.preventDefault(),!e&&r(a)}},t)}function y({compact:e,nextLabel:t,page:a,pageClick:r,perPage:n,prevLabel:o,total:i,variant:c="outlined"}){const s=((e,t)=>{let a=[1,e-2,e-1,e,e+1,e+2,t];a.sort(((e,t)=>e-t)),a=a.filter(((e,a,r)=>e>=1&&e<=t&&r.lastIndexOf(e)===a));for(let e=a.length-2;e>=0;e--)a[e]===a[e+1]&&a.splice(e+1,1);return a})(a,Math.ceil(i/n)),p=f()("alignwide wp-block-query-pagination is-content-justification-space-between is-layout-flex wp-block-query-pagination-is-layout-flex",`is-${c}`,{"is-compact":e});return(0,l.createElement)("nav",{className:p},o&&(0,l.createElement)(b,{key:"prev",page:a-1,pageClick:r,active:1===a,"aria-label":o,className:"wp-block-query-pagination-previous block-editor-block-list__block"},o),!e&&(0,l.createElement)("div",{className:"block-editor-block-list__block wp-block wp-block-query-pagination-numbers"},s.map((e=>(0,l.createElement)(b,{key:e,page:e,pageClick:r,active:e===a,className:"page-numbers"},e)))),t&&(0,l.createElement)(b,{key:"next",page:a+1,pageClick:r,active:a===Math.ceil(i/n),"aria-label":t,className:"wp-block-query-pagination-next block-editor-block-list__block"},t))}function g(){return window._activityPubOptions||{}}function h({selectedUser:e,per_page:t,order:a,title:r,page:n,setPage:o,className:c="",followLinks:s=!0,followerData:p=!1}){const m="site"===e?0:e,[w,f]=(0,l.useState)([]),[b,h]=(0,l.useState)(0),[k,E]=(0,l.useState)(0),[x,S]=function(){const[e,t]=(0,l.useState)(1);return[e,t]}(),N=n||x,C=o||S,O=(0,i.createInterpolateElement)(/* translators: arrow for previous followers link */ /* translators: arrow for previous followers link */ -(0,u.__)(" Less","activitypub"),{span:(0,l.createElement)("span",{className:"wp-block-query-pagination-previous-arrow is-arrow-arrow","aria-hidden":"true"})}),P=(0,i.createInterpolateElement)(/* translators: arrow for next followers link */ /* translators: arrow for next followers link */ -(0,u.__)("More ","activitypub"),{span:(0,l.createElement)("span",{className:"wp-block-query-pagination-next-arrow is-arrow-arrow","aria-hidden":"true"})}),I=(e,a)=>{f(e),E(a),h(Math.ceil(a/t))};return(0,l.useEffect)((()=>{if(p&&1===N)return I(p.followers,p.total);const e=function(e,t,a,r){const{namespace:n}=g(),l=`/${n}/actors/${e}/followers`,o={per_page:t,order:a,page:r,context:"full"};return(0,d.addQueryArgs)(l,o)}(m,t,a,N);v()({path:e}).then((e=>I(e.orderedItems,e.totalItems))).catch((()=>{}))}),[m,t,a,N,p]),(0,l.createElement)("div",{className:"activitypub-follower-block "+c},(0,l.createElement)("h3",null,r),(0,l.createElement)("ul",null,w&&w.map((e=>(0,l.createElement)("li",{key:e.url},(0,l.createElement)(_,{...e,followLinks:s}))))),b>1&&(0,l.createElement)(y,{page:N,perPage:t,total:k,pageClick:C,nextLabel:P,prevLabel:O,compact:"is-style-compact"===c}))}function _({name:e,icon:t,url:a,preferredUsername:r,followLinks:n=!0}){const i=`@${r}`,c={};n||(c.onClick=e=>e.preventDefault());const{defaultAvatarUrl:s}=g(),p=t.url||s;return(0,l.createElement)(o.ExternalLink,{className:"activitypub-link",href:a,title:i,...c},(0,l.createElement)("img",{width:"40",height:"40",src:p,className:"avatar activitypub-avatar",alt:e,onError:e=>{e.target.src=s}}),(0,l.createElement)("span",{className:"activitypub-actor"},(0,l.createElement)("strong",{className:"activitypub-name"},e),(0,l.createElement)("span",{className:"sep"},"/"),(0,l.createElement)("span",{className:"activitypub-handle"},i)))}function k({name:e}){const{enabled:t}=g(),a=t?.site?"":(0,u.__)("It will be empty in other non-author contexts.","activitypub"),r=(0,u.sprintf)(/* translators: %1$s: block name, %2$s: extra information for non-author context */ /* translators: %1$s: block name, %2$s: extra information for non-author context */ -(0,u.__)("This %1$s block will adapt to the page it is on, displaying the user profile associated with a post author (in a loop) or a user archive. %2$s","activitypub"),e,a).trim();return(0,l.createElement)(o.Card,null,(0,l.createElement)(o.CardBody,null,(0,i.createInterpolateElement)(r,{strong:(0,l.createElement)("strong",null)})))}(0,e.registerBlockType)("activitypub/followers",{edit:function({attributes:e,setAttributes:t,context:{postType:a,postId:r}}){const{order:n,per_page:m,selectedUser:v,title:d}=e,w=(0,c.useBlockProps)(),[f,b]=(0,i.useState)(1),y=[{label:(0,u.__)("New to old","activitypub"),value:"desc"},{label:(0,u.__)("Old to new","activitypub"),value:"asc"}],_=function({withInherit:e=!1}){const{enabled:t}=g(),a=t?.users?(0,s.useSelect)((e=>e("core").getUsers({who:"authors"}))):[];return(0,i.useMemo)((()=>{if(!a)return[];const r=[];return t?.site&&r.push({label:(0,u.__)("Site","activitypub"),value:"site"}),e&&t?.users&&r.push({label:(0,u.__)("Dynamic User","activitypub"),value:"inherit"}),a.reduce(((e,t)=>(e.push({label:t.name,value:`${t.id}`}),e)),r)}),[a])}({withInherit:!0}),E=e=>a=>{b(1),t({[e]:a})},x=(0,s.useSelect)((e=>{const{getEditedEntityRecord:t}=e(p.store),n=t("postType",a,r)?.author;return null!=n?n:null}),[a,r]);return(0,i.useEffect)((()=>{_.length&&(_.find((({value:e})=>e===v))||t({selectedUser:_[0].value}))}),[v,_]),(0,l.createElement)("div",{...w},(0,l.createElement)(c.InspectorControls,{key:"setting"},(0,l.createElement)(o.PanelBody,{title:(0,u.__)("Followers Options","activitypub")},(0,l.createElement)(o.TextControl,{label:(0,u.__)("Title","activitypub"),help:(0,u.__)("Title to display above the list of followers. Blank for none.","activitypub"),value:d,onChange:e=>t({title:e})}),_.length>1&&(0,l.createElement)(o.SelectControl,{label:(0,u.__)("Select User","activitypub"),value:v,options:_,onChange:E("selectedUser")}),(0,l.createElement)(o.SelectControl,{label:(0,u.__)("Sort","activitypub"),value:n,options:y,onChange:E("order")}),(0,l.createElement)(o.RangeControl,{label:(0,u.__)("Number of Followers","activitypub"),value:m,onChange:E("per_page"),min:1,max:10}))),"inherit"===v?x?(0,l.createElement)(h,{...e,page:f,setPage:b,followLinks:!1,selectedUser:x}):(0,l.createElement)(k,{name:(0,u.__)("Followers","activitypub")}):(0,l.createElement)(h,{...e,page:f,setPage:b,followLinks:!1}))},save:()=>null,icon:n})})()})(); \ No newline at end of file diff --git a/build/followers/view.asset.php b/build/followers/view.asset.php deleted file mode 100644 index 1f3eb4d7c..000000000 --- a/build/followers/view.asset.php +++ /dev/null @@ -1 +0,0 @@ - array('react', 'wp-api-fetch', 'wp-components', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-url'), 'version' => 'e1915374b8c762ae4376'); diff --git a/build/followers/view.js b/build/followers/view.js deleted file mode 100644 index 451e63ae0..000000000 --- a/build/followers/view.js +++ /dev/null @@ -1,3 +0,0 @@ -(()=>{var e,t={73:(e,t,a)=>{"use strict";const r=window.React,n=window.wp.apiFetch;var l=a.n(n);const o=window.wp.url,c=window.wp.element,i=window.wp.i18n;var s=a(942),p=a.n(s);function u({active:e,children:t,page:a,pageClick:n,className:l}){const o=p()("wp-block activitypub-pager",l,{current:e});return(0,r.createElement)("a",{className:o,onClick:t=>{t.preventDefault(),!e&&n(a)}},t)}function m({compact:e,nextLabel:t,page:a,pageClick:n,perPage:l,prevLabel:o,total:c,variant:i="outlined"}){const s=((e,t)=>{let a=[1,e-2,e-1,e,e+1,e+2,t];a.sort(((e,t)=>e-t)),a=a.filter(((e,a,r)=>e>=1&&e<=t&&r.lastIndexOf(e)===a));for(let e=a.length-2;e>=0;e--)a[e]===a[e+1]&&a.splice(e+1,1);return a})(a,Math.ceil(c/l)),m=p()("alignwide wp-block-query-pagination is-content-justification-space-between is-layout-flex wp-block-query-pagination-is-layout-flex",`is-${i}`,{"is-compact":e});return(0,r.createElement)("nav",{className:m},o&&(0,r.createElement)(u,{key:"prev",page:a-1,pageClick:n,active:1===a,"aria-label":o,className:"wp-block-query-pagination-previous block-editor-block-list__block"},o),!e&&(0,r.createElement)("div",{className:"block-editor-block-list__block wp-block wp-block-query-pagination-numbers"},s.map((e=>(0,r.createElement)(u,{key:e,page:e,pageClick:n,active:e===a,className:"page-numbers"},e)))),t&&(0,r.createElement)(u,{key:"next",page:a+1,pageClick:n,active:a===Math.ceil(c/l),"aria-label":t,className:"wp-block-query-pagination-next block-editor-block-list__block"},t))}const f=window.wp.components;function v(){return window._activityPubOptions||{}}function b({selectedUser:e,per_page:t,order:a,title:n,page:s,setPage:p,className:u="",followLinks:f=!0,followerData:b=!1}){const w="site"===e?0:e,[g,y]=(0,r.useState)([]),[k,h]=(0,r.useState)(0),[E,N]=(0,r.useState)(0),[x,_]=function(){const[e,t]=(0,r.useState)(1);return[e,t]}(),O=s||x,S=p||_,C=(0,c.createInterpolateElement)(/* translators: arrow for previous followers link */ /* translators: arrow for previous followers link */ -(0,i.__)(" Less","activitypub"),{span:(0,r.createElement)("span",{className:"wp-block-query-pagination-previous-arrow is-arrow-arrow","aria-hidden":"true"})}),L=(0,c.createInterpolateElement)(/* translators: arrow for next followers link */ /* translators: arrow for next followers link */ -(0,i.__)("More ","activitypub"),{span:(0,r.createElement)("span",{className:"wp-block-query-pagination-next-arrow is-arrow-arrow","aria-hidden":"true"})}),q=(e,a)=>{y(e),N(a),h(Math.ceil(a/t))};return(0,r.useEffect)((()=>{if(b&&1===O)return q(b.followers,b.total);const e=function(e,t,a,r){const{namespace:n}=v(),l=`/${n}/actors/${e}/followers`,c={per_page:t,order:a,page:r,context:"full"};return(0,o.addQueryArgs)(l,c)}(w,t,a,O);l()({path:e}).then((e=>q(e.orderedItems,e.totalItems))).catch((()=>{}))}),[w,t,a,O,b]),(0,r.createElement)("div",{className:"activitypub-follower-block "+u},(0,r.createElement)("h3",null,n),(0,r.createElement)("ul",null,g&&g.map((e=>(0,r.createElement)("li",{key:e.url},(0,r.createElement)(d,{...e,followLinks:f}))))),k>1&&(0,r.createElement)(m,{page:O,perPage:t,total:E,pageClick:S,nextLabel:L,prevLabel:C,compact:"is-style-compact"===u}))}function d({name:e,icon:t,url:a,preferredUsername:n,followLinks:l=!0}){const o=`@${n}`,c={};l||(c.onClick=e=>e.preventDefault());const{defaultAvatarUrl:i}=v(),s=t.url||i;return(0,r.createElement)(f.ExternalLink,{className:"activitypub-link",href:a,title:o,...c},(0,r.createElement)("img",{width:"40",height:"40",src:s,className:"avatar activitypub-avatar",alt:e,onError:e=>{e.target.src=i}}),(0,r.createElement)("span",{className:"activitypub-actor"},(0,r.createElement)("strong",{className:"activitypub-name"},e),(0,r.createElement)("span",{className:"sep"},"/"),(0,r.createElement)("span",{className:"activitypub-handle"},o)))}const w=window.wp.domReady;a.n(w)()((()=>{[].forEach.call(document.querySelectorAll(".activitypub-follower-block"),(e=>{const t=JSON.parse(e.dataset.attrs);(0,c.createRoot)(e).render((0,r.createElement)(b,{...t}))}))}))},942:(e,t)=>{var a;!function(){"use strict";var r={}.hasOwnProperty;function n(){for(var e="",t=0;t{if(!a){var o=1/0;for(p=0;p=l)&&Object.keys(r.O).every((e=>r.O[e](a[i])))?a.splice(i--,1):(c=!1,l0&&e[p-1][2]>l;p--)e[p]=e[p-1];e[p]=[a,n,l]},r.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return r.d(t,{a:t}),t},r.d=(e,t)=>{for(var a in t)r.o(t,a)&&!r.o(e,a)&&Object.defineProperty(e,a,{enumerable:!0,get:t[a]})},r.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e={996:0,528:0};r.O.j=t=>0===e[t];var t=(t,a)=>{var n,l,[o,c,i]=a,s=0;if(o.some((t=>0!==e[t]))){for(n in c)r.o(c,n)&&(r.m[n]=c[n]);if(i)var p=i(r)}for(t&&t(a);sr(73)));n=r.O(n)})(); \ No newline at end of file diff --git a/build/reactions/block.json b/build/reactions/block.json deleted file mode 100644 index c35f01771..000000000 --- a/build/reactions/block.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "$schema": "https://schemas.wp.org/trunk/block.json", - "name": "activitypub/reactions", - "apiVersion": 2, - "version": "1.0.0", - "title": "Fediverse Reactions", - "category": "widgets", - "icon": "heart", - "description": "Display Fediverse likes and reposts", - "supports": { - "html": false, - "align": true, - "layout": { - "default": { - "type": "constrained", - "orientation": "vertical", - "justifyContent": "center" - } - } - }, - "attributes": {}, - "blockHooks": { - "core/post-content": "after" - }, - "textdomain": "activitypub", - "editorScript": "file:./index.js", - "style": [ - "file:./style-index.css", - "wp-components" - ], - "viewScript": "file:./view.js" -} \ No newline at end of file diff --git a/build/reactions/index.asset.php b/build/reactions/index.asset.php deleted file mode 100644 index 2f8424559..000000000 --- a/build/reactions/index.asset.php +++ /dev/null @@ -1 +0,0 @@ - array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-element', 'wp-i18n'), 'version' => '13c017000d37a3025875'); diff --git a/build/reactions/index.js b/build/reactions/index.js deleted file mode 100644 index 0b4f85ba5..000000000 --- a/build/reactions/index.js +++ /dev/null @@ -1,3 +0,0 @@ -(()=>{"use strict";var e,t={29:(e,t,n)=>{const r=window.wp.blocks,a=[{attributes:{title:{type:"string",default:"Fediverse reactions"}},supports:{html:!1,align:!0,layout:{default:{type:"constrained",orientation:"vertical",justifyContent:"center"}}},isEligible:e=>!!e.title,migrate(e){const{title:t,...n}=e;return[n,[(0,r.createBlock)("core/heading",{content:t,level:6})]]}}],l=window.React,o=window.wp.blockEditor,i=window.wp.element,s=window.wp.i18n,c=window.wp.components,u=window.wp.apiFetch;var m=n.n(u);function p(){return window._activityPubOptions||{}}const h=({reactions:e})=>{const{defaultAvatarUrl:t}=p(),[n,r]=(0,i.useState)(new Set),[a,o]=(0,i.useState)(new Map),s=(0,i.useRef)([]),c=()=>{s.current.forEach((e=>clearTimeout(e))),s.current=[]},u=(t,n)=>{c();const a=100,l=e.length;n&&o((e=>{const n=new Map(e);return n.set(t,"clockwise"),n}));const i=e=>{const i="right"===e,c=i?l-1:0,u=i?1:-1;for(let e=i?t:t-1;i?e<=c:e>=c;e+=u){const l=Math.abs(e-t),i=setTimeout((()=>{r((t=>{const r=new Set(t);return n?r.add(e):r.delete(e),r})),n&&e!==t&&o((t=>{const n=new Map(t),r=e-u,a=n.get(r);return n.set(e,"clockwise"===a?"counter":"clockwise"),n}))}),l*a);s.current.push(i)}};if(i("right"),i("left"),!n){const e=Math.max((l-t)*a,t*a),n=setTimeout((()=>{o(new Map)}),e+a);s.current.push(n)}};return(0,i.useEffect)((()=>()=>c()),[]),(0,l.createElement)("ul",{className:"reaction-avatars"},e.map(((e,r)=>{const o=a.get(r),i=["reaction-avatar",n.has(r)?"wave-active":"",o?`rotate-${o}`:""].filter(Boolean).join(" "),s=e.avatar||t;return(0,l.createElement)("li",{key:r},(0,l.createElement)("a",{href:e.url,target:"_blank",rel:"noopener noreferrer",onMouseEnter:()=>u(r,!0),onMouseLeave:()=>u(r,!1)},(0,l.createElement)("img",{src:s,alt:e.name,className:i,width:"32",height:"32",onError:e=>{e.target.src=t}})))})))},f=({reactions:e,type:t})=>(0,l.createElement)("ul",{className:"activitypub-reaction-list"},e.map(((e,t)=>(0,l.createElement)("li",{key:t},(0,l.createElement)("a",{href:e.url,className:"reaction-item",target:"_blank",rel:"noopener noreferrer"},(0,l.createElement)("img",{src:e.avatar,alt:e.name,width:"32",height:"32"}),(0,l.createElement)("span",null,e.name)))))),d=({items:e,label:t})=>{const[n,r]=(0,i.useState)(!1),[a,o]=(0,i.useState)(null),[s,u]=(0,i.useState)(e.length),m=(0,i.useRef)(null);(0,i.useEffect)((()=>{if(!m.current)return;const t=()=>{const t=m.current;if(!t)return;const n=t.offsetWidth-(a?.offsetWidth||0)-12,r=Math.max(1,Math.floor((n-32)/22));u(Math.min(r,e.length))};t();const n=new ResizeObserver(t);return n.observe(m.current),()=>{n.disconnect()}}),[a,e.length]);const p=e.slice(0,s);return(0,l.createElement)("div",{className:"reaction-group",ref:m},(0,l.createElement)(h,{reactions:p}),(0,l.createElement)(c.Button,{ref:o,className:"reaction-label is-link",onClick:()=>r(!n),"aria-expanded":n},t),n&&a&&(0,l.createElement)(c.Popover,{anchor:a,onClose:()=>r(!1)},(0,l.createElement)(f,{reactions:e})))};function g({postId:e=null,reactions:t=null,title:n=null}){const{namespace:r}=p(),[a,o]=(0,i.useState)(t),[s,c]=(0,i.useState)(!t);return(0,i.useEffect)((()=>{if(t)return o(t),void c(!1);e?(c(!0),m()({path:`/${r}/posts/${e}/reactions`}).then((e=>{o(e),c(!1)})).catch((()=>c(!1)))):c(!1)}),[e,t]),s?null:a&&Object.values(a).some((e=>e.items?.length>0))?(0,l.createElement)(l.Fragment,null,n&&(0,l.createElement)("h6",null,n),Object.entries(a).map((([e,t])=>t.items?.length?(0,l.createElement)(d,{key:e,items:t.items,label:t.label}):null))):null}const v=e=>{const t=["#FF6B6B","#4ECDC4","#45B7D1","#96CEB4","#FFEEAD","#D4A5A5","#9B59B6","#3498DB","#E67E22"],n=(()=>{const e=["Bouncy","Cosmic","Dancing","Fluffy","Giggly","Hoppy","Jazzy","Magical","Nifty","Perky","Quirky","Sparkly","Twirly","Wiggly","Zippy"],t=["Badger","Capybara","Dolphin","Echidna","Flamingo","Giraffe","Hedgehog","Iguana","Jellyfish","Koala","Lemur","Manatee","Narwhal","Octopus","Penguin"];return`${e[Math.floor(Math.random()*e.length)]} ${t[Math.floor(Math.random()*t.length)]}`})(),r=t[Math.floor(Math.random()*t.length)],a=n.charAt(0),l=document.createElement("canvas");l.width=64,l.height=64;const o=l.getContext("2d");return o.fillStyle=r,o.beginPath(),o.arc(32,32,32,0,2*Math.PI),o.fill(),o.fillStyle="#FFFFFF",o.font="32px sans-serif",o.textAlign="center",o.textBaseline="middle",o.fillText(a,32,32),{name:n,url:"#",avatar:l.toDataURL()}},b=JSON.parse('{"UU":"activitypub/reactions"}');(0,r.registerBlockType)(b.UU,{deprecated:a,edit:function({attributes:e,__unstableLayoutClassNames:t}){const n=(0,o.useBlockProps)({className:t}),[r]=(0,i.useState)({likes:{label:(0,s.sprintf)(/* translators: %d: Number of likes */ /* translators: %d: Number of likes */ -(0,s._x)("%d likes","number of likes","activitypub"),9),items:Array.from({length:9},((e,t)=>v()))},reposts:{label:(0,s.sprintf)(/* translators: %d: Number of reposts */ /* translators: %d: Number of reposts */ -(0,s._x)("%d reposts","number of reposts","activitypub"),6),items:Array.from({length:6},((e,t)=>v()))}}),a=[["core/heading",{level:6,placeholder:(0,s.__)("Fediverse Reactions","activitypub"),content:(0,s.__)("Fediverse Reactions","activitypub")}]];return(0,l.createElement)("div",{...n},(0,l.createElement)(o.InnerBlocks,{template:a,allowedBlocks:["core/heading"],templateLock:!1}),(0,l.createElement)(g,{reactions:r}))},save:function(){return(0,l.createElement)(l.Fragment,null,(0,l.createElement)(o.InnerBlocks.Content,null),(0,l.createElement)("div",{className:"activitypub-reactions-block"}))}})}},n={};function r(e){var a=n[e];if(void 0!==a)return a.exports;var l=n[e]={exports:{}};return t[e](l,l.exports,r),l.exports}r.m=t,e=[],r.O=(t,n,a,l)=>{if(!n){var o=1/0;for(u=0;u=l)&&Object.keys(r.O).every((e=>r.O[e](n[s])))?n.splice(s--,1):(i=!1,l0&&e[u-1][2]>l;u--)e[u]=e[u-1];e[u]=[n,a,l]},r.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return r.d(t,{a:t}),t},r.d=(e,t)=>{for(var n in t)r.o(t,n)&&!r.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},r.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e={608:0,104:0};r.O.j=t=>0===e[t];var t=(t,n)=>{var a,l,[o,i,s]=n,c=0;if(o.some((t=>0!==e[t]))){for(a in i)r.o(i,a)&&(r.m[a]=i[a]);if(s)var u=s(r)}for(t&&t(n);cr(29)));a=r.O(a)})(); \ No newline at end of file diff --git a/build/reactions/style-index-rtl.css b/build/reactions/style-index-rtl.css deleted file mode 100644 index 728c87784..000000000 --- a/build/reactions/style-index-rtl.css +++ /dev/null @@ -1 +0,0 @@ -.wp-block-activitypub-reactions .reaction-group{align-items:center;display:flex;gap:.75em;justify-content:flex-start;margin:.5em 0;position:relative;width:100%}@media(max-width:782px){.wp-block-activitypub-reactions .reaction-group:has(.reaction-avatars:not(:empty)){justify-content:space-between}}.wp-block-activitypub-reactions .reaction-avatars{align-items:center;display:flex;flex-direction:row;list-style:none;margin:0;padding:0}.wp-block-activitypub-reactions .reaction-avatars li{margin:0 0 0 -10px;padding:0}.wp-block-activitypub-reactions .reaction-avatars li:last-child{margin-left:0}.wp-block-activitypub-reactions .reaction-avatars li a{display:block;text-decoration:none}.wp-block-activitypub-reactions .reaction-avatars .reaction-avatar{max-height:32px;max-width:32px;overflow:hidden;-moz-force-broken-image-icon:1;border:.5px solid var(--wp--preset--color--contrast,hsla(0,0%,100%,.8));border-radius:50%;box-shadow:0 0 0 .5px hsla(0,0%,100%,.8),0 1px 3px rgba(0,0,0,.2);transition:transform .6s cubic-bezier(.34,1.56,.64,1);will-change:transform}.wp-block-activitypub-reactions .reaction-avatars .reaction-avatar.wave-active{transform:translateY(-5px)}.wp-block-activitypub-reactions .reaction-avatars .reaction-avatar.wave-active.rotate-clockwise{transform:translateY(-5px) rotate(-30deg)}.wp-block-activitypub-reactions .reaction-avatars .reaction-avatar.wave-active.rotate-counter{transform:translateY(-5px) rotate(30deg)}.wp-block-activitypub-reactions .reaction-avatars .reaction-avatar:hover{position:relative;z-index:1}.wp-block-activitypub-reactions .reaction-label.components-button{color:var(--wp--preset--color--contrast,--wp--preset--color--secondary,#2271b1);flex:0 0 auto;height:auto;padding:0;text-decoration:none;white-space:nowrap}.wp-block-activitypub-reactions .reaction-label.components-button:hover{color:var(--wp--preset--color--contrast,--wp--preset--color--secondary,#135e96);text-decoration:underline}.wp-block-activitypub-reactions .reaction-label.components-button:focus:not(:disabled){box-shadow:none;outline:1px solid var(--wp--preset--color--contrast,#135e96);outline-offset:2px}.activitypub-reaction-list{background-color:var(--wp--preset--color--background,var(--wp--preset--color--custom-background,var(--wp--preset--color--base)));list-style:none;margin:0;max-width:300px;padding:.25em .7em .25em 1.3em;width:-moz-max-content;width:max-content}.activitypub-reaction-list ul{margin:0;padding:0}.activitypub-reaction-list li{font-size:var(--wp--preset--font-size--small);margin:0;padding:0}.activitypub-reaction-list a{align-items:center;color:var(--wp--preset--color--contrast,var(--wp--preset--color--secondary));display:flex;font-size:var(--wp--preset--font-size--small,.75rem);gap:.5em;justify-content:flex-start;padding:.5em;text-decoration:none}.activitypub-reaction-list a:hover{text-decoration:underline}.activitypub-reaction-list a img{border-radius:50%;flex:none;height:24px;width:24px} diff --git a/build/reactions/style-index.css b/build/reactions/style-index.css deleted file mode 100644 index 6776730f0..000000000 --- a/build/reactions/style-index.css +++ /dev/null @@ -1 +0,0 @@ -.wp-block-activitypub-reactions .reaction-group{align-items:center;display:flex;gap:.75em;justify-content:flex-start;margin:.5em 0;position:relative;width:100%}@media(max-width:782px){.wp-block-activitypub-reactions .reaction-group:has(.reaction-avatars:not(:empty)){justify-content:space-between}}.wp-block-activitypub-reactions .reaction-avatars{align-items:center;display:flex;flex-direction:row;list-style:none;margin:0;padding:0}.wp-block-activitypub-reactions .reaction-avatars li{margin:0 -10px 0 0;padding:0}.wp-block-activitypub-reactions .reaction-avatars li:last-child{margin-right:0}.wp-block-activitypub-reactions .reaction-avatars li a{display:block;text-decoration:none}.wp-block-activitypub-reactions .reaction-avatars .reaction-avatar{max-height:32px;max-width:32px;overflow:hidden;-moz-force-broken-image-icon:1;border:.5px solid var(--wp--preset--color--contrast,hsla(0,0%,100%,.8));border-radius:50%;box-shadow:0 0 0 .5px hsla(0,0%,100%,.8),0 1px 3px rgba(0,0,0,.2);transition:transform .6s cubic-bezier(.34,1.56,.64,1);will-change:transform}.wp-block-activitypub-reactions .reaction-avatars .reaction-avatar.wave-active{transform:translateY(-5px)}.wp-block-activitypub-reactions .reaction-avatars .reaction-avatar.wave-active.rotate-clockwise{transform:translateY(-5px) rotate(30deg)}.wp-block-activitypub-reactions .reaction-avatars .reaction-avatar.wave-active.rotate-counter{transform:translateY(-5px) rotate(-30deg)}.wp-block-activitypub-reactions .reaction-avatars .reaction-avatar:hover{position:relative;z-index:1}.wp-block-activitypub-reactions .reaction-label.components-button{color:var(--wp--preset--color--contrast,--wp--preset--color--secondary,#2271b1);flex:0 0 auto;height:auto;padding:0;text-decoration:none;white-space:nowrap}.wp-block-activitypub-reactions .reaction-label.components-button:hover{color:var(--wp--preset--color--contrast,--wp--preset--color--secondary,#135e96);text-decoration:underline}.wp-block-activitypub-reactions .reaction-label.components-button:focus:not(:disabled){box-shadow:none;outline:1px solid var(--wp--preset--color--contrast,#135e96);outline-offset:2px}.activitypub-reaction-list{background-color:var(--wp--preset--color--background,var(--wp--preset--color--custom-background,var(--wp--preset--color--base)));list-style:none;margin:0;max-width:300px;padding:.25em 1.3em .25em .7em;width:-moz-max-content;width:max-content}.activitypub-reaction-list ul{margin:0;padding:0}.activitypub-reaction-list li{font-size:var(--wp--preset--font-size--small);margin:0;padding:0}.activitypub-reaction-list a{align-items:center;color:var(--wp--preset--color--contrast,var(--wp--preset--color--secondary));display:flex;font-size:var(--wp--preset--font-size--small,.75rem);gap:.5em;justify-content:flex-start;padding:.5em;text-decoration:none}.activitypub-reaction-list a:hover{text-decoration:underline}.activitypub-reaction-list a img{border-radius:50%;flex:none;height:24px;width:24px} diff --git a/build/reactions/view.asset.php b/build/reactions/view.asset.php deleted file mode 100644 index 2f63d937d..000000000 --- a/build/reactions/view.asset.php +++ /dev/null @@ -1 +0,0 @@ - array('react', 'wp-api-fetch', 'wp-components', 'wp-dom-ready', 'wp-element', 'wp-i18n'), 'version' => '2f19592adb33b4030d68'); diff --git a/build/reactions/view.js b/build/reactions/view.js deleted file mode 100644 index 666da2de3..000000000 --- a/build/reactions/view.js +++ /dev/null @@ -1 +0,0 @@ -(()=>{"use strict";var e={n:t=>{var n=t&&t.__esModule?()=>t.default:()=>t;return e.d(n,{a:n}),n},d:(t,n)=>{for(var r in n)e.o(n,r)&&!e.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:n[r]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)};const t=window.React,n=window.wp.element,r=window.wp.domReady;var a=e.n(r);const c=window.wp.components,o=window.wp.apiFetch;var l=e.n(o);function s(){return window._activityPubOptions||{}}window.wp.i18n;const i=({reactions:e})=>{const{defaultAvatarUrl:r}=s(),[a,c]=(0,n.useState)(new Set),[o,l]=(0,n.useState)(new Map),i=(0,n.useRef)([]),u=()=>{i.current.forEach((e=>clearTimeout(e))),i.current=[]},m=(t,n)=>{u();const r=100,a=e.length;n&&l((e=>{const n=new Map(e);return n.set(t,"clockwise"),n}));const o=e=>{const o="right"===e,s=o?a-1:0,u=o?1:-1;for(let e=o?t:t-1;o?e<=s:e>=s;e+=u){const a=Math.abs(e-t),o=setTimeout((()=>{c((t=>{const r=new Set(t);return n?r.add(e):r.delete(e),r})),n&&e!==t&&l((t=>{const n=new Map(t),r=e-u,a=n.get(r);return n.set(e,"clockwise"===a?"counter":"clockwise"),n}))}),a*r);i.current.push(o)}};if(o("right"),o("left"),!n){const e=Math.max((a-t)*r,t*r),n=setTimeout((()=>{l(new Map)}),e+r);i.current.push(n)}};return(0,n.useEffect)((()=>()=>u()),[]),(0,t.createElement)("ul",{className:"reaction-avatars"},e.map(((e,n)=>{const c=o.get(n),l=["reaction-avatar",a.has(n)?"wave-active":"",c?`rotate-${c}`:""].filter(Boolean).join(" "),s=e.avatar||r;return(0,t.createElement)("li",{key:n},(0,t.createElement)("a",{href:e.url,target:"_blank",rel:"noopener noreferrer",onMouseEnter:()=>m(n,!0),onMouseLeave:()=>m(n,!1)},(0,t.createElement)("img",{src:s,alt:e.name,className:l,width:"32",height:"32",onError:e=>{e.target.src=r}})))})))},u=({reactions:e,type:n})=>(0,t.createElement)("ul",{className:"activitypub-reaction-list"},e.map(((e,n)=>(0,t.createElement)("li",{key:n},(0,t.createElement)("a",{href:e.url,className:"reaction-item",target:"_blank",rel:"noopener noreferrer"},(0,t.createElement)("img",{src:e.avatar,alt:e.name,width:"32",height:"32"}),(0,t.createElement)("span",null,e.name)))))),m=({items:e,label:r})=>{const[a,o]=(0,n.useState)(!1),[l,s]=(0,n.useState)(null),[m,p]=(0,n.useState)(e.length),h=(0,n.useRef)(null);(0,n.useEffect)((()=>{if(!h.current)return;const t=()=>{const t=h.current;if(!t)return;const n=t.offsetWidth-(l?.offsetWidth||0)-12,r=Math.max(1,Math.floor((n-32)/22));p(Math.min(r,e.length))};t();const n=new ResizeObserver(t);return n.observe(h.current),()=>{n.disconnect()}}),[l,e.length]);const f=e.slice(0,m);return(0,t.createElement)("div",{className:"reaction-group",ref:h},(0,t.createElement)(i,{reactions:f}),(0,t.createElement)(c.Button,{ref:s,className:"reaction-label is-link",onClick:()=>o(!a),"aria-expanded":a},r),a&&l&&(0,t.createElement)(c.Popover,{anchor:l,onClose:()=>o(!1)},(0,t.createElement)(u,{reactions:e})))};function p({postId:e=null,reactions:r=null,title:a=null}){const{namespace:c}=s(),[o,i]=(0,n.useState)(r),[u,p]=(0,n.useState)(!r);return(0,n.useEffect)((()=>{if(r)return i(r),void p(!1);e?(p(!0),l()({path:`/${c}/posts/${e}/reactions`}).then((e=>{i(e),p(!1)})).catch((()=>p(!1)))):p(!1)}),[e,r]),u?null:o&&Object.values(o).some((e=>e.items?.length>0))?(0,t.createElement)(t.Fragment,null,a&&(0,t.createElement)("h6",null,a),Object.entries(o).map((([e,n])=>n.items?.length?(0,t.createElement)(m,{key:e,items:n.items,label:n.label}):null))):null}a()((()=>{[].forEach.call(document.querySelectorAll(".activitypub-reactions-block"),(e=>{const r=JSON.parse(e.dataset.attrs||e.parentElement.dataset.attrs);(0,n.createRoot)(e).render((0,t.createElement)(p,{...r}))}))}))})(); \ No newline at end of file diff --git a/build/reply/index.js b/build/reply/index.js deleted file mode 100644 index c8ee59e4e..000000000 --- a/build/reply/index.js +++ /dev/null @@ -1 +0,0 @@ -(()=>{"use strict";var e,t={20:(e,t,r)=>{var n=r(609),o=Symbol.for("react.element"),a=(Symbol.for("react.fragment"),Object.prototype.hasOwnProperty),i=n.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,l={key:!0,ref:!0,__self:!0,__source:!0};t.jsx=function(e,t,r){var n,c={},s=null,d=null;for(n in void 0!==r&&(s=""+r),void 0!==t.key&&(s=""+t.key),void 0!==t.ref&&(d=t.ref),t)a.call(t,n)&&!l.hasOwnProperty(n)&&(c[n]=t[n]);if(e&&e.defaultProps)for(n in t=e.defaultProps)void 0===c[n]&&(c[n]=t[n]);return{$$typeof:o,type:e,key:s,ref:d,props:c,_owner:i.current}}},238:(e,t,r)=>{const n=window.wp.blocks,o=window.wp.primitives;var a=r(848);const i=(0,a.jsx)(o.SVG,{width:"24",height:"24",viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg",children:(0,a.jsx)(o.Path,{d:"M6.68822 10.625L6.24878 11.0649L5.5 11.8145L5.5 5.5L12.5 5.5V8L14 6.5V5C14 4.44772 13.5523 4 13 4H5C4.44772 4 4 4.44771 4 5V13.5247C4 13.8173 4.16123 14.086 4.41935 14.2237C4.72711 14.3878 5.10601 14.3313 5.35252 14.0845L7.31 12.125H8.375L9.875 10.625H7.31H6.68822ZM14.5605 10.4983L11.6701 13.75H16.9975C17.9963 13.75 18.7796 14.1104 19.3553 14.7048C19.9095 15.2771 20.2299 16.0224 20.4224 16.7443C20.7645 18.0276 20.7543 19.4618 20.7487 20.2544C20.7481 20.345 20.7475 20.4272 20.7475 20.4999L19.2475 20.5001C19.2475 20.4191 19.248 20.3319 19.2484 20.2394V20.2394C19.2526 19.4274 19.259 18.2035 18.973 17.1307C18.8156 16.5401 18.586 16.0666 18.2778 15.7483C17.9909 15.4521 17.5991 15.25 16.9975 15.25H11.8106L14.5303 17.9697L13.4696 19.0303L8.96956 14.5303L13.4394 9.50171L14.5605 10.4983Z"})});var l=r(609);const c=window.wp.blockEditor,s=window.wp.components,d=window.wp.i18n,u=window.wp.element,m=window.wp.compose,p=window.wp.apiFetch;var f=r.n(p);const h=window.wp.url,w=window.wp.data;function b({html:e}){const t=(0,u.useRef)(null),[r,n]=(0,u.useState)(300),o=(0,u.useRef)(300),a=(0,u.useCallback)((()=>{if(t.current)try{const e=t.current;let r=300;try{e.contentDocument&&e.contentDocument.body?r=e.contentDocument.body.scrollHeight:e.contentWindow&&e.contentWindow.document&&e.contentWindow.document.body&&(r=e.contentWindow.document.body.scrollHeight)}catch(e){console.log("Could not access iframe content document:",e)}r+=5,Math.abs(r-o.current)>5&&(o.current=r,n(r))}catch(e){console.error("Error adjusting iframe height:",e)}}),[]),i=(0,u.useCallback)((()=>{if(t.current)try{a()}catch(e){console.error("Error setting up iframe height adjustment:",e)}}),[a]);return(0,u.useEffect)((()=>{t.current&&t.current.addEventListener("load",i);const e=setInterval(a,1e3);return()=>{clearInterval(e),t.current&&t.current.removeEventListener("load",i)}}),[i,a]),(0,u.useEffect)((()=>{if(t.current){const e=setTimeout((()=>{a()}),100);return()=>clearTimeout(e)}}),[e,a]),{iframeRef:t,iframeHeight:r,adjustIframeHeight:a,handleIframeLoad:i}}const v={class:"className",frameborder:"frameBorder",allowfullscreen:"allowFullScreen",allowtransparency:"allowTransparency",marginheight:"marginHeight",marginwidth:"marginWidth"};function y({onClick:e}){return(0,l.createElement)("div",{className:"activitypub-embed-overlay",onClick:e,style:{position:"absolute",top:0,left:0,width:"100%",height:"100%",cursor:"pointer",zIndex:1}})}function g({html:e,onSelectBlock:t}){const r=(0,u.useRef)(),[n,o]=(0,u.useState)(282),[a,i]=(0,u.useState)(!1),c=(0,u.useCallback)((()=>{const t=(new window.DOMParser).parseFromString(e,"text/html").querySelector("iframe"),r={};return t?(Array.from(t.attributes).forEach((({name:e,value:t})=>{"style"!==e&&(r[v[e]||e]=t)})),r):r}),[e]),s=c();return(0,u.useEffect)((()=>{if(!r.current)return;const{ownerDocument:e}=r.current,{defaultView:t}=e;function n({data:{secret:e,message:t,value:r}={}}){"height"===t&&e===s["data-secret"]&&o(r)}return t.addEventListener("message",n),()=>{t.removeEventListener("message",n)}}),[s]),s.src?(0,l.createElement)("div",{className:"wp-block-embed__wrapper",style:{position:"relative"}},(0,l.createElement)("iframe",{ref:r,title:s.title||(0,d.__)("Embedded WordPress content","activitypub"),...s,height:n,style:{width:"100%",maxWidth:"100%"}}),!a&&(0,l.createElement)(y,{onClick:t})):(0,l.createElement)("div",{className:"wp-block-embed__wrapper",style:{position:"relative"}},(0,l.createElement)("div",{dangerouslySetInnerHTML:{__html:e}}),(0,l.createElement)(y,{onClick:t}))}function _({html:e,onClick:t,isSelected:r}){const{iframeRef:n,iframeHeight:o,adjustIframeHeight:a,handleIframeLoad:i}=b({html:e}),c=(0,u.useCallback)((()=>`\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\t${e}\n\t\t\t\n\t\t\t\n\t\t`),[e]);return(0,l.createElement)("div",{className:"wp-block-embed__wrapper",style:{position:"relative"}},(0,l.createElement)("iframe",{ref:n,srcDoc:c(),sandbox:"allow-scripts allow-same-origin allow-popups allow-forms",style:{width:"100%",height:`${o}px`,border:"none",overflow:"hidden"},onLoad:i}),r&&(0,l.createElement)("div",{onClick:t,style:{position:"absolute",top:0,left:0,width:"100%",height:"100%",cursor:"pointer",zIndex:1,display:r?"block":"none"}}))}const E={default:(0,d.__)("Enter the URL of a post from the Fediverse (Mastodon, Pixelfed, etc.) that you want to reply to.","activitypub"),checking:()=>(0,l.createElement)(l.Fragment,null,(0,l.createElement)(s.Spinner,null)," "+(0,d.__)("Checking if this URL supports ActivityPub replies...","activitypub")),valid:(0,d.__)("The author will be notified of your response.","activitypub"),error:(0,d.__)("This URL probably won't receive your reply. We'll still try.","activitypub")},k={valid:(0,d.__)("This post can be embedded with your reply.","activitypub"),invalid:(0,d.__)("This post cannot be embedded.","activitypub")};(0,n.registerBlockType)("activitypub/reply",{edit:function({attributes:e,setAttributes:t,clientId:r,isSelected:n}){const{url:o}=e,{namespace:a}=window._activityPubOptions||{},[i,p]=(0,u.useState)(E.default),[v,y]=(0,u.useState)(!1),[C,L]=(0,u.useState)(!1),[S,O]=(0,u.useState)(!1),[P,x]=(0,u.useState)(!0===e.embedPost||!o),[R,T]=(0,u.useState)(null),{iframeRef:H,iframeHeight:I,adjustIframeHeight:j,handleIframeLoad:D}=b({html:R}),{insertAfterBlock:B,removeBlock:N}=(0,w.useDispatch)("core/block-editor"),W=(0,c.useBlockProps)(),F=(0,u.useRef)(),M=((0,u.useRef)(),(0,u.useRef)(P)),U=()=>{setTimeout((()=>F.current?.focus()),50)};(0,u.useEffect)((()=>{M.current=P}),[P]);const A=(0,u.useCallback)((e=>{y(e),M.current&&e&&t({embedPost:!0})}),[t]),V=(e=!1)=>{O(e),y(!1),L(!1),T("")},$=(0,m.useDebounce)((async e=>{if(e)try{V(!0),p(E.checking());const t=await f()({path:(0,h.addQueryArgs)(`${a}/url/validate`,{url:e})});A(t.is_activitypub),L(t.is_real_oembed),T(t.html||""),p(E.valid)}catch(e){V(),p(E.error)}finally{O(!1)}else V()}),250);return(0,u.useEffect)((()=>{o&&$(o)}),[o]),(0,l.createElement)(l.Fragment,null,(0,l.createElement)(c.InspectorControls,null,(0,l.createElement)(s.PanelBody,{title:(0,d.__)("Settings","activitypub")},(0,l.createElement)(s.ToggleControl,{label:(0,d.__)("Embed Post","activitypub"),checked:e.embedPost,onChange:e=>{t({embedPost:e}),x(e)},disabled:!v,help:v?k.valid:k.invalid}))),(0,l.createElement)("div",{...W},n&&(0,l.createElement)(s.TextControl,{label:(0,d.__)("Your post is a reply to the following URL","activitypub"),value:o,onChange:e=>t({url:e}),help:i,onKeyDown:t=>{"Enter"===t.key&&B(r),!e.url&&["Backspace","Delete"].includes(t.key)&&N(r)},ref:F}),v&&e.embedPost&&R&&(0,l.createElement)("div",{className:"activitypub-embed-container"},C&&(Y=R)&&(Y.includes("wp-embedded-content")||Y.includes("wp-embed/")||Y.includes('class="wp-embed"'))?(0,l.createElement)(g,{html:R,onSelectBlock:U}):(0,l.createElement)(_,{html:R,onClick:U,isSelected:n})),o&&(!e.embedPost||!R)&&(0,l.createElement)("div",{className:"activitypub-reply-block-editor__preview",contentEditable:!1,onClick:U,style:{cursor:"pointer"}},(0,l.createElement)("a",{href:o,className:"u-in-reply-to",target:"_blank",rel:"noreferrer"},"↬"+o.replace(/^https?:\/\//,"")))));var Y},save:()=>null,icon:i})},609:e=>{e.exports=window.React},848:(e,t,r)=>{e.exports=r(20)}},r={};function n(e){var o=r[e];if(void 0!==o)return o.exports;var a=r[e]={exports:{}};return t[e](a,a.exports,n),a.exports}n.m=t,e=[],n.O=(t,r,o,a)=>{if(!r){var i=1/0;for(d=0;d=a)&&Object.keys(n.O).every((e=>n.O[e](r[c])))?r.splice(c--,1):(l=!1,a0&&e[d-1][2]>a;d--)e[d]=e[d-1];e[d]=[r,o,a]},n.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return n.d(t,{a:t}),t},n.d=(e,t)=>{for(var r in t)n.o(t,r)&&!n.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},n.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e={780:0,356:0};n.O.j=t=>0===e[t];var t=(t,r)=>{var o,a,[i,l,c]=r,s=0;if(i.some((t=>0!==e[t]))){for(o in l)n.o(l,o)&&(n.m[o]=l[o]);if(c)var d=c(n)}for(t&&t(r);sn(238)));o=n.O(o)})(); \ No newline at end of file diff --git a/build/reply/style-index-rtl.css b/build/reply/style-index-rtl.css deleted file mode 100644 index ec651ab69..000000000 --- a/build/reply/style-index-rtl.css +++ /dev/null @@ -1 +0,0 @@ -.activitypub-embed-container{margin-top:1em;min-height:100px;pointer-events:none;position:relative;-webkit-user-select:none;-moz-user-select:none;user-select:none}.activitypub-embed-loading{align-items:center;display:flex;justify-content:center}.activitypub-embed-container .wp-block-embed{pointer-events:none!important}.activitypub-embed-preview,.activitypub-embed-preview iframe{pointer-events:none}.activitypub-reply-display{margin:1em 0}.activitypub-reply-display p{margin:0}.activitypub-reply-display a{color:#2271b1;text-decoration:none}.activitypub-reply-display a:hover{color:#135e96;text-decoration:underline} diff --git a/build/reply/style-index.css b/build/reply/style-index.css deleted file mode 100644 index ec651ab69..000000000 --- a/build/reply/style-index.css +++ /dev/null @@ -1 +0,0 @@ -.activitypub-embed-container{margin-top:1em;min-height:100px;pointer-events:none;position:relative;-webkit-user-select:none;-moz-user-select:none;user-select:none}.activitypub-embed-loading{align-items:center;display:flex;justify-content:center}.activitypub-embed-container .wp-block-embed{pointer-events:none!important}.activitypub-embed-preview,.activitypub-embed-preview iframe{pointer-events:none}.activitypub-reply-display{margin:1em 0}.activitypub-reply-display p{margin:0}.activitypub-reply-display a{color:#2271b1;text-decoration:none}.activitypub-reply-display a:hover{color:#135e96;text-decoration:underline} diff --git a/build/wp-admin/admin-rtl.css b/build/wp-admin/admin-rtl.css new file mode 100644 index 000000000..31baaa4ed --- /dev/null +++ b/build/wp-admin/admin-rtl.css @@ -0,0 +1,3 @@ +.activitypub-settings{margin:0 auto;max-width:800px;position:relative}.settings_page_activitypub div:not(.wrap)>.notice{margin:0 auto 30px;max-width:800px}.settings_page_activitypub div:not(.wrap)>.update-nag{margin:25px 22px 15px 20px}.settings_page_activitypub .wrap{padding-right:22px}.activitypub-settings p.interactions{margin-bottom:1em}.activitypub-settings-header{background:#fff;border-bottom:1px solid #dcdcde;margin:0 0 1rem;text-align:center}.activitypub-settings-header h1{display:inline-block;font-size:23px;font-weight:600;line-height:1.3;margin:0 .8rem 1rem;padding:9px 0 4px}.activitypub-settings-title-section{align-items:center;clear:both;display:flex;justify-content:center;padding-top:8px}.settings_page_activitypub #wpcontent{padding-right:0}.activitypub-settings-tabs-scroller{overflow-x:auto;padding-top:2px;width:100%;-webkit-overflow-scrolling:touch;scroll-behavior:smooth}.activitypub-settings-tabs-wrapper{display:inline-flex;flex-wrap:nowrap;gap:0;vertical-align:top}.activitypub-settings-tab.active{box-shadow:inset 0 -3px #3582c4;font-weight:600}.activitypub-settings-tab{color:inherit;display:block;margin:0 1rem;padding:.5rem 1rem 1rem;text-decoration:none;transition:box-shadow .5s ease-in-out;white-space:nowrap}.activitypub-settings .row{margin-bottom:16px}.activitypub-settings .row>div{display:inline-flex;flex-direction:column;max-width:calc(100% - 24px)}.activitypub-settings .row .description{margin-top:0}.wp-header-end{margin:-2px 0 0;visibility:hidden}summary{color:#2271b1;cursor:pointer;text-decoration:underline}.activitypub-settings-accordion{border:1px solid #c3c4c7}.activitypub-settings-accordion-heading{border-top:1px solid #c3c4c7;color:inherit;font-size:inherit;font-weight:600;line-height:inherit;margin:0}.activitypub-settings-accordion-heading:first-child{border-top:none}.activitypub-settings-accordion-panel{background:#fff;margin:0;padding:1em 1.5em}.activitypub-settings-accordion-trigger{align-items:center;background:#fff;border:0;color:#2c3338;cursor:pointer;display:flex;font-weight:400;justify-content:space-between;margin:0;min-height:46px;padding:1em 1.5em 1em 3.5em;position:relative;text-align:right;-webkit-user-select:auto;-moz-user-select:auto;user-select:auto;width:100%}.activitypub-settings-accordion-trigger .title{flex-grow:1;font-weight:600;pointer-events:none}.activitypub-settings-accordion-trigger .icon,.activitypub-settings-accordion-viewed .icon{border:solid #50575e;border-width:0 0 2px 2px;height:.5rem;pointer-events:none;position:absolute;left:1.5em;top:50%;transform:translateY(-70%) rotate(-45deg);width:.5rem}.activitypub-settings-accordion-trigger[aria-expanded=true] .icon{transform:translateY(-30%) rotate(135deg)}.activitypub-settings-accordion-trigger:active,.activitypub-settings-accordion-trigger:hover{background:#f6f7f7}.activitypub-settings-accordion-trigger:focus{background-color:#f6f7f7;border:none;box-shadow:none;color:#1d2327;outline:2px solid #2271b1;outline-offset:-1px}.activitypub-settings +input.blog-user-identifier{text-align:left}.activitypub-settings +.header-image{background-image:#a8a5af;background-image:linear-gradient(-180deg,red,#ff0);background-size:cover;display:block;height:80px;margin-bottom:40px;position:relative;width:100%}.activitypub-settings .logo{height:80px;right:40px;position:relative;top:40px;width:80px}.settings_page_activitypub .plugin-recommendations{border-bottom:none;margin-bottom:0}#dashboard_right_now li a.activitypub-followers:before{content:"\f307";font-family:dashicons}.like .dashboard-comment-wrap,.repost .dashboard-comment-wrap{padding-inline-start:63px}.like .dashboard-comment-wrap .comment-author,.repost .dashboard-comment-wrap .comment-author{margin-block:0}.extra-fields-nav a+a{margin-right:8px}.rtl .extra-fields-nav a+a{margin-right:auto;margin-left:8px}.contextual-help-tabs-wrap dt{font-weight:600}.contextual-help-tabs-wrap .activitypub-block-screenshot{margin:10px 0}.contextual-help-tabs-wrap .activitypub-block-screenshot img{border:1px solid #ddd;border-radius:4px;box-shadow:0 1px 3px rgba(0,0,0,.1);height:auto;max-width:100%}.contextual-help-tabs-wrap .activitypub-block-screenshot figcaption{color:#555;font-size:.9em;font-style:italic;margin-top:5px}.contextual-help-tabs-wrap blockquote{background-color:#f6f7f7;border-right:4px solid #3582c4;margin:0 0 20px;padding:16px 20px}.contextual-help-tabs-wrap blockquote p{line-height:1.5;margin:0 0 10px}.contextual-help-tabs-wrap blockquote p:last-child{margin-bottom:0}.contextual-help-tabs-wrap blockquote cite{color:#50575e;display:block;font-size:.9em;font-weight:600;margin-top:8px}.contextual-help-tabs-wrap blockquote cite:before{content:"—"}.plugin-list{align-items:stretch;display:flex;flex-wrap:wrap;gap:16px}.plugin-list .plugin-card{border-radius:4px;box-shadow:0 1px 3px rgba(0,0,0,.1);box-sizing:border-box;display:flex;flex:1 1 300px;flex-direction:column;margin:0}.plugin-list .plugin-card .desc{flex:1 1 auto}.plugin-list .plugin-action-buttons li{margin:0 0 10px}.plugin-list .plugin-card .action-links{margin-right:148px;position:static;width:auto}.plugin-list .plugin-action-buttons{float:none;margin:1em 0 0;text-align:right}.plugin-list .plugin-action-buttons li{display:inline-block;vertical-align:middle}.plugin-list .plugin-action-buttons li .button{margin-left:20px}.plugin-list .plugin-card h3{margin-left:24px}.plugin-card .desc>p,.plugin-list .plugin-card .desc,.plugin-list .plugin-card .name{margin-left:0}.plugin-list .plugin-card .desc p:first-of-type{margin-top:0}.rtl .contextual-help-tabs-wrap blockquote{border-right:none;border-left:4px solid #3582c4;padding:16px 20px}#activitypub-follow-form .highlight{animation:highlight-fade 3s ease-in-out;border-color:#3582c4!important;box-shadow:0 0 0 1px #3582c4}@keyframes highlight-fade{0%{background-color:#e7f3ff;border-color:#3582c4;box-shadow:0 0 0 1px #3582c4}to{background-color:#fff;border-color:#8c8f94;box-shadow:none}}@media screen and (max-width:782px){.activitypub-settings{margin:0 22px}.activitypub-settings .row>div{max-width:calc(100% - 36px);width:100%}} diff --git a/build/wp-admin/admin.css b/build/wp-admin/admin.css new file mode 100644 index 000000000..0fec908dd --- /dev/null +++ b/build/wp-admin/admin.css @@ -0,0 +1,3 @@ +.activitypub-settings{margin:0 auto;max-width:800px;position:relative}.settings_page_activitypub div:not(.wrap)>.notice{margin:0 auto 30px;max-width:800px}.settings_page_activitypub div:not(.wrap)>.update-nag{margin:25px 20px 15px 22px}.settings_page_activitypub .wrap{padding-left:22px}.activitypub-settings p.interactions{margin-bottom:1em}.activitypub-settings-header{background:#fff;border-bottom:1px solid #dcdcde;margin:0 0 1rem;text-align:center}.activitypub-settings-header h1{display:inline-block;font-size:23px;font-weight:600;line-height:1.3;margin:0 .8rem 1rem;padding:9px 0 4px}.activitypub-settings-title-section{align-items:center;clear:both;display:flex;justify-content:center;padding-top:8px}.settings_page_activitypub #wpcontent{padding-left:0}.activitypub-settings-tabs-scroller{overflow-x:auto;padding-top:2px;width:100%;-webkit-overflow-scrolling:touch;scroll-behavior:smooth}.activitypub-settings-tabs-wrapper{display:inline-flex;flex-wrap:nowrap;gap:0;vertical-align:top}.activitypub-settings-tab.active{box-shadow:inset 0 -3px #3582c4;font-weight:600}.activitypub-settings-tab{color:inherit;display:block;margin:0 1rem;padding:.5rem 1rem 1rem;text-decoration:none;transition:box-shadow .5s ease-in-out;white-space:nowrap}.activitypub-settings .row{margin-bottom:16px}.activitypub-settings .row>div{display:inline-flex;flex-direction:column;max-width:calc(100% - 24px)}.activitypub-settings .row .description{margin-top:0}.wp-header-end{margin:-2px 0 0;visibility:hidden}summary{color:#2271b1;cursor:pointer;text-decoration:underline}.activitypub-settings-accordion{border:1px solid #c3c4c7}.activitypub-settings-accordion-heading{border-top:1px solid #c3c4c7;color:inherit;font-size:inherit;font-weight:600;line-height:inherit;margin:0}.activitypub-settings-accordion-heading:first-child{border-top:none}.activitypub-settings-accordion-panel{background:#fff;margin:0;padding:1em 1.5em}.activitypub-settings-accordion-trigger{align-items:center;background:#fff;border:0;color:#2c3338;cursor:pointer;display:flex;font-weight:400;justify-content:space-between;margin:0;min-height:46px;padding:1em 3.5em 1em 1.5em;position:relative;text-align:left;-webkit-user-select:auto;-moz-user-select:auto;user-select:auto;width:100%}.activitypub-settings-accordion-trigger .title{flex-grow:1;font-weight:600;pointer-events:none}.activitypub-settings-accordion-trigger .icon,.activitypub-settings-accordion-viewed .icon{border:solid #50575e;border-width:0 2px 2px 0;height:.5rem;pointer-events:none;position:absolute;right:1.5em;top:50%;transform:translateY(-70%) rotate(45deg);width:.5rem}.activitypub-settings-accordion-trigger[aria-expanded=true] .icon{transform:translateY(-30%) rotate(-135deg)}.activitypub-settings-accordion-trigger:active,.activitypub-settings-accordion-trigger:hover{background:#f6f7f7}.activitypub-settings-accordion-trigger:focus{background-color:#f6f7f7;border:none;box-shadow:none;color:#1d2327;outline:2px solid #2271b1;outline-offset:-1px}.activitypub-settings +input.blog-user-identifier{text-align:right}.activitypub-settings +.header-image{background-image:#a8a5af;background-image:linear-gradient(180deg,red,#ff0);background-size:cover;display:block;height:80px;margin-bottom:40px;position:relative;width:100%}.activitypub-settings .logo{height:80px;left:40px;position:relative;top:40px;width:80px}.settings_page_activitypub .plugin-recommendations{border-bottom:none;margin-bottom:0}#dashboard_right_now li a.activitypub-followers:before{content:"\f307";font-family:dashicons}.like .dashboard-comment-wrap,.repost .dashboard-comment-wrap{padding-inline-start:63px}.like .dashboard-comment-wrap .comment-author,.repost .dashboard-comment-wrap .comment-author{margin-block:0}.extra-fields-nav a+a{margin-left:8px}.rtl .extra-fields-nav a+a{margin-left:auto;margin-right:8px}.contextual-help-tabs-wrap dt{font-weight:600}.contextual-help-tabs-wrap .activitypub-block-screenshot{margin:10px 0}.contextual-help-tabs-wrap .activitypub-block-screenshot img{border:1px solid #ddd;border-radius:4px;box-shadow:0 1px 3px rgba(0,0,0,.1);height:auto;max-width:100%}.contextual-help-tabs-wrap .activitypub-block-screenshot figcaption{color:#555;font-size:.9em;font-style:italic;margin-top:5px}.contextual-help-tabs-wrap blockquote{background-color:#f6f7f7;border-left:4px solid #3582c4;margin:0 0 20px;padding:16px 20px}.contextual-help-tabs-wrap blockquote p{line-height:1.5;margin:0 0 10px}.contextual-help-tabs-wrap blockquote p:last-child{margin-bottom:0}.contextual-help-tabs-wrap blockquote cite{color:#50575e;display:block;font-size:.9em;font-weight:600;margin-top:8px}.contextual-help-tabs-wrap blockquote cite:before{content:"—"}.plugin-list{align-items:stretch;display:flex;flex-wrap:wrap;gap:16px}.plugin-list .plugin-card{border-radius:4px;box-shadow:0 1px 3px rgba(0,0,0,.1);box-sizing:border-box;display:flex;flex:1 1 300px;flex-direction:column;margin:0}.plugin-list .plugin-card .desc{flex:1 1 auto}.plugin-list .plugin-action-buttons li{margin:0 0 10px}.plugin-list .plugin-card .action-links{margin-left:148px;position:static;width:auto}.plugin-list .plugin-action-buttons{float:none;margin:1em 0 0;text-align:left}.plugin-list .plugin-action-buttons li{display:inline-block;vertical-align:middle}.plugin-list .plugin-action-buttons li .button{margin-right:20px}.plugin-list .plugin-card h3{margin-right:24px}.plugin-card .desc>p,.plugin-list .plugin-card .desc,.plugin-list .plugin-card .name{margin-right:0}.plugin-list .plugin-card .desc p:first-of-type{margin-top:0}.rtl .contextual-help-tabs-wrap blockquote{border-left:none;border-right:4px solid #3582c4;padding:16px 20px}#activitypub-follow-form .highlight{animation:highlight-fade 3s ease-in-out;border-color:#3582c4!important;box-shadow:0 0 0 1px #3582c4}@keyframes highlight-fade{0%{background-color:#e7f3ff;border-color:#3582c4;box-shadow:0 0 0 1px #3582c4}to{background-color:#fff;border-color:#8c8f94;box-shadow:none}}@media screen and (max-width:782px){.activitypub-settings{margin:0 22px}.activitypub-settings .row>div{max-width:calc(100% - 36px);width:100%}} diff --git a/build/wp-admin/header-image.asset.php b/build/wp-admin/header-image.asset.php new file mode 100644 index 000000000..1af3a4e68 --- /dev/null +++ b/build/wp-admin/header-image.asset.php @@ -0,0 +1 @@ + array('jquery'), 'version' => 'ee5b8c2099a3b65812a6'); diff --git a/build/wp-admin/header-image.js b/build/wp-admin/header-image.js new file mode 100644 index 000000000..3a12fddb3 --- /dev/null +++ b/build/wp-admin/header-image.js @@ -0,0 +1,4 @@ +(()=>{"use strict";var t={n:e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return t.d(a,{a}),a},d:(e,a)=>{for(var i in a)t.o(a,i)&&!t.o(e,i)&&Object.defineProperty(e,i,{enumerable:!0,get:a[i]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e)};const e=window.jQuery;!function(t){var e,a,i=t("#activitypub-choose-from-library-button"),r=t("#activitypub-header-image-preview-wrapper"),s=t("#activitypub-header-image-preview"),d=t("#activitypub_header_image"),n=t("#activitypub-remove-header-image");function h(t){var e,a,i=t.get("width"),r=t.get("height"),s=1500,d=500,n=s/d,h=s,o=d;return i/r>n?s=(d=r)*n:d=(s=i)/n,{aspectRatio:s+":"+d,handles:!0,keys:!0,instance:!0,persistent:!0,imageWidth:i,imageHeight:r,minWidth:h>s?s:h,minHeight:o>d?d:o,x1:e=(i-s)/2,y1:a=(r-d)/2,x2:s+e,y2:d+a}}function o(t){var e;t.alt?wp.i18n.sprintf(/* translators: %s: The selected image alt text. */ +wp.i18n.__("Header Image preview: Current image: %s"),t.alt):(e=wp.i18n.sprintf(/* translators: %s: The selected image filename. */ +wp.i18n.__("Header Image preview: The current image has no alternative text. The file name is: %s"),t.filename),wp.i18n.sprintf(/* translators: %s: The selected image filename. */ +wp.i18n.__("Header Image preview: The current image has no alternative text. The file name is: %s"),t.filename)),s.attr({src:t.url,alt:e}),r.removeClass("hidden"),n.removeClass("hidden"),"1"!==i.attr("data-state")&&i.attr({class:i.attr("data-alt-classes"),"data-alt-classes":i.attr("class"),"data-state":"1"}),i.text(i.attr("data-update-text"))}a=wp.media.controller.CustomizeImageCropper.extend({doCrop:function(t){var e=t.get("cropDetails"),a=this.get("control"),i=e.width/e.height;return a.params.flex_width&&a.params.flex_height?(e.dst_width=e.width,e.dst_height=e.height):(e.dst_width=a.params.flex_width?a.params.height*i:a.params.width,e.dst_height=a.params.flex_height?a.params.width/i:a.params.height),wp.ajax.post("crop-image",{nonce:t.get("nonces").edit,id:t.get("id"),context:a.id,cropDetails:e})}}),i.on("click",(function(){var i=t(this),r=i.data("userId"),s={type:"image"};r&&(s.author=r),(e=wp.media({button:{text:i.data("update"),close:!1},states:[new wp.media.controller.Library({title:i.data("choose-text"),library:wp.media.query(s),date:!1,suggestedWidth:i.data("width"),suggestedHeight:i.data("height")}),new a({control:{id:"activitypub-header-image",params:{width:i.data("width"),height:i.data("height")}},imgSelectOptions:h})]})).on("cropped",(function(t){d.val(t.id),o(t),e.close(),e=null})),e.on("select",(function(){var t=e.state().get("selection").first(),a=i.data("width")/i.data("height"),r=t.attributes.width/t.attributes.height,s=!1;Math.abs(r-a)<.01&&(t.id!==parseInt(d.val(),10)&&d.val(t.id),s=!0),s?(o(t.attributes),e.close()):e.setState("cropper")})),e.open()})),n.on("click",(function(){d.val("false"),t(this).toggleClass("hidden"),r.toggleClass("hidden"),s.attr({src:"",alt:""}),i.attr({class:i.attr("data-alt-classes"),"data-alt-classes":i.attr("class"),"data-state":""}).text(i.attr("data-choose-text")).trigger("focus")}))}(t.n(e)())})(); \ No newline at end of file diff --git a/build/wp-admin/post-preview-rtl.css b/build/wp-admin/post-preview-rtl.css new file mode 100644 index 000000000..f0b922def --- /dev/null +++ b/build/wp-admin/post-preview-rtl.css @@ -0,0 +1 @@ +body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-size:1em;line-height:1.5;margin:0;padding:0}main{background-color:#fff;border:1px solid #ccc;border-radius:4px;flex:1;margin:1em;max-width:600px}main p{margin-bottom:1em}hr{background:transparent;border:0;border-top:1px solid #ccc;flex:0 0 auto;margin:10px 0}.columns{display:flex;flex-direction:row;justify-content:space-between;margin:0 auto;max-width:1200px}.sidebar{flex:1;max-width:285px;padding:1em}.sidebar h1{background-color:#6364ff;border-radius:4px;color:#fff;display:inline-block;font-size:1.5em;margin-bottom:1em;margin-top:0;padding:5px 10px}.sidebar ul{list-style-type:none;padding:0}.sidebar ul li{color:#ccc;padding:5px}.sidebar input[type=search],.sidebar textarea{background-color:#f6f6f6;border:1px solid #ccc;border-radius:4px;box-sizing:border-box;color:#333;display:block;font-size:1em;margin-bottom:1em;padding:.5em;width:100%}.sidebar>div,main address{align-items:center;display:flex;font-style:normal;margin-bottom:1em}main address .name,main address .webfinger{color:#000}.name,.webfinger{color:#ccc;display:block;font-weight:700}.webfinger{font-size:.8em;margin-top:.5em}.sidebar .fake-image,address img{background-color:#333;border-radius:8px;height:48px;margin-left:1em;width:48px}main article{padding:1em}main .content{margin:1em 0}main .content,main .content h2{font-size:1.2em}main .attachments{border-radius:8px;box-sizing:border-box;display:grid;gap:2px;grid-template-columns:1fr 1fr;grid-template-rows:1fr 1fr;margin:1em 0;min-height:64px;overflow:hidden;position:relative;width:100%}main .attachments.layout-1{grid-template-columns:1fr;grid-template-rows:1fr}main .attachments.layout-2{aspect-ratio:auto;grid-template-rows:1fr;height:auto}main .attachments.layout-3>img:first-child{grid-row:span 2}main .attachments img{border:0;box-sizing:border-box;display:inline-block;height:100%;-o-object-fit:cover;object-fit:cover;overflow:hidden;position:relative;width:100%}main .attachments audio,main .attachments video{display:block;grid-column:1/span 2;margin:1em 0;max-width:100%}main .attachments audio{width:100%}main .tags a{background-color:#f6f6f6;border-radius:4px;color:#333;display:inline-block;margin-left:.5em;padding:.5em;text-decoration:none}main .tags a:hover{background-color:#e6e6e6;text-decoration:underline}main .column-header{border-bottom:1px solid #ccc;font-size:1.5em;margin:0;padding:5px 10px;vertical-align:middle} diff --git a/build/wp-admin/post-preview.css b/build/wp-admin/post-preview.css new file mode 100644 index 000000000..2f6c5d371 --- /dev/null +++ b/build/wp-admin/post-preview.css @@ -0,0 +1 @@ +body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-size:1em;line-height:1.5;margin:0;padding:0}main{background-color:#fff;border:1px solid #ccc;border-radius:4px;flex:1;margin:1em;max-width:600px}main p{margin-bottom:1em}hr{background:transparent;border:0;border-top:1px solid #ccc;flex:0 0 auto;margin:10px 0}.columns{display:flex;flex-direction:row;justify-content:space-between;margin:0 auto;max-width:1200px}.sidebar{flex:1;max-width:285px;padding:1em}.sidebar h1{background-color:#6364ff;border-radius:4px;color:#fff;display:inline-block;font-size:1.5em;margin-bottom:1em;margin-top:0;padding:5px 10px}.sidebar ul{list-style-type:none;padding:0}.sidebar ul li{color:#ccc;padding:5px}.sidebar input[type=search],.sidebar textarea{background-color:#f6f6f6;border:1px solid #ccc;border-radius:4px;box-sizing:border-box;color:#333;display:block;font-size:1em;margin-bottom:1em;padding:.5em;width:100%}.sidebar>div,main address{align-items:center;display:flex;font-style:normal;margin-bottom:1em}main address .name,main address .webfinger{color:#000}.name,.webfinger{color:#ccc;display:block;font-weight:700}.webfinger{font-size:.8em;margin-top:.5em}.sidebar .fake-image,address img{background-color:#333;border-radius:8px;height:48px;margin-right:1em;width:48px}main article{padding:1em}main .content{margin:1em 0}main .content,main .content h2{font-size:1.2em}main .attachments{border-radius:8px;box-sizing:border-box;display:grid;gap:2px;grid-template-columns:1fr 1fr;grid-template-rows:1fr 1fr;margin:1em 0;min-height:64px;overflow:hidden;position:relative;width:100%}main .attachments.layout-1{grid-template-columns:1fr;grid-template-rows:1fr}main .attachments.layout-2{aspect-ratio:auto;grid-template-rows:1fr;height:auto}main .attachments.layout-3>img:first-child{grid-row:span 2}main .attachments img{border:0;box-sizing:border-box;display:inline-block;height:100%;-o-object-fit:cover;object-fit:cover;overflow:hidden;position:relative;width:100%}main .attachments audio,main .attachments video{display:block;grid-column:1/span 2;margin:1em 0;max-width:100%}main .attachments audio{width:100%}main .tags a{background-color:#f6f6f6;border-radius:4px;color:#333;display:inline-block;margin-right:.5em;padding:.5em;text-decoration:none}main .tags a:hover{background-color:#e6e6e6;text-decoration:underline}main .column-header{border-bottom:1px solid #ccc;font-size:1.5em;margin:0;padding:5px 10px;vertical-align:middle} diff --git a/build/wp-admin/script.asset.php b/build/wp-admin/script.asset.php new file mode 100644 index 000000000..c86a261fe --- /dev/null +++ b/build/wp-admin/script.asset.php @@ -0,0 +1 @@ + array('jquery'), 'version' => '8e8ff978d9eef5ac0a85'); diff --git a/build/wp-admin/script.js b/build/wp-admin/script.js new file mode 100644 index 000000000..f537291bb --- /dev/null +++ b/build/wp-admin/script.js @@ -0,0 +1 @@ +(()=>{"use strict";var t={n:e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return t.d(a,{a}),a},d:(e,a)=>{for(var i in a)t.o(a,i)&&!t.o(e,i)&&Object.defineProperty(e,i,{enumerable:!0,get:a[i]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e)};const e=window.jQuery;t.n(e)()((function(t){t(".activitypub-settings-accordion").on("click",".activitypub-settings-accordion-trigger",(function(){"true"===t(this).attr("aria-expanded")?(t(this).attr("aria-expanded","false"),t("#"+t(this).attr("aria-controls")).attr("hidden",!0)):(t(this).attr("aria-expanded","true"),t("#"+t(this).attr("aria-controls")).attr("hidden",!1))})),t(document).on("wp-plugin-install-success",(function(e,a){setTimeout((function(){t(".activate-now").removeClass("thickbox open-plugin-details-modal")}),1200)}))}))})(); \ No newline at end of file diff --git a/build/wp-admin/welcome-rtl.css b/build/wp-admin/welcome-rtl.css new file mode 100644 index 000000000..e068ac32a --- /dev/null +++ b/build/wp-admin/welcome-rtl.css @@ -0,0 +1 @@ +.activitypub-welcome-container{background-color:#fff;border-radius:8px;box-shadow:0 1px 3px rgba(0,0,0,.1);margin:40px auto;max-width:800px;padding:30px}.activitypub-welcome-header{margin-bottom:30px;position:relative;text-align:center}.activitypub-progress-circle{height:120px;margin:0 auto 20px;position:relative;width:120px}.activitypub-progress-circle-content{align-items:center;color:#1e1e1e;display:flex;font-size:16px;font-weight:500;height:100%;justify-content:center;right:0;position:absolute;top:0;width:100%;z-index:2}.activitypub-progress-ring{overflow:visible;transform:rotate(90deg)}.activitypub-progress-ring-bg{fill:none;stroke:#f0f0f1;stroke-width:6}.activitypub-progress-ring-circle{fill:none;stroke:#2271b1;stroke-width:6;stroke-linecap:round;transition:stroke-dashoffset .5s ease}.activitypub-welcome-title{font-size:28px;font-weight:400;margin:20px 0 10px}.activitypub-welcome-subtitle{color:#646970;font-size:16px;font-weight:400;margin:0 0 20px}.activitypub-onboarding-step{align-items:center;background-color:#f6f7f7;border-radius:4px;display:flex;margin-bottom:15px;padding:20px;transition:background-color .2s ease}.activitypub-onboarding-step:last-child{margin-bottom:0}.activitypub-onboarding-step:hover{background-color:#f0f0f1}.activitypub-step-completed{background-color:#f0f7ee}.activitypub-step-completed:hover{background-color:#e2f1dc}.activitypub-step-completed .step-text h3{margin:0}.activitypub-step-completed .step-text h3:after{content:"."}.activitypub-step-completed .step-action,.activitypub-step-completed .step-text p{display:none}.step-indicator{flex-shrink:0;margin-left:15px}.step-icon{align-items:center;display:flex;font-size:24px;height:24px;justify-content:center;width:24px}.activitypub-step-completed .step-icon{color:#008a20}.dashicons-warning{color:#dba617}.step-content{align-items:center;display:flex;flex-grow:1;justify-content:space-between;width:100%}.step-text{flex-grow:1}.step-text h3{font-size:16px;font-weight:500;margin:0 0 5px}.step-text p{color:#646970;font-size:14px;margin:0}.step-action{flex-shrink:0;margin-right:20px}.step-action .button{min-width:120px;text-align:center}.activitypub-profiles-section{border-top:1px solid #f0f0f1;margin-top:40px;padding-top:30px}.profiles-description{color:#1e1e1e;font-size:16px;margin-bottom:20px}.activitypub-profiles-container{display:flex;flex-wrap:wrap;gap:20px;margin-bottom:30px}.activitypub-profile-card{background-color:#fff;border:1px solid #c3c4c7;border-radius:4px;box-shadow:0 1px 3px rgba(0,0,0,.1);flex:1;min-width:300px}.profile-card-header{align-items:center;background-color:#f0f0f1;border-bottom:1px solid #c3c4c7;display:flex;padding:15px}.profile-icon{margin-left:10px}.profile-icon .dashicons{font-size:20px;height:20px;width:20px}.profile-card-header h3{font-size:16px;font-weight:500;margin:0}.profile-card-content{padding:15px}.profile-field{margin-bottom:15px}.profile-field label{color:#646970;display:block;font-size:13px;font-weight:500;margin-bottom:5px}.profile-field input{background-color:#f6f7f7;border:1px solid #dcdcde;border-radius:3px;font-size:13px;padding:8px;width:100%}.profile-description{color:#646970;font-size:13px;line-height:1.5;margin:15px 0}.profile-card-content .button{margin-top:10px;text-align:center;width:100%}.activitypub-welcome-footer{margin-top:30px;text-align:center}.skip-steps-link{color:#2271b1;font-size:14px;text-decoration:none}.skip-steps-link:hover{color:#135e96;text-decoration:underline}@media screen and (max-width:782px){.activitypub-welcome-container{margin:20px;padding:20px}.step-content{align-items:flex-start;flex-direction:column}.step-action{margin-right:0;margin-top:15px;width:100%}.step-action .button{text-align:center;width:100%}.activitypub-profiles-container{flex-direction:column}.activitypub-profile-card{width:100%}} diff --git a/build/wp-admin/welcome.css b/build/wp-admin/welcome.css new file mode 100644 index 000000000..06a56e9da --- /dev/null +++ b/build/wp-admin/welcome.css @@ -0,0 +1 @@ +.activitypub-welcome-container{background-color:#fff;border-radius:8px;box-shadow:0 1px 3px rgba(0,0,0,.1);margin:40px auto;max-width:800px;padding:30px}.activitypub-welcome-header{margin-bottom:30px;position:relative;text-align:center}.activitypub-progress-circle{height:120px;margin:0 auto 20px;position:relative;width:120px}.activitypub-progress-circle-content{align-items:center;color:#1e1e1e;display:flex;font-size:16px;font-weight:500;height:100%;justify-content:center;left:0;position:absolute;top:0;width:100%;z-index:2}.activitypub-progress-ring{overflow:visible;transform:rotate(-90deg)}.activitypub-progress-ring-bg{fill:none;stroke:#f0f0f1;stroke-width:6}.activitypub-progress-ring-circle{fill:none;stroke:#2271b1;stroke-width:6;stroke-linecap:round;transition:stroke-dashoffset .5s ease}.activitypub-welcome-title{font-size:28px;font-weight:400;margin:20px 0 10px}.activitypub-welcome-subtitle{color:#646970;font-size:16px;font-weight:400;margin:0 0 20px}.activitypub-onboarding-step{align-items:center;background-color:#f6f7f7;border-radius:4px;display:flex;margin-bottom:15px;padding:20px;transition:background-color .2s ease}.activitypub-onboarding-step:last-child{margin-bottom:0}.activitypub-onboarding-step:hover{background-color:#f0f0f1}.activitypub-step-completed{background-color:#f0f7ee}.activitypub-step-completed:hover{background-color:#e2f1dc}.activitypub-step-completed .step-text h3{margin:0}.activitypub-step-completed .step-text h3:after{content:"."}.activitypub-step-completed .step-action,.activitypub-step-completed .step-text p{display:none}.step-indicator{flex-shrink:0;margin-right:15px}.step-icon{align-items:center;display:flex;font-size:24px;height:24px;justify-content:center;width:24px}.activitypub-step-completed .step-icon{color:#008a20}.dashicons-warning{color:#dba617}.step-content{align-items:center;display:flex;flex-grow:1;justify-content:space-between;width:100%}.step-text{flex-grow:1}.step-text h3{font-size:16px;font-weight:500;margin:0 0 5px}.step-text p{color:#646970;font-size:14px;margin:0}.step-action{flex-shrink:0;margin-left:20px}.step-action .button{min-width:120px;text-align:center}.activitypub-profiles-section{border-top:1px solid #f0f0f1;margin-top:40px;padding-top:30px}.profiles-description{color:#1e1e1e;font-size:16px;margin-bottom:20px}.activitypub-profiles-container{display:flex;flex-wrap:wrap;gap:20px;margin-bottom:30px}.activitypub-profile-card{background-color:#fff;border:1px solid #c3c4c7;border-radius:4px;box-shadow:0 1px 3px rgba(0,0,0,.1);flex:1;min-width:300px}.profile-card-header{align-items:center;background-color:#f0f0f1;border-bottom:1px solid #c3c4c7;display:flex;padding:15px}.profile-icon{margin-right:10px}.profile-icon .dashicons{font-size:20px;height:20px;width:20px}.profile-card-header h3{font-size:16px;font-weight:500;margin:0}.profile-card-content{padding:15px}.profile-field{margin-bottom:15px}.profile-field label{color:#646970;display:block;font-size:13px;font-weight:500;margin-bottom:5px}.profile-field input{background-color:#f6f7f7;border:1px solid #dcdcde;border-radius:3px;font-size:13px;padding:8px;width:100%}.profile-description{color:#646970;font-size:13px;line-height:1.5;margin:15px 0}.profile-card-content .button{margin-top:10px;text-align:center;width:100%}.activitypub-welcome-footer{margin-top:30px;text-align:center}.skip-steps-link{color:#2271b1;font-size:14px;text-decoration:none}.skip-steps-link:hover{color:#135e96;text-decoration:underline}@media screen and (max-width:782px){.activitypub-welcome-container{margin:20px;padding:20px}.step-content{align-items:flex-start;flex-direction:column}.step-action{margin-left:0;margin-top:15px;width:100%}.step-action .button{text-align:center;width:100%}.activitypub-profiles-container{flex-direction:column}.activitypub-profile-card{width:100%}} diff --git a/composer.json b/composer.json index b3aca80e3..dbfc7eb99 100644 --- a/composer.json +++ b/composer.json @@ -1,66 +1,66 @@ { - "name": "pfefferle/wordpress-activitypub", - "description": "The ActivityPub protocol is a decentralized social networking protocol based upon the ActivityStreams 2.0 data format.", - "type": "wordpress-plugin", - "require": { - "php": ">=7.2", - "composer/installers": "^1.0 || ^2.0" - }, - "require-dev": { - "automattic/jetpack-changelogger": "6.0.0", - "phpunit/phpunit": "^8 || ^9", - "phpcompatibility/php-compatibility": "*", - "phpcompatibility/phpcompatibility-wp": "*", - "squizlabs/php_codesniffer": "3.*", - "wp-coding-standards/wpcs": "dev-develop", - "yoast/phpunit-polyfills": "^4.0", - "dealerdirect/phpcodesniffer-composer-installer": "^1.0.0", - "sirbrillig/phpcs-variable-analysis": "^2.11", - "phpcsstandards/phpcsextra": "^1.1.0", - "dms/phpunit-arraysubset-asserts": "^0.5.0" - }, - "config": { - "allow-plugins": true - }, - "allow-plugins": { - "composer/installers": true - }, - "license": "MIT", - "authors": [ - { - "name": "Matthias Pfefferle", - "email": "pfefferle@gmail.com" - } - ], - "extra": { - "installer-name": "activitypub", - "changelogger": { - "changes-dir": ".github/changelog/", - "link-template": "https://github.com/Automattic/wordpress-activitypub/compare/${old}...${new}" - } - }, - "scripts": { - "test": [ - "composer install", - "bin/install-wp-tests.sh activitypub-test root activitypub-test test-db latest true", - "vendor/bin/phpunit" - ], - "test:wp-env": [ - "wp-env run tests-cli --env-cwd=\"wp-content/plugins/activitypub\" vendor/bin/phpunit" - ], - "lint": [ - "vendor/bin/phpcs" - ], - "lint:fix": [ - "vendor/bin/phpcbf" - ], - "changelog:add": [ - "composer install", - "vendor/bin/changelogger add" - ], - "changelog:write": [ - "composer install", - "vendor/bin/changelogger write --add-pr-num" - ] - } + "name": "pfefferle/wordpress-activitypub", + "description": "The ActivityPub protocol is a decentralized social networking protocol based upon the ActivityStreams 2.0 data format.", + "type": "wordpress-plugin", + "require": { + "php": ">=7.2", + "composer/installers": "^1.0 || ^2.0" + }, + "require-dev": { + "automattic/jetpack-changelogger": "6.0.0", + "phpunit/phpunit": "^8 || ^9", + "phpcompatibility/php-compatibility": "*", + "phpcompatibility/phpcompatibility-wp": "*", + "squizlabs/php_codesniffer": "3.*", + "wp-coding-standards/wpcs": "dev-develop", + "yoast/phpunit-polyfills": "^4.0", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0.0", + "sirbrillig/phpcs-variable-analysis": "^2.11", + "phpcsstandards/phpcsextra": "^1.1.0", + "dms/phpunit-arraysubset-asserts": "^0.5.0" + }, + "config": { + "allow-plugins": true + }, + "allow-plugins": { + "composer/installers": true + }, + "license": "MIT", + "authors": [ + { + "name": "Matthias Pfefferle", + "email": "pfefferle@gmail.com" + } + ], + "extra": { + "installer-name": "activitypub", + "changelogger": { + "changes-dir": ".github/changelog/", + "link-template": "https://github.com/Automattic/wordpress-activitypub/compare/${old}...${new}" + } + }, + "scripts": { + "test": [ + "composer install", + "bin/install-wp-tests.sh activitypub-test root activitypub-test test-db latest true", + "vendor/bin/phpunit" + ], + "test:wp-env": [ + "wp-env run tests-cli --env-cwd=\"wp-content/plugins/activitypub\" vendor/bin/phpunit" + ], + "lint": [ + "vendor/bin/phpcs" + ], + "lint:fix": [ + "vendor/bin/phpcbf" + ], + "changelog:add": [ + "composer install", + "vendor/bin/changelogger add" + ], + "changelog:write": [ + "composer install", + "vendor/bin/changelogger write --add-pr-num" + ] + } } diff --git a/docs/developer-docs.md b/docs/developer-docs.md index 76543bb7f..767e84fec 100644 --- a/docs/developer-docs.md +++ b/docs/developer-docs.md @@ -3,6 +3,12 @@ ## Table of Contents - [Introduction](#introduction) - [Extending the Settings Interface](#extending-the-settings-interface) +- [JavaScript and CSS Development](#javascript-and-css-development) + - [Block Development](#block-development) + - [Feature-Based Asset Organization](#feature-based-asset-organization) +- [Development Workflow](#development-workflow) + - [Available Commands](#available-commands) + - [Build Process](#build-process) ## Introduction This documentation provides information for developers who want to extend and build upon the ActivityPub plugin. Whether you're developing a complementary plugin or integrating ActivityPub features into your existing WordPress plugin, this guide will help you understand the available hooks and customization options. @@ -59,3 +65,170 @@ add_action( 'admin_enqueue_scripts', function( $hook ) { } } ); ``` + +## JavaScript and CSS Development + +### Block Development + +The ActivityPub plugin uses the WordPress Block Editor (Gutenberg) architecture for developing custom blocks. All block-related code is located in the `/src/blocks` directory. + +#### Block Structure + +Each block typically follows this directory structure: + +``` +/src/blocks/block-name/ +├── block.json # Block configuration. +├── edit.js # Edit component. +├── index.js # Block registration. +├── style.scss # Block styles. +└── view.js # Frontend JavaScript (optional). +``` + +#### Best Practices for Block Development + +1. **Follow WordPress Coding Standards**: Adhere to WordPress JavaScript and CSS coding standards. +2. **Use WordPress Components**: Leverage existing WordPress components from `@wordpress/components`. +3. **Internationalization**: Make all user-facing strings translatable using the `__()` function. +4. **Accessibility**: Ensure your blocks are accessible to all users. +5. **Indentation**: Use tabs for indentation in SCSS files, not spaces. + +### Feature-Based Asset Organization + +The ActivityPub plugin organizes scripts and styles by feature rather than by file type. Each feature has its own directory containing all related assets. + +#### Structure + +``` +/src/ +├── blocks/ # Block-specific code and styles +│ ├── reply/ # Reply block +│ └── ... # Other blocks +├── wp-admin/ # Admin-related features +│ ├── admin.js # Admin JavaScript +│ ├── admin.scss # Admin styles +│ └── ... # Other admin features +├── feature-name/ # Any other feature +│ ├── script.js # Feature JavaScript +│ ├── style.scss # Feature styles +│ └── ... # Other feature files +└── ... # Other feature directories + +/build/ # Compiled assets, organized by feature +``` + +#### Adding New Feature Assets + +1. Create a new directory for your feature in the `/src/` directory: + ``` + /src/your-feature/ + ``` + +2. Add your JavaScript and/or SCSS files to this directory: + ``` + /src/your-feature/script.js + /src/your-feature/style.scss + ``` + +3. When you run `npm run build`, WordPress Scripts will: + - Compile your JavaScript file to `/build/your-feature/script.js`. + - Compile and minify your SCSS to `/build/your-feature/style.css`. + - Generate source maps. + +4. Enqueue your script and/or stylesheet in PHP, using the generated asset file: + +```php +/** + * Enqueue admin scripts. + */ +function activitypub_enqueue_admin_scripts() { + // Load the asset file to get dependencies and version. + $asset_data = include ACTIVITYPUB_PLUGIN_DIR . 'build/your-feature/script.asset.php'; + + wp_enqueue_script( + 'activitypub-your-feature', + plugins_url( + 'build/your-feature/script.js', + ACTIVITYPUB_PLUGIN_FILE + ), + $asset_data['dependencies'], + $asset_data['version'], + true + ); +} +add_action( 'admin_enqueue_scripts', 'activitypub_enqueue_admin_scripts' ); + +/** + * Enqueue admin styles. + */ +function activitypub_enqueue_admin_styles() { + wp_enqueue_style( + 'activitypub-your-feature', + plugins_url( + 'build/your-feature/style.css', + ACTIVITYPUB_PLUGIN_FILE + ), + array(), + ACTIVITYPUB_PLUGIN_VERSION + ); + + // Add RTL support. + wp_style_add_data( 'activitypub-your-feature', 'rtl', 'replace' ); +} +add_action( 'admin_enqueue_scripts', 'activitypub_enqueue_admin_styles' ); +``` + +## Development Workflow + +The ActivityPub plugin uses WordPress Scripts (`@wordpress/scripts`) for development, building, and linting JavaScript and CSS files. + +### Available Commands + +The following npm scripts are available in `package.json`: + +- `npm run dev`: Start the development server with hot reloading. +- `npm run build`: Format code and build production assets. +- `npm run format`: Format JavaScript files using Prettier. Also part of the build process. +- `npm run lint:css`: Lint CSS/SCSS files. +- `npm run lint:js`: Lint JavaScript files. +- `npm run env`: Run WordPress environment commands. +- `npm run env-start`: Start the WordPress development environment. +- `npm run env-stop`: Stop the WordPress development environment. +- `npm run env-test`: Run PHPUnit tests in the WordPress environment. +- `npm run release`: Create a new release. + +### Build Process + +#### Development + +During development, use the following workflow: + +1. Start the development server: + ``` + npm run dev + ``` + This will watch for changes in your JavaScript and SCSS files and automatically rebuild them. + +2. Make your changes to the source files in `/src` or `/assets`. + +3. Test your changes in the browser. + +#### Production + +Before committing and pushing your changes to the remote repository: + +1. Build the production assets: + ``` + npm run build + ``` + This will: + - Format your JavaScript files. + - Compile and minify JavaScript. + - Compile and minify SCSS to CSS. + - Generate source maps. + +2. Commit both your source files and the built assets. + +3. Push your changes to the remote repository. + +> **Important**: Always run `npm run build` before pushing your changes to ensure that the built assets are up-to-date with your source code. diff --git a/docs/how-to/readme.md b/docs/how-to/readme.md new file mode 100644 index 000000000..004387c51 --- /dev/null +++ b/docs/how-to/readme.md @@ -0,0 +1,3 @@ +# How-To + +This folder contains How-To guides for common problems or edge cases. If you miss a guide, please [open an issue](https://github.com/Automattic/wordpress-activitypub/issues/new/choose) or [submit a pull request](https://github.com/Automattic/wordpress-activitypub/pulls). diff --git a/docs/how-to/reverse-proxy.md b/docs/how-to/reverse-proxy.md new file mode 100644 index 000000000..c97036e68 --- /dev/null +++ b/docs/how-to/reverse-proxy.md @@ -0,0 +1,17 @@ +# Handling reverse proxy setups with Apache + +If you are using a reverse proxy with Apache to serve your site, you may find that followers are unable to follow your blog. This happens because the proxy rewrites the `Host` header to your server’s internal DNS name, which the plugin then uses to sign replies. However, remote servers expect replies to be signed with your public DNS name. To resolve this, you need to use the `ProxyPreserveHost On` directive to ensure that the external host name is passed through to the backend server. + +If you are using SSL between the reverse proxy and the internal host, you may also need to set `SSLProxyCheckPeerName off` if the internal host does not present the correct SSL certificate. Be aware that this can introduce a security risk in some environments. + +## Example + +```apache + + ServerName example.com + ProxyPreserveHost On + SSLProxyCheckPeerName off + ProxyPass / http://localhost:8080/ + ProxyPassReverse / http://localhost:8080/ + +``` diff --git a/docs/how-to/wordpress-in-a-subdir.md b/docs/how-to/wordpress-in-a-subdir.md new file mode 100644 index 000000000..567f674c8 --- /dev/null +++ b/docs/how-to/wordpress-in-a-subdir.md @@ -0,0 +1,24 @@ +# Using a subdirectory for your blog + +For WebFinger to function properly, it needs to be mapped to the root directory of your blog’s URL. + +## Apache + +Add the following lines to the `.htaccess` file located in your site's root directory: + + RedirectMatch "^\/\.well-known/(webfinger|nodeinfo)(.*)$" /blog/.well-known/$1$2 + +…where `blog` is the path to the subdirectory where your blog is installed. + +## Nginx + +Add the following lines to your `site.conf` file in the `sites-available` directory: + + location ~* /.well-known { + allow all; + try_files $uri $uri/ /blog/?$args; + } + +Where `blog` is the path to the subdirectory where your blog is installed. + +If your blog is installed in a subdirectory but you’ve set a different [wp_siteurl](https://wordpress.org/documentation/article/giving-wordpress-its-own-directory/), you don’t need the redirect — index.php will handle it automatically. diff --git a/docs/readme.md b/docs/readme.md index 261bbefd3..2319460b1 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -1 +1,8 @@ # The ActivityPub plugin documentation! + +In this directory you will find the documentation for the ActivityPub plugin. + +## How-To + +If you need help, check out the [How-To section](./how-to) or use [the support forums on WordPress.org](https://wordpress.org/support/plugin/activitypub/). + diff --git a/includes/activity/class-actor.php b/includes/activity/class-actor.php index 7796a6a0f..4724888af 100644 --- a/includes/activity/class-actor.php +++ b/includes/activity/class-actor.php @@ -23,6 +23,7 @@ class Actor extends Base_Object { 'https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1', 'https://purl.archive.org/socialweb/webfinger', + 'https://w3id.org/fep/844e', array( 'schema' => 'http://schema.org#', 'toot' => 'http://joinmastodon.org/ns#', @@ -221,4 +222,90 @@ class Actor extends Base_Object { * @var array */ protected $also_known_as; + + /** + * The Featured-Posts. + * + * @see https://docs.joinmastodon.org/spec/activitypub/#featured + * + * @context { + * "@id": "http://joinmastodon.org/ns#featured", + * "@type": "@id" + * } + * + * @var string + */ + protected $featured; + + /** + * The Featured-Tags. + * + * @see https://docs.joinmastodon.org/spec/activitypub/#featuredTags + * + * @context { + * "@id": "http://joinmastodon.org/ns#featuredTags", + * "@type": "@id" + * } + * + * @var string + */ + protected $featured_tags; + + /** + * Whether the User is discoverable. + * + * @see https://docs.joinmastodon.org/spec/activitypub/#discoverable + * + * @context http://joinmastodon.org/ns#discoverable + * + * @var boolean + */ + protected $discoverable; + + /** + * Whether the User is indexable. + * + * @see https://docs.joinmastodon.org/spec/activitypub/#indexable + * + * @context http://joinmastodon.org/ns#indexable + * + * @var boolean + */ + protected $indexable; + + /** + * The WebFinger Resource. + * + * @see https://codeberg.org/fediverse/fep/src/branch/main/fep/2c59/fep-2c59.md + * + * @var string + */ + protected $webfinger; + + /** + * URL to the Moderators endpoint. + * + * @see https://join-lemmy.org/docs/contributors/05-federation.html + * + * @var string + */ + protected $moderators; + + /** + * Restrict posting to mods. + * + * @see https://join-lemmy.org/docs/contributors/05-federation.html + * + * @var boolean + */ + protected $posting_restricted_to_mods; + + /** + * Listing Implemented Specifications on the Application Actor + * + * @see https://codeberg.org/helge/fep/src/commit/e1b2a16707b542ea5ea0cfb390ac1abce89f05bb/fep/aaa3/fep-aaa3.md + * + * @var array + */ + protected $implemented; } diff --git a/includes/activity/class-base-object.php b/includes/activity/class-base-object.php index 508d24da6..8a96ab964 100644 --- a/includes/activity/class-base-object.php +++ b/includes/activity/class-base-object.php @@ -32,6 +32,7 @@ class Base_Object extends Generic_Object { array( 'Hashtag' => 'as:Hashtag', 'sensitive' => 'as:sensitive', + 'dcterms' => 'http://purl.org/dc/terms/', ), ); @@ -416,6 +417,16 @@ class Base_Object extends Generic_Object { */ protected $sensitive; + /** + * The dcterms namespace. + * + * @see https://codeberg.org/fediverse/fep/src/branch/main/fep/b2b8/fep-b2b8.md#sensitive + * @see https://www.dublincore.org/specifications/dublin-core/dcmi-terms/ + * + * @var array + */ + protected $dcterms; + /** * Generic getter. * diff --git a/includes/activity/class-generic-object.php b/includes/activity/class-generic-object.php index 3dddcea0e..bdb3c6698 100644 --- a/includes/activity/class-generic-object.php +++ b/includes/activity/class-generic-object.php @@ -19,25 +19,28 @@ * * @since 5.3.0 * - * @method string|null get_actor() Gets one or more entities that performed or are expected to perform the activity. - * @method string[]|null get_also_known_as() Gets the also known as property of the object. - * @method string|null get_attributed_to() Gets the entity attributed as the original author. - * @method array[]|null get_attachment() Gets the attachment property of the object. - * @method string[]|null get_cc() Gets the secondary recipients of the object. - * @method string|null get_content() Gets the content property of the object. - * @method string[]|null get_icon() Gets the icon property of the object. - * @method string|null get_id() Gets the object's unique global identifier. - * @method string[]|null get_image() Gets the image property of the object. - * @method string[]|string|null get_in_reply_to() Gets the objects this object is in reply to. - * @method string|null get_name() Gets the natural language name of the object. - * @method Base_Object|string|null get_object() Gets the direct object of the activity. - * @method string|null get_published() Gets the date and time the object was published in ISO 8601 format. - * @method string|null get_summary() Gets the natural language summary of the object. - * @method array[]|null get_tag() Gets the tag property of the object. - * @method string[]|string|null get_to() Gets the primary recipients of the object. - * @method string get_type() Gets the type of the object. - * @method string|null get_updated() Gets the date and time the object was updated in ISO 8601 format. - * @method string|null get_url() Gets the URL of the object. + * @method string|null get_actor() Gets one or more entities that performed or are expected to perform the activity. + * @method string[]|null get_also_known_as() Gets the also known as property of the object. + * @method string|null get_attributed_to() Gets the entity attributed as the original author. + * @method array[]|null get_attachment() Gets the attachment property of the object. + * @method string[]|null get_cc() Gets the secondary recipients of the object. + * @method string|null get_content() Gets the content property of the object. + * @method string[]|null get_endpoints() Gets the endpoint property of the object. + * @method string[]|null get_icon() Gets the icon property of the object. + * @method string|null get_id() Gets the object's unique global identifier. + * @method string[]|null get_image() Gets the image property of the object. + * @method string[]|string|null get_in_reply_to() Gets the objects this object is in reply to. + * @method string|null get_inbox() Gets the inbox property of the object. + * @method string|null get_name() Gets the natural language name of the object. + * @method Base_Object|string|null get_object() Gets the direct object of the activity. + * @method string|null get_preferred_username() Gets the preferred username of the object. + * @method string|null get_published() Gets the date and time the object was published in ISO 8601 format. + * @method string|null get_summary() Gets the natural language summary of the object. + * @method array[]|null get_tag() Gets the tag property of the object. + * @method string[]|string|null get_to() Gets the primary recipients of the object. + * @method string get_type() Gets the type of the object. + * @method string|null get_updated() Gets the date and time the object was updated in ISO 8601 format. + * @method string|null get_url() Gets the URL of the object. * * @method string|string[] add_cc( string|array $cc ) Adds one or more entities to the secondary audience of the object. * @method string|string[] add_to( string|array $to ) Adds one or more entities to the primary audience of the object. @@ -294,8 +297,11 @@ public function to_array( $include_json_ld_context = true ) { $value = $value->to_array( false ); } - // If value is still empty, ignore it for the array and continue. - if ( isset( $value ) ) { + if ( is_array( $value ) && $this->is_namespaced( $key ) ) { + foreach ( $value as $sub_key => $sub_value ) { + $array[ snake_to_camel_case( $key ) . ':' . snake_to_camel_case( $sub_key ) ] = $sub_value; + } + } elseif ( isset( $value ) ) { $array[ snake_to_camel_case( $key ) ] = $value; } } @@ -370,4 +376,23 @@ public function get_object_var_keys() { public function get_json_ld_context() { return static::JSON_LD_CONTEXT; } + + /** + * Checks if an attribute is in a namespace. + * + * @param string $attribute The attribute to check. + * + * @return bool Whether the attribute is namespaced. + */ + private function is_namespaced( $attribute ) { + $namespaces = array(); + + foreach ( static::JSON_LD_CONTEXT as $context ) { + if ( is_array( $context ) ) { + $namespaces = \array_merge( $namespaces, $context ); + } + } + + return isset( $namespaces[ $attribute ] ) && \wp_http_validate_url( $namespaces[ $attribute ] ); + } } diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 9d0343948..20367363e 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -48,8 +48,9 @@ public static function init() { \add_filter( 'activitypub_get_actor_extra_fields', array( Extra_Fields::class, 'default_actor_extra_fields' ), 10, 2 ); - \add_action( 'updated_postmeta', array( self::class, 'updated_postmeta' ), 10, 4 ); - \add_action( 'added_post_meta', array( self::class, 'updated_postmeta' ), 10, 4 ); + \add_filter( 'add_post_metadata', array( self::class, 'prevent_empty_post_meta' ), 10, 4 ); + \add_filter( 'update_post_metadata', array( self::class, 'prevent_empty_post_meta' ), 10, 4 ); + \add_filter( 'default_post_metadata', array( self::class, 'default_post_metadata' ), 10, 3 ); \add_action( 'init', array( self::class, 'register_user_meta' ), 11 ); @@ -309,6 +310,7 @@ public static function add_query_vars( $vars ) { $vars[] = 'preview'; $vars[] = 'author'; $vars[] = 'actor'; + $vars[] = 'type'; $vars[] = 'c'; $vars[] = 'p'; @@ -332,18 +334,14 @@ public static function pre_get_avatar_data( $args, $id_or_email ) { return $args; } + /** + * Filter allowed comment types for avatars. + * + * @param array $allowed_comment_types Array of allowed comment types. + */ $allowed_comment_types = \apply_filters( 'get_avatar_comment_types', array( 'comment' ) ); - if ( - ! empty( $id_or_email->comment_type ) && - ! \in_array( - $id_or_email->comment_type, - (array) $allowed_comment_types, - true - ) - ) { - $args['url'] = false; - /** This filter is documented in wp-includes/link-template.php */ - return \apply_filters( 'get_avatar_data', $args, $id_or_email ); + if ( ! \in_array( $id_or_email->comment_type ?: 'comment', $allowed_comment_types, true ) ) { // phpcs:ignore Universal.Operators.DisallowShortTernary + return $args; } // Check if comment has an avatar. @@ -356,8 +354,12 @@ public static function pre_get_avatar_data( $args, $id_or_email ) { $args['class'] = \explode( ' ', $args['class'] ); } - $args['url'] = $avatar; + /** This filter is documented in wp-includes/link-template.php */ + $args['url'] = \apply_filters( 'get_avatar_url', $avatar, $id_or_email, $args ); + $args['class'][] = 'avatar'; $args['class'][] = 'avatar-activitypub'; + $args['class'][] = 'avatar-' . (int) $args['size']; + $args['class'][] = 'photo'; $args['class'][] = 'u-photo'; $args['class'] = \array_unique( $args['class'] ); } @@ -455,7 +457,7 @@ public static function theme_compat() { */ private static function register_post_types() { \register_post_type( - Followers::POST_TYPE, + Actors::POST_TYPE, array( 'labels' => array( 'name' => _x( 'Followers', 'post_type plural name', 'activitypub' ), @@ -472,7 +474,7 @@ private static function register_post_types() { ); \register_post_meta( - Followers::POST_TYPE, + Actors::POST_TYPE, '_activitypub_inbox', array( 'type' => 'string', @@ -482,7 +484,7 @@ private static function register_post_types() { ); \register_post_meta( - Followers::POST_TYPE, + Actors::POST_TYPE, '_activitypub_errors', array( 'type' => 'string', @@ -498,8 +500,8 @@ private static function register_post_types() { ); \register_post_meta( - Followers::POST_TYPE, - '_activitypub_user_id', + Actors::POST_TYPE, + Followers::FOLLOWER_META_KEY, array( 'type' => 'string', 'single' => false, @@ -509,18 +511,6 @@ private static function register_post_types() { ) ); - \register_post_meta( - Followers::POST_TYPE, - '_activitypub_actor_json', - array( - 'type' => 'string', - 'single' => true, - 'sanitize_callback' => function ( $value ) { - return sanitize_text_field( $value ); - }, - ) - ); - // Register Outbox Post-Type. register_post_type( Outbox::POST_TYPE, @@ -692,18 +682,58 @@ public static function user_register( $user_id ) { } /** - * Delete `activitypub_content_visibility` when updated to an empty value. + * Prevent empty or default meta values. * - * @param int $meta_id ID of updated metadata entry. - * @param int $object_id Post ID. + * @param null|bool $check Whether to allow updating metadata for the given type. + * @param int $object_id ID of the object metadata is for. + * @param string $meta_key Metadata key. + * @param mixed $meta_value Metadata value. Must be serializable if non-scalar. + */ + public static function prevent_empty_post_meta( $check, $object_id, $meta_key, $meta_value ) { + $post_metas = array( + 'activitypub_content_visibility' => '', + 'activitypub_content_warning' => '', + 'activitypub_max_image_attachments' => (string) \get_option( 'activitypub_max_image_attachments', ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS ), + ); + + if ( isset( $post_metas[ $meta_key ] ) && $post_metas[ $meta_key ] === (string) $meta_value ) { + if ( 'update_post_metadata' === current_action() ) { + \delete_post_meta( $object_id, $meta_key ); + } + + $check = true; + } + + return $check; + } + + /** + * Adjusts default post meta values. + * + * @param mixed $meta_value The meta value. + * @param int $object_id ID of the object metadata is for. * @param string $meta_key Metadata key. - * @param mixed $meta_value Metadata value. This will be a PHP-serialized string representation of the value - * if the value is an array, an object, or itself a PHP-serialized string. + * + * @return mixed The meta value. */ - public static function updated_postmeta( $meta_id, $object_id, $meta_key, $meta_value ) { - if ( 'activitypub_content_visibility' === $meta_key && empty( $meta_value ) ) { - \delete_post_meta( $object_id, 'activitypub_content_visibility' ); + public static function default_post_metadata( $meta_value, $object_id, $meta_key ) { + // Check if the meta key is `activitypub_content_visibility`. + if ( 'activitypub_content_visibility' !== $meta_key ) { + return $meta_value; + } + + // If the post is federated, return the default visibility. + if ( 'federated' === \get_post_meta( $object_id, 'activitypub_status', true ) ) { + return $meta_value; + } + + // If the post is not federated and older than a year, return local visibility. + $date = \get_the_date( 'U', $object_id ); + if ( $date < \strtotime( '-1 month' ) ) { + return ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL; } + + return $meta_value; } /** @@ -732,7 +762,7 @@ public static function register_user_meta() { 'description' => 'An array of URLs that the user is known by.', 'single' => true, 'default' => array(), - 'sanitize_callback' => array( Sanitize::class, 'url_list' ), + 'sanitize_callback' => array( Sanitize::class, 'identifier_list' ), ) ); diff --git a/includes/class-blocks.php b/includes/class-blocks.php index 83060b8e0..7c01bbe1f 100644 --- a/includes/class-blocks.php +++ b/includes/class-blocks.php @@ -8,7 +8,6 @@ namespace Activitypub; use Activitypub\Collection\Actors; -use Activitypub\Collection\Followers; /** * Block class. @@ -21,12 +20,11 @@ public static function init() { // This is already being called on the init hook, so just add it. self::register_blocks(); - \add_action( 'wp_head', array( self::class, 'inject_activitypub_options' ), 11 ); - \add_action( 'admin_print_scripts', array( self::class, 'inject_activitypub_options' ) ); \add_action( 'load-post-new.php', array( self::class, 'handle_in_reply_to_get_param' ) ); // Add editor plugin. \add_action( 'enqueue_block_editor_assets', array( self::class, 'enqueue_editor_assets' ) ); \add_action( 'init', array( self::class, 'register_postmeta' ), 11 ); + \add_action( 'rest_api_init', array( self::class, 'register_rest_fields' ) ); \add_filter( 'activitypub_import_mastodon_post_data', array( self::class, 'filter_import_mastodon_post_data' ), 10, 2 ); } @@ -44,13 +42,7 @@ public static function register_postmeta() { 'show_in_rest' => true, 'single' => true, 'type' => 'string', - 'sanitize_callback' => function ( $warning ) { - if ( $warning ) { - return \sanitize_text_field( $warning ); - } - - return null; - }, + 'sanitize_callback' => 'sanitize_text_field', ) ); @@ -95,14 +87,25 @@ public static function register_postmeta() { * Enqueue the block editor assets. */ public static function enqueue_editor_assets() { + $data = array( + 'namespace' => ACTIVITYPUB_REST_NAMESPACE, + 'defaultAvatarUrl' => ACTIVITYPUB_PLUGIN_URL . 'assets/img/mp.jpg', + 'enabled' => array( + 'blog' => ! is_user_type_disabled( 'blog' ), + 'users' => ! is_user_type_disabled( 'user' ), + ), + ); + wp_localize_script( 'wp-editor', '_activityPubOptions', $data ); + // Check for our supported post types. $current_screen = \get_current_screen(); $ap_post_types = \get_post_types_by_support( 'activitypub' ); if ( ! $current_screen || ! in_array( $current_screen->post_type, $ap_post_types, true ) ) { return; } - $asset_data = include ACTIVITYPUB_PLUGIN_DIR . 'build/editor-plugin/plugin.asset.php'; - $plugin_url = plugins_url( 'build/editor-plugin/plugin.js', ACTIVITYPUB_PLUGIN_FILE ); + + $asset_data = include ACTIVITYPUB_PLUGIN_DIR . 'build/blocks/editor-plugin/plugin.asset.php'; + $plugin_url = plugins_url( 'build/blocks/editor-plugin/plugin.js', ACTIVITYPUB_PLUGIN_FILE ); wp_enqueue_script( 'activitypub-block-editor', $plugin_url, $asset_data['dependencies'], $asset_data['version'], true ); } @@ -116,100 +119,69 @@ public static function handle_in_reply_to_get_param() { return; } - $asset_data = include ACTIVITYPUB_PLUGIN_DIR . 'build/reply-intent/plugin.asset.php'; - $plugin_url = plugins_url( 'build/reply-intent/plugin.js', ACTIVITYPUB_PLUGIN_FILE ); + $asset_data = include ACTIVITYPUB_PLUGIN_DIR . 'build/blocks/reply-intent/plugin.asset.php'; + $plugin_url = plugins_url( 'build/blocks/reply-intent/plugin.js', ACTIVITYPUB_PLUGIN_FILE ); wp_enqueue_script( 'activitypub-reply-intent', $plugin_url, $asset_data['dependencies'], $asset_data['version'], true ); } - /** - * Output ActivityPub options as a script tag. - */ - public static function inject_activitypub_options() { - $data = array( - 'namespace' => ACTIVITYPUB_REST_NAMESPACE, - 'defaultAvatarUrl' => ACTIVITYPUB_PLUGIN_URL . 'assets/img/mp.jpg', - 'enabled' => array( - 'site' => ! is_user_type_disabled( 'blog' ), - 'users' => ! is_user_type_disabled( 'user' ), - ), - 'maxImageAttachments' => \get_option( 'activitypub_max_image_attachments', ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS ), - ); - - printf( - "\n", - wp_json_encode( $data ) - ); - } - /** * Register the blocks. */ public static function register_blocks() { - \register_block_type_from_metadata( - ACTIVITYPUB_PLUGIN_DIR . '/build/followers', - array( - 'render_callback' => array( self::class, 'render_follower_block' ), - ) - ); - \register_block_type_from_metadata( - ACTIVITYPUB_PLUGIN_DIR . '/build/follow-me', - array( - 'render_callback' => array( self::class, 'render_follow_me_block' ), - ) - ); - \register_block_type_from_metadata( - ACTIVITYPUB_PLUGIN_DIR . '/build/reply', - array( - 'render_callback' => array( self::class, 'render_reply_block' ), - ) - ); + \register_block_type_from_metadata( ACTIVITYPUB_PLUGIN_DIR . '/build/blocks/follow-me' ); + \register_block_type_from_metadata( ACTIVITYPUB_PLUGIN_DIR . '/build/blocks/followers' ); + \register_block_type_from_metadata( ACTIVITYPUB_PLUGIN_DIR . '/build/blocks/reactions' ); \register_block_type_from_metadata( - ACTIVITYPUB_PLUGIN_DIR . '/build/reactions', + ACTIVITYPUB_PLUGIN_DIR . '/build/blocks/reply', array( - 'render_callback' => array( self::class, 'render_post_reactions_block' ), + 'render_callback' => array( self::class, 'render_reply_block' ), ) ); } /** - * Render the post reactions block. - * - * @param array $attrs The block attributes. - * @param string $content Inner blocks. - * - * @return string The HTML to render. + * Register REST fields needed for blocks. */ - public static function render_post_reactions_block( $attrs, $content ) { - if ( ! isset( $attrs['postId'] ) ) { - $attrs['postId'] = get_the_ID(); - } - - $args = array( 'data-attrs' => wp_json_encode( $attrs ) ); - if ( isset( $attrs['title'] ) ) { - $args['class'] = 'activitypub-reactions-block'; - } - - return sprintf( - '
%2$s
', - get_block_wrapper_attributes( $args ), - $content + public static function register_rest_fields() { + // Register the post_count field for Follow Me block. + register_rest_field( + 'user', + 'post_count', + array( + /** + * Get the number of published posts. + * + * @param array $response Prepared response array. + * @param string $field_name The field name. + * @param \WP_REST_Request $request The request object. + * @return int The number of published posts. + */ + 'get_callback' => function ( $response, $field_name, $request ) { + return (int) count_user_posts( $request->get_param( 'id' ), 'post', true ); + }, + 'schema' => array( + 'description' => 'Number of published posts', + 'type' => 'integer', + 'context' => array( 'activitypub' ), + ), + ) ); } /** * Get the user ID from a user string. * - * @param string $user_string The user string. Can be a user ID, 'site', or 'inherit'. + * @param string $user_string The user string. Can be a user ID, 'blog', or 'inherit'. * @return int|null The user ID, or null if the 'inherit' string is not supported in this context. */ - private static function get_user_id( $user_string ) { + public static function get_user_id( $user_string ) { if ( is_numeric( $user_string ) ) { return absint( $user_string ); } - // If the user string is 'site', return the Blog User ID. - if ( 'site' === $user_string ) { + // If the user string is 'blog', return the Blog User ID. + if ( 'blog' === $user_string ) { return Actors::BLOG_USER_ID; } @@ -249,103 +221,6 @@ private static function get_user_id( $user_string ) { return null; } - /** - * Filter an array by a list of keys. - * - * @param array $data The array to filter. - * @param array $keys The keys to keep. - * @return array The filtered array. - */ - protected static function filter_array_by_keys( $data, $keys ) { - return array_intersect_key( $data, array_flip( $keys ) ); - } - - /** - * Render the follow me block. - * - * @param array $attrs The block attributes. - * @return string The HTML to render. - */ - public static function render_follow_me_block( $attrs ) { - $user_id = self::get_user_id( $attrs['selectedUser'] ); - $user = Actors::get_by_id( $user_id ); - if ( is_wp_error( $user ) ) { - if ( 'inherit' === $attrs['selectedUser'] ) { - // If the user is 'inherit' and we couldn't determine the user, don't render anything. - return ''; - } else { - // If the user is a specific ID and we couldn't find it, render an error message. - return ''; - } - } - - $attrs['profileData'] = self::filter_array_by_keys( - $user->to_array(), - array( 'icon', 'name', 'webfinger' ) - ); - - $wrapper_attributes = get_block_wrapper_attributes( - array( - 'class' => 'activitypub-follow-me-block-wrapper', - 'data-attrs' => wp_json_encode( $attrs ), - ) - ); - // todo: render more than an empty div? - return '
'; - } - - /** - * Render the follower block. - * - * @param array $attrs The block attributes. - * - * @return string The HTML to render. - */ - public static function render_follower_block( $attrs ) { - $followee_user_id = self::get_user_id( $attrs['selectedUser'] ); - if ( is_null( $followee_user_id ) ) { - return ''; - } - - $user = Actors::get_by_id( $followee_user_id ); - if ( is_wp_error( $user ) ) { - return ''; - } - - $per_page = absint( $attrs['per_page'] ); - $follower_data = Followers::get_followers_with_count( $followee_user_id, $per_page ); - - $attrs['followerData']['total'] = $follower_data['total']; - $attrs['followerData']['followers'] = array_map( - function ( $follower ) { - return self::filter_array_by_keys( - $follower->to_array(), - array( 'icon', 'name', 'preferredUsername', 'url' ) - ); - }, - $follower_data['followers'] - ); - $wrapper_attributes = get_block_wrapper_attributes( - array( - 'aria-label' => __( 'Fediverse Followers', 'activitypub' ), - 'class' => 'activitypub-follower-block', - 'data-attrs' => wp_json_encode( $attrs ), - ) - ); - - $html = '
'; - if ( $attrs['title'] ) { - $html .= '

' . esc_html( $attrs['title'] ) . '

'; - } - $html .= '
    '; - foreach ( $follower_data['followers'] as $follower ) { - $html .= '
  • ' . self::render_follower( $follower ) . '
  • '; - } - // We are only pagination on the JS side. Could be revisited but we gotta ship! - $html .= '
'; - return $html; - } - /** * Render the reply block. * @@ -396,36 +271,50 @@ public static function render_reply_block( $attrs ) { } /** - * Render a follower. - * - * @param \Activitypub\Model\Follower $follower The follower to render. + * Renders a modal component that can be used by different blocks. * - * @return string The HTML to render. + * @param array $args Arguments for the modal. */ - public static function render_follower( $follower ) { - $external_svg = ''; - $template = - ' - - - %s - / - @%s - - %s - '; - - $data = $follower->to_array(); - - return sprintf( - $template, - esc_url( object_to_uri( $data['url'] ) ), - esc_attr( $data['name'] ), - esc_attr( $data['icon']['url'] ), - esc_html( $data['name'] ), - esc_html( $data['preferredUsername'] ), - $external_svg + public static function render_modal( $args = array() ) { + $defaults = array( + 'content' => '', + 'is_compact' => false, + 'title' => '', ); + + $args = \wp_parse_args( $args, $defaults ); + ?> + + + next_tag( $selector ) ) { + foreach ( $attributes as $key => $value ) { + if ( 'class' === $key ) { + $tags->add_class( $value ); + continue; + } + + $tags->set_attribute( $key, $value ); + } + } + + return $tags->get_updated_html(); + } } diff --git a/includes/class-cli.php b/includes/class-cli.php index b5c9d224d..a9db67ca4 100644 --- a/includes/class-cli.php +++ b/includes/class-cli.php @@ -227,4 +227,32 @@ public function move( $args ) { \WP_CLI::success( 'Move Scheduled.' ); } } + + /** + * Follow a user. + * + * ## OPTIONS + * + * + * The remote user to follow. + * + * ## EXAMPLES + * + * $ wp activitypub follow https://example.com/@user + * $ wp --user=pfefferle activitypub follow https://example.com/@user + * + * @synopsis + * + * @param array $args The arguments. + */ + public function follow( $args ) { + $user_id = \get_current_user_id(); + $follow = follow( $args[0], $user_id ); + + if ( is_wp_error( $follow ) ) { + \WP_CLI::error( $follow->get_error_message() ); + } else { + \WP_CLI::success( 'Follow Scheduled.' ); + } + } } diff --git a/includes/class-comment.php b/includes/class-comment.php index 58268ed2c..469a7b9a2 100644 --- a/includes/class-comment.php +++ b/includes/class-comment.php @@ -23,10 +23,11 @@ class Comment { public static function init() { self::register_comment_types(); + \add_filter( 'map_meta_cap', array( self::class, 'map_meta_cap' ), 10, 4 ); \add_filter( 'comment_reply_link', array( self::class, 'comment_reply_link' ), 10, 3 ); \add_filter( 'comment_class', array( self::class, 'comment_class' ), 10, 3 ); + \add_filter( 'comment_feed_where', array( static::class, 'comment_feed_where' ) ); \add_filter( 'get_comment_link', array( self::class, 'remote_comment_link' ), 11, 2 ); - \add_action( 'wp_enqueue_scripts', array( self::class, 'enqueue_scripts' ) ); \add_action( 'pre_get_comments', array( static::class, 'comment_query' ) ); \add_filter( 'pre_comment_approved', array( static::class, 'pre_comment_approved' ), 10, 2 ); \add_filter( 'get_avatar_comment_types', array( static::class, 'get_avatar_comment_types' ), 99 ); @@ -35,6 +36,26 @@ public static function init() { \add_filter( 'pre_wp_update_comment_count_now', array( static::class, 'pre_wp_update_comment_count_now' ), 10, 3 ); } + /** + * Remove edit capabilities for comments received via ActivityPub. + * + * @param array $caps Array of capabilities. + * @param string $cap Capability name. + * @param int $user_id User ID. + * @param array $args Array of arguments. + * + * @return array Modified array of capabilities. + */ + public static function map_meta_cap( $caps, $cap, $user_id, $args ) { + if ( 'edit_comment' === $cap && self::was_received( $args[0] ) ) { + if ( ! \is_admin() || ( isset( $GLOBALS['current_screen'] ) && 'comment' === $GLOBALS['current_screen']->id ) ) { + $caps[] = 'do_not_allow'; + } + } + + return $caps; + } + /** * Filter the comment reply link. * @@ -56,24 +77,23 @@ public static function comment_reply_link( $link, $args, $comment ) { return $link; } - $attrs = array( + if ( ! \WP_Block_Type_Registry::get_instance()->is_registered( 'activitypub/remote-reply' ) ) { + \register_block_type_from_metadata( ACTIVITYPUB_PLUGIN_DIR . 'build/remote-reply' ); + } + + $attributes = array( 'selectedComment' => self::generate_id( $comment ), 'commentId' => $comment->comment_ID, ); - $div = sprintf( - '
', - esc_attr( wp_json_encode( $attrs ) ) - ); + $block = \do_blocks( \sprintf( '', \wp_json_encode( $attributes ) ) ); /** * Filters the HTML markup for the ActivityPub remote comment reply container. * - * @param string $div The HTML markup for the remote reply container. Default is a div - * with class 'activitypub-remote-reply' and data attributes for - * the selected comment ID and internal comment ID. + * @param string $block The HTML markup for the remote reply container. */ - return apply_filters( 'activitypub_comment_reply_link', $div ); + return \apply_filters( 'activitypub_comment_reply_link', $block ); } /** @@ -342,6 +362,38 @@ public static function comment_class( $classes, $css_class, $comment_id ) { return $classes; } + /** + * Makes the comment feed filterable by comment type. + * + * Also excludes ActivityPub comment types from the feed when no type is specified. + * + * @param string $where The `WHERE` clause for the comment feed query. + * + * @return string The modified `WHERE` clause. + */ + public static function comment_feed_where( $where ) { + global $wpdb; + + $comment_type = \get_query_var( 'type' ); + + if ( 'all' === $comment_type ) { + return $where; + } + + $comment_types = self::get_comment_type_slugs(); + + if ( \in_array( $comment_type, $comment_types, true ) ) { + $where .= $wpdb->prepare( ' AND comment_type = %s', $comment_type ); + } else { + $comment_types = \array_map( 'esc_sql', $comment_types ); + $placeholders = implode( ', ', array_fill( 0, count( $comment_types ), '%s' ) ); + // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, WordPress.DB.PreparedSQL.NotPrepared + $where .= $wpdb->prepare( sprintf( ' AND comment_type NOT IN (%s)', $placeholders ), ...$comment_types ); + } + + return $where; + } + /** * Gets the public comment id via the WordPress comments meta. * @@ -395,9 +447,12 @@ public static function remote_comment_link( $comment_link, $comment ) { return $comment_link; } - $public_comment_link = self::get_source_url( $comment->comment_ID ); + $remote_comment_link = null; + if ( 'comment' === $comment->comment_type ) { + $remote_comment_link = self::get_source_url( $comment->comment_ID ); + } - return $public_comment_link ?? $comment_link; + return $remote_comment_link ?? $comment_link; } @@ -452,60 +507,6 @@ private static function post_has_remote_comments( $post_id ) { return ! empty( $comments ); } - /** - * Enqueue scripts for remote comments - */ - public static function enqueue_scripts() { - if ( ! \is_singular() || \is_user_logged_in() ) { - // Only on single pages, only for logged-out users. - return; - } - - if ( ! \post_type_supports( \get_post_type(), 'activitypub' ) ) { - // Post type does not support ActivityPub. - return; - } - - if ( ! \comments_open() || ! \get_comments_number() ) { - // No comments, no need to load the script. - return; - } - - if ( ! self::post_has_remote_comments( \get_the_ID() ) ) { - // No remote comments, no need to load the script. - return; - } - - $handle = 'activitypub-remote-reply'; - $data = array( - 'namespace' => ACTIVITYPUB_REST_NAMESPACE, - 'defaultAvatarUrl' => ACTIVITYPUB_PLUGIN_URL . 'assets/img/mp.jpg', - ); - $js = sprintf( 'var _activityPubOptions = %s;', wp_json_encode( $data ) ); - $asset_file = ACTIVITYPUB_PLUGIN_DIR . 'build/remote-reply/index.asset.php'; - - if ( \file_exists( $asset_file ) ) { - $assets = require_once $asset_file; - - \wp_enqueue_script( - $handle, - \plugins_url( 'build/remote-reply/index.js', __DIR__ ), - $assets['dependencies'], - $assets['version'], - true - ); - \wp_add_inline_script( $handle, $js, 'before' ); - \wp_set_script_translations( $handle, 'activitypub' ); - - \wp_enqueue_style( - $handle, - \plugins_url( 'build/remote-reply/style-index.css', __DIR__ ), - array( 'wp-components' ), - $assets['version'] - ); - } - } - /** * Get the comment type by activity type. * @@ -563,19 +564,6 @@ public static function get_comment_type_slugs() { return array_keys( self::get_comment_types() ); } - /** - * Return the registered custom comment type slugs. - * - * @deprecated 4.5.0 Use get_comment_type_slugs instead. - * - * @return array The registered custom comment type slugs. - */ - public static function get_comment_type_names() { - _deprecated_function( __METHOD__, '4.5.0', 'get_comment_type_slugs' ); - - return self::get_comment_type_slugs(); - } - /** * Get the custom comment type. * @@ -742,6 +730,14 @@ public static function pre_comment_approved( $approved, $comment_data ) { return $approved; } + // Maybe auto-approve likes and reposts. + if ( + \in_array( $comment_data['comment_type'], self::get_comment_type_slugs(), true ) && + '1' === \get_option( 'activitypub_auto_approve_reactions' ) + ) { + return 1; + } + if ( '1' !== \get_option( 'comment_previously_approved' ) ) { return $approved; } diff --git a/includes/class-debug.php b/includes/class-debug.php index ce91edf70..28629813b 100644 --- a/includes/class-debug.php +++ b/includes/class-debug.php @@ -19,9 +19,12 @@ class Debug { public static function init() { if ( \WP_DEBUG && \WP_DEBUG_LOG ) { \add_action( 'activitypub_safe_remote_post_response', array( self::class, 'log_remote_post_responses' ), 10, 2 ); + \add_action( 'activitypub_inbox', array( self::class, 'log_inbox' ), 10, 3 ); \add_action( 'activitypub_rest_inbox_disallowed', array( self::class, 'log_inbox' ), 10, 3 ); + \add_action( 'activitypub_add_to_outbox_failed', array( self::class, 'log_outbox_error' ), 10, 4 ); + \add_action( 'activitypub_sent_to_inbox', array( self::class, 'log_sent_to_inbox' ), 10, 2 ); } } @@ -51,10 +54,25 @@ public static function log_inbox( $data, $user_id, $type ) { $actor = $data['actor'] ?? ''; $url = object_to_uri( $actor ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions - \error_log( "[INBOX] Request From: {$url} with Activity: " . \print_r( $data, true ) ); + \error_log( "[INBOX] Request from: {$url} with Activity: " . \print_r( $data, true ) ); } } + /** + * Log failed outbox requests. + * + * @param false|\WP_Error $error The error object or false. + * @param array $data The Activity array. + * @param string $type The type of the request. + * @param int $user_id The ID of the local blog user. + */ + public static function log_outbox_error( $error, $data, $type, $user_id ) { + $error_message = \is_wp_error( $error ) ? $error->get_error_message() : 'Unknown'; + + // phpcs:ignore WordPress.PHP.DevelopmentFunctions + \error_log( "[OUTBOX] Failed to add {$type}-Activity from: {$user_id} (Error: {$error_message}) with Activity: " . \print_r( $data, true ) ); + } + /** * Logs Follower notifications. * diff --git a/includes/class-dispatcher.php b/includes/class-dispatcher.php index cbd46add0..0c9b6e370 100644 --- a/includes/class-dispatcher.php +++ b/includes/class-dispatcher.php @@ -45,29 +45,6 @@ public static function init() { \add_filter( 'activitypub_additional_inboxes', array( self::class, 'add_inboxes_by_mentioned_actors' ), 10, 3 ); \add_filter( 'activitypub_additional_inboxes', array( self::class, 'add_inboxes_of_replied_urls' ), 10, 3 ); \add_filter( 'activitypub_additional_inboxes', array( self::class, 'add_inboxes_of_relays' ), 10, 3 ); - - // Fallback for `activitypub_send_to_inboxes` filter. - \add_filter( - 'activitypub_additional_inboxes', - function ( $inboxes, $actor_id, $activity ) { - /** - * Filters the list of interactees inboxes to send the Activity to. - * - * @param array $inboxes The list of inboxes to send to. - * @param int $actor_id The actor ID. - * @param Activity $activity The ActivityPub Activity. - * - * @deprecated 5.2.0 Use `activitypub_additional_inboxes` instead. - * @deprecated 5.4.0 Use `activitypub_additional_inboxes` instead. - */ - $inboxes = \apply_filters_deprecated( 'activitypub_send_to_inboxes', array( $inboxes, $actor_id, $activity ), '5.2.0', 'activitypub_additional_inboxes' ); - $inboxes = \apply_filters_deprecated( 'activitypub_interactees_inboxes', array( $inboxes, $actor_id, $activity ), '5.4.0', 'activitypub_additional_inboxes' ); - - return $inboxes; - }, - 10, - 3 - ); } /** @@ -364,23 +341,6 @@ public static function add_inboxes_of_replied_urls( $inboxes, $actor_id, $activi return $inboxes; } - /** - * Adds Blog Actor inboxes to Updates so the Blog User's followers are notified of edits. - * - * @deprecated 5.2.0 Use {@see Followers::maybe_add_inboxes_of_blog_user} instead. - * - * @param array $inboxes The list of Inboxes. - * @param int $actor_id The WordPress Actor-ID. - * @param Activity $activity The ActivityPub Activity. - * - * @return array The filtered Inboxes. - */ - public static function maybe_add_inboxes_of_blog_user( $inboxes, $actor_id, $activity ) { // phpcs:ignore - _deprecated_function( __METHOD__, '5.2.0', 'Followers::maybe_add_inboxes_of_blog_user' ); - - return $inboxes; - } - /** * Check if passed Activity is public. * diff --git a/includes/class-embed.php b/includes/class-embed.php index bb503a923..e7d4362f3 100644 --- a/includes/class-embed.php +++ b/includes/class-embed.php @@ -132,8 +132,8 @@ public static function get_html_for_object( $activity_object, $inline_css = true ); if ( $inline_css ) { - // Grab the CSS. - $css = \file_get_contents( ACTIVITYPUB_PLUGIN_DIR . 'assets/css/activitypub-embed.css' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents + $path = \is_rtl() ? 'build/embed/embed-rtl.css' : 'build/embed/embed.css'; + $css = \file_get_contents( ACTIVITYPUB_PLUGIN_DIR . $path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents // We embed CSS directly because this may be in an iframe. printf( '', $css ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } diff --git a/includes/class-handler.php b/includes/class-handler.php index b0ead9b4e..595318691 100644 --- a/includes/class-handler.php +++ b/includes/class-handler.php @@ -7,12 +7,14 @@ namespace Activitypub; +use Activitypub\Handler\Accept; use Activitypub\Handler\Announce; use Activitypub\Handler\Create; use Activitypub\Handler\Delete; use Activitypub\Handler\Follow; use Activitypub\Handler\Like; use Activitypub\Handler\Move; +use Activitypub\Handler\Reject; use Activitypub\Handler\Undo; use Activitypub\Handler\Update; @@ -31,14 +33,16 @@ public static function init() { * Register handlers. */ public static function register_handlers() { + Accept::init(); Announce::init(); Create::init(); Delete::init(); Follow::init(); - Undo::init(); - Update::init(); Like::init(); Move::init(); + Reject::init(); + Undo::init(); + Update::init(); /** * Register additional handlers. diff --git a/includes/class-hashtag.php b/includes/class-hashtag.php index 8012f1df2..cfe5084fc 100644 --- a/includes/class-hashtag.php +++ b/includes/class-hashtag.php @@ -32,10 +32,7 @@ public static function init() { * @return array The filtered activity object array. */ public static function filter_activity_object( $activity ) { - /* phpcs:ignore Squiz.PHP.CommentedOutCode.Found - Only changed it for Person and Group as long is not merged: https://github.com/mastodon/mastodon/pull/28629 - */ - if ( ! empty( $activity['summary'] ) && in_array( $activity['type'], array( 'Person', 'Group' ), true ) ) { + if ( ! empty( $activity['summary'] ) ) { $activity['summary'] = self::the_content( $activity['summary'] ); } diff --git a/includes/class-http.php b/includes/class-http.php index 9f9a8dd0e..b1e7402f3 100644 --- a/includes/class-http.php +++ b/includes/class-http.php @@ -35,19 +35,14 @@ public static function post( $url, $body, $user_id ) { */ \do_action( 'activitypub_pre_http_post', $url, $body, $user_id ); - $date = \gmdate( 'D, d M Y H:i:s T' ); - $digest = Signature::generate_digest( $body ); - $signature = Signature::generate_signature( $user_id, 'post', $url, $date, $digest ); - - $wp_version = get_masked_wp_version(); - /** * Filters the HTTP headers user agent string. * * @param string $user_agent The user agent string. */ - $user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . \get_bloginfo( 'url' ) ); + $user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . get_masked_wp_version() . '; ' . \get_bloginfo( 'url' ) ); $args = array( + 'method' => 'POST', 'timeout' => 100, 'limit_response_size' => 1048576, 'redirection' => 3, @@ -55,13 +50,15 @@ public static function post( $url, $body, $user_id ) { 'headers' => array( 'Accept' => 'application/activity+json', 'Content-Type' => 'application/activity+json', - 'Digest' => $digest, - 'Signature' => $signature, - 'Date' => $date, + 'Date' => \gmdate( 'D, d M Y H:i:s T' ), ), 'body' => $body, + 'key_id' => Actors::get_by_id( $user_id )->get_id() . '#main-key', + 'private_key' => Actors::get_private_key( $user_id ), ); + $args = Signature::sign_request( $args, $url ); + $response = \wp_safe_remote_post( $url, $args ); $code = \wp_remote_retrieve_response_code( $response ); @@ -123,11 +120,6 @@ public static function get( $url, $cached = false ) { } } - $date = \gmdate( 'D, d M Y H:i:s T' ); - $signature = Signature::generate_signature( Actors::APPLICATION_USER_ID, 'get', $url, $date ); - - $wp_version = get_masked_wp_version(); - /** * Filters the HTTP headers user agent string. * @@ -136,7 +128,7 @@ public static function get( $url, $cached = false ) { * * @param string $user_agent The user agent string. */ - $user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . \get_bloginfo( 'url' ) ); + $user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . get_masked_wp_version() . '; ' . \get_bloginfo( 'url' ) ); /** * Filters the timeout duration for remote GET requests in ActivityPub. @@ -146,6 +138,7 @@ public static function get( $url, $cached = false ) { $timeout = \apply_filters( 'activitypub_remote_get_timeout', 100 ); $args = array( + 'method' => 'GET', 'timeout' => $timeout, 'limit_response_size' => 1048576, 'redirection' => 3, @@ -153,11 +146,14 @@ public static function get( $url, $cached = false ) { 'headers' => array( 'Accept' => 'application/activity+json', 'Content-Type' => 'application/activity+json', - 'Signature' => $signature, - 'Date' => $date, + 'Date' => \gmdate( 'D, d M Y H:i:s T' ), ), + 'key_id' => Actors::get_by_id( Actors::APPLICATION_USER_ID )->get_id() . '#main-key', + 'private_key' => Actors::get_private_key( Actors::APPLICATION_USER_ID ), ); + $args = Signature::sign_request( $args, $url ); + $response = \wp_safe_remote_get( $url, $args ); $code = \wp_remote_retrieve_response_code( $response ); @@ -235,6 +231,17 @@ public static function generate_cache_key( $url ) { * @return array|WP_Error The Object data as array or WP_Error on failure. */ public static function get_remote_object( $url_or_object, $cached = true ) { + /** + * Filters the preemptive return value of a remote object request. + * + * @param array|string|null $response The response. + * @param array|string|null $url_or_object The Object or the Object URL. + */ + $response = apply_filters( 'activitypub_pre_http_get_remote_object', null, $url_or_object ); + if ( null !== $response ) { + return $response; + } + $url = object_to_uri( $url_or_object ); if ( preg_match( '/^@?' . ACTIVITYPUB_USERNAME_REGEXP . '$/i', $url ) ) { diff --git a/includes/class-link.php b/includes/class-link.php index 783f1fecd..10a5f7b0e 100644 --- a/includes/class-link.php +++ b/includes/class-link.php @@ -28,10 +28,7 @@ public static function init() { * @return array Rhe activity object array. */ public static function filter_activity_object( $activity ) { - /* phpcs:ignore Squiz.PHP.CommentedOutCode.Found - Only changed it for Person and Group as long is not merged: https://github.com/mastodon/mastodon/pull/28629 - */ - if ( ! empty( $activity['summary'] ) && in_array( $activity['type'], array( 'Person', 'Group' ), true ) ) { + if ( ! empty( $activity['summary'] ) ) { $activity['summary'] = self::the_content( $activity['summary'] ); } diff --git a/includes/class-mailer.php b/includes/class-mailer.php index 1e16f6d8c..0b911b9e2 100644 --- a/includes/class-mailer.php +++ b/includes/class-mailer.php @@ -88,8 +88,26 @@ public static function comment_notification_text( $message, $comment_id ) { $post = \get_post( $comment->comment_post_ID ); $comment_author_domain = \gethostbyaddr( $comment->comment_author_IP ); - /* translators: 1: Comment type, 2: Post title */ - $notify_message = \sprintf( html_entity_decode( esc_html__( 'New %1$s on your post “%2$s”.', 'activitypub' ) ), \esc_html( $comment_type['singular'] ), \esc_html( $post->post_title ) ) . "\r\n\r\n"; + // Check if this is a reaction to a post or a comment. + if ( 0 === (int) $comment->comment_parent ) { + $notify_message = \sprintf( + /* translators: 1: Comment type, 2: Post title */ + \html_entity_decode( esc_html__( 'New %1$s on your post “%2$s”.', 'activitypub' ) ), + \esc_html( $comment_type['singular'] ), + \esc_html( $post->post_title ) + ) . PHP_EOL . PHP_EOL; + + } else { + $parent_comment = \get_comment( $comment->comment_parent ); + $notify_message = \sprintf( + /* translators: 1: Comment type, 2: Post title, 3: Parent comment author */ + \html_entity_decode( esc_html__( 'New %1$s on your post “%2$s” in reply to %3$s’s comment.', 'activitypub' ) ), + \esc_html( $comment_type['singular'] ), + \esc_html( $post->post_title ), + \esc_html( $parent_comment->comment_author ) + ) . PHP_EOL . PHP_EOL; + } + /* translators: 1: Website name, 2: Website IP address, 3: Website hostname. */ $notify_message .= \sprintf( \esc_html__( 'From: %1$s (IP address: %2$s, %3$s)', 'activitypub' ), \esc_html( $comment->comment_author ), \esc_html( $comment->comment_author_IP ), \esc_html( $comment_author_domain ) ) . "\r\n"; /* translators: Reaction author URL. */ diff --git a/includes/class-migration.php b/includes/class-migration.php index bb4a0c833..ea5225482 100644 --- a/includes/class-migration.php +++ b/includes/class-migration.php @@ -7,6 +7,7 @@ namespace Activitypub; +use Activitypub\Activity\Actor; use Activitypub\Collection\Actors; use Activitypub\Collection\Extra_Fields; use Activitypub\Collection\Followers; @@ -30,22 +31,6 @@ public static function init() { self::maybe_migrate(); } - /** - * Get the target version. - * - * This is the version that the database structure will be updated to. - * It is the same as the plugin version. - * - * @deprecated 4.2.0 Use constant ACTIVITYPUB_PLUGIN_VERSION directly. - * - * @return string The target version. - */ - public static function get_target_version() { - _deprecated_function( __FUNCTION__, '4.2.0', 'ACTIVITYPUB_PLUGIN_VERSION' ); - - return ACTIVITYPUB_PLUGIN_VERSION; - } - /** * The current version of the database structure. * @@ -175,14 +160,10 @@ public static function maybe_migrate() { add_action( 'init', 'flush_rewrite_rules', 20 ); } if ( \version_compare( $version_from_db, '5.0.0', '<' ) ) { - Scheduler::register_schedules(); \wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'create_post_outbox_items' ) ); \wp_schedule_single_event( \time() + 15, 'activitypub_upgrade', array( 'create_comment_outbox_items' ) ); add_action( 'init', 'flush_rewrite_rules', 20 ); } - if ( \version_compare( $version_from_db, '5.2.0', '<' ) ) { - Scheduler::register_schedules(); - } if ( \version_compare( $version_from_db, '5.4.0', '<' ) ) { \wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'update_actor_json_slashing' ) ); \wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'update_comment_author_emails' ) ); @@ -195,6 +176,32 @@ public static function maybe_migrate() { self::update_notification_options(); } + if ( \version_compare( $version_from_db, '6.0.0', '<' ) ) { + self::migrate_followers_to_ap_actor_cpt(); + \wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'update_actor_json_storage' ) ); + } + + if ( \version_compare( $version_from_db, '6.0.1', '<' ) ) { + self::migrate_followers_to_ap_actor_cpt(); + \wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'update_actor_json_storage' ) ); + } + + if ( \version_compare( $version_from_db, '7.0.0', '<' ) ) { + wp_unschedule_hook( 'activitypub_update_followers' ); + wp_unschedule_hook( 'activitypub_cleanup_followers' ); + + if ( ! \wp_next_scheduled( 'activitypub_update_remote_actors' ) ) { + \wp_schedule_event( time(), 'hourly', 'activitypub_update_remote_actors' ); + } + + if ( ! \wp_next_scheduled( 'activitypub_cleanup_remote_actors' ) ) { + \wp_schedule_event( time(), 'daily', 'activitypub_cleanup_remote_actors' ); + } + } + + // Ensure all required cron schedules are registered. + Scheduler::register_schedules(); + /* * Add new update routines above this comment. ^ * @@ -491,7 +498,7 @@ public static function migrate_to_4_7_2() { global $wpdb; // phpcs:ignore WordPress.DB $followers = $wpdb->get_col( - $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_type = %s", Followers::POST_TYPE ) + $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_type = %s", Actors::POST_TYPE ) ); foreach ( $followers as $id ) { clean_post_cache( $id ); @@ -835,7 +842,7 @@ private static function add_default_extra_field() { } /** - * Rename meta keys. + * Rename user meta keys. * * @param string $old_key The old comment meta key. * @param string $new_key The new comment meta key. @@ -852,6 +859,24 @@ private static function update_usermeta_key( $old_key, $new_key ) { ); } + /** + * Update post meta keys. + * + * @param string $old_key The old post meta key. + * @param string $new_key The new post meta key. + */ + private static function update_postmeta_key( $old_key, $new_key ) { + global $wpdb; + + $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery + $wpdb->postmeta, + array( 'meta_key' => $new_key ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key + array( 'meta_key' => $old_key ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key + array( '%s' ), + array( '%s' ) + ); + } + /** * Rename option keys. * @@ -941,4 +966,86 @@ public static function update_notification_options() { \delete_option( 'activitypub_mailer_new_dm' ); \delete_option( 'activitypub_mailer_new_follower' ); } + + /** + * Migrate followers to the new CPT. + */ + public static function migrate_followers_to_ap_actor_cpt() { + global $wpdb; + + $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery + $wpdb->posts, + array( 'post_type' => Actors::POST_TYPE ), + array( 'post_type' => 'ap_follower' ), + array( '%s' ), + array( '%s' ) + ); + + self::update_postmeta_key( '_activitypub_user_id', Followers::FOLLOWER_META_KEY ); + } + + /** + * Update _activitypub_actor_json meta values to ensure they are properly slashed. + * + * @param int $batch_size Optional. Number of meta values to process per batch. Default 100. + * + * @return array|void Array with batch size and offset if there are more meta values to process, void otherwise. + */ + public static function update_actor_json_storage( $batch_size = 100 ) { + global $wpdb; + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery + $meta_values = $wpdb->get_results( + $wpdb->prepare( + "SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = '_activitypub_actor_json' LIMIT %d", + $batch_size + ) + ); + + $has_kses = false !== \has_filter( 'content_save_pre', 'wp_filter_post_kses' ); + if ( $has_kses ) { + // Prevent KSES from corrupting JSON in post_content. + \kses_remove_filters(); + } + + foreach ( $meta_values as $meta ) { + $post = \get_post( $meta->post_id ); + + if ( ! $post ) { + \delete_post_meta( $meta->post_id, '_activitypub_actor_json' ); + continue; + } + + $post_content = \json_decode( $meta->meta_value, true ); + + if ( \json_last_error() !== JSON_ERROR_NONE ) { + $post_content = Http::get_remote_object( $post->guid ); + + if ( \is_wp_error( $post_content ) ) { + \delete_post_meta( $post->ID, '_activitypub_actor_json' ); + continue; + } + } + + \wp_update_post( + array( + 'ID' => $post->ID, + 'post_content' => \wp_slash( \wp_json_encode( $post_content ) ), + ) + ); + + \delete_post_meta( $post->ID, '_activitypub_actor_json' ); + } + + if ( $has_kses ) { + // Restore KSES filters. + \kses_init_filters(); + } + + if ( \count( $meta_values ) === $batch_size ) { + return array( + 'batch_size' => $batch_size, + ); + } + } } diff --git a/includes/class-options.php b/includes/class-options.php index ee7978e31..a015ebb41 100644 --- a/includes/class-options.php +++ b/includes/class-options.php @@ -137,7 +137,8 @@ public static function maybe_disable_interactions( $pre ) { * Default max image attachments. * * @param string $value The value of the option. - * @return string|int + * + * @return string|int The value of the option. */ public static function default_max_image_attachments( $value ) { if ( ! \is_numeric( $value ) ) { diff --git a/includes/class-query.php b/includes/class-query.php index 2af21e51f..ea5eae870 100644 --- a/includes/class-query.php +++ b/includes/class-query.php @@ -267,50 +267,41 @@ protected function get_request_url() { * @return bool True if the request is an ActivityPub request, false otherwise. */ public function is_activitypub_request() { - if ( isset( $this->is_activitypub_request ) ) { - return $this->is_activitypub_request; - } - - global $wp_query; + if ( ! isset( $this->is_activitypub_request ) ) { + global $wp_query; - // One can trigger an ActivityPub request by adding `?activitypub` to the URL. - if ( - isset( $wp_query->query_vars['activitypub'] ) || - // phpcs:ignore WordPress.Security.NonceVerification.Recommended - isset( $_GET['activitypub'] ) - ) { - \defined( 'ACTIVITYPUB_REQUEST' ) || \define( 'ACTIVITYPUB_REQUEST', true ); - $this->is_activitypub_request = true; + $this->is_activitypub_request = false; - return true; - } - - /* - * The other (more common) option to make an ActivityPub request - * is to send an Accept header. - */ - if ( isset( $_SERVER['HTTP_ACCEPT'] ) ) { - $accept = \sanitize_text_field( \wp_unslash( $_SERVER['HTTP_ACCEPT'] ) ); - - /* - * $accept can be a single value, or a comma separated list of values. - * We want to support both scenarios, - * and return true when the header includes at least one of the following: - * - application/activity+json - * - application/ld+json - * - application/json - */ - if ( \preg_match( '/(application\/(ld\+json|activity\+json|json))/i', $accept ) ) { + // One can trigger an ActivityPub request by adding `?activitypub` to the URL. + if ( isset( $wp_query->query_vars['activitypub'] ) || isset( $_GET['activitypub'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended \defined( 'ACTIVITYPUB_REQUEST' ) || \define( 'ACTIVITYPUB_REQUEST', true ); $this->is_activitypub_request = true; - return true; + // The other (more common) option to make an ActivityPub request is to send an Accept header. + } elseif ( isset( $_SERVER['HTTP_ACCEPT'] ) ) { + $accept = \sanitize_text_field( \wp_unslash( $_SERVER['HTTP_ACCEPT'] ) ); + + /* + * $accept can be a single value, or a comma separated list of values. + * We want to support both scenarios, + * and return true when the header includes at least one of the following: + * - application/activity+json + * - application/ld+json + * - application/json + */ + if ( \preg_match( '/(application\/(ld\+json|activity\+json|json))/i', $accept ) ) { + \defined( 'ACTIVITYPUB_REQUEST' ) || \define( 'ACTIVITYPUB_REQUEST', true ); + $this->is_activitypub_request = true; + } } } - $this->is_activitypub_request = false; - - return false; + /** + * Filters whether the current request is an ActivityPub request. + * + * @param bool $is_activitypub_request True if the request is an ActivityPub request, false otherwise. + */ + return \apply_filters( 'activitypub_is_activitypub_request', $this->is_activitypub_request ); } /** diff --git a/includes/class-sanitize.php b/includes/class-sanitize.php index 42d32491d..0f9be1c83 100644 --- a/includes/class-sanitize.php +++ b/includes/class-sanitize.php @@ -7,6 +7,7 @@ namespace Activitypub; +use Activitypub\Collection\Actors; use Activitypub\Model\Blog; /** @@ -32,6 +33,50 @@ public static function url_list( $value ) { return \array_values( $value ); } + /** + * Sanitize and normalize a list of account identifiers to ActivityPub IDs. + * + * This function processes various identifier formats, such as URLs and + * webfinger identifiers, and normalizes them into a consistent format. + * + * @param string|array $value The value to sanitize. + * + * @return array The sanitized and normalized list of account identifiers. + */ + public static function identifier_list( $value ) { + if ( ! \is_array( $value ) ) { + $value = \explode( PHP_EOL, $value ); + } + + $value = \array_filter( $value ); + $uris = array(); + + foreach ( $value as $uri ) { + $uri = \trim( $uri ); + $uri = \ltrim( $uri, '@' ); + + if ( \is_email( $uri ) ) { + $_uri = Webfinger::resolve( $uri ); + if ( \is_wp_error( $_uri ) ) { + $uris[] = $uri; + continue; + } + + $uri = $_uri; + } + + $uri = \sanitize_url( $uri ); + $actor = Actors::fetch_remote_by_uri( $uri ); + if ( \is_wp_error( $actor ) ) { + $uris[] = $uri; + } else { + $uris[] = \sanitize_url( $actor->guid ); + } + } + + return \array_values( \array_unique( $uris ) ); + } + /** * Sanitize a list of hosts. * @@ -72,6 +117,10 @@ public static function blog_identifier( $value ) { $sanitized = \array_map( 'sanitize_title', $parts ); $sanitized = \implode( '.', $sanitized ); + if ( empty( $sanitized ) ) { + return Blog::get_default_username(); + } + // Check for login or nicename. $user = new \WP_User_Query( array( @@ -119,4 +168,18 @@ public static function constant_value( $value ) { return $value; } + + /** + * Sanitize a webfinger identifier. + * + * @param string $value The value to sanitize. + * + * @return string The sanitized webfinger identifier. + */ + public static function webfinger( $value ) { + $value = \str_replace( 'acct:', '', $value ); + $value = \trim( $value, '@' ); + + return $value; + } } diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php index a6c475618..d34d93919 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -14,7 +14,6 @@ use Activitypub\Scheduler\Comment; use Activitypub\Collection\Actors; use Activitypub\Collection\Outbox; -use Activitypub\Collection\Followers; /** * Scheduler class. @@ -42,8 +41,8 @@ public static function init() { ); // Follower Cleanups. - \add_action( 'activitypub_update_followers', array( self::class, 'update_followers' ) ); - \add_action( 'activitypub_cleanup_followers', array( self::class, 'cleanup_followers' ) ); + \add_action( 'activitypub_update_remote_actors', array( self::class, 'update_remote_actors' ) ); + \add_action( 'activitypub_cleanup_remote_actors', array( self::class, 'cleanup_remote_actors' ) ); // Event callbacks. \add_action( 'activitypub_async_batch', array( self::class, 'async_batch' ), 10, 99 ); @@ -78,12 +77,12 @@ public static function register_schedulers() { * Schedule all ActivityPub schedules. */ public static function register_schedules() { - if ( ! \wp_next_scheduled( 'activitypub_update_followers' ) ) { - \wp_schedule_event( time(), 'hourly', 'activitypub_update_followers' ); + if ( ! \wp_next_scheduled( 'activitypub_update_remote_actors' ) ) { + \wp_schedule_event( time(), 'hourly', 'activitypub_update_remote_actors' ); } - if ( ! \wp_next_scheduled( 'activitypub_cleanup_followers' ) ) { - \wp_schedule_event( time(), 'daily', 'activitypub_cleanup_followers' ); + if ( ! \wp_next_scheduled( 'activitypub_cleanup_remote_actors' ) ) { + \wp_schedule_event( time(), 'daily', 'activitypub_cleanup_remote_actors' ); } if ( ! \wp_next_scheduled( 'activitypub_reprocess_outbox' ) ) { @@ -101,8 +100,8 @@ public static function register_schedules() { * @return void */ public static function deregister_schedules() { - wp_unschedule_hook( 'activitypub_update_followers' ); - wp_unschedule_hook( 'activitypub_cleanup_followers' ); + wp_unschedule_hook( 'activitypub_update_remote_actors' ); + wp_unschedule_hook( 'activitypub_cleanup_remote_actors' ); wp_unschedule_hook( 'activitypub_reprocess_outbox' ); wp_unschedule_hook( 'activitypub_outbox_purge' ); } @@ -142,9 +141,9 @@ public static function unschedule_events_for_item( $outbox_item_id ) { } /** - * Update followers. + * Update remote Actors. */ - public static function update_followers() { + public static function update_remote_actors() { $number = 5; if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) { @@ -152,31 +151,32 @@ public static function update_followers() { } /** - * Filter the number of followers to update. + * Filter the number of remote Actors to update. * - * @param int $number The number of followers to update. + * @param int $number The number of remote Actors to update. */ - $number = apply_filters( 'activitypub_update_followers_number', $number ); - $followers = Followers::get_outdated_followers( $number ); + $number = apply_filters( 'activitypub_update_remote_actors_number', $number ); + $actors = Actors::get_outdated( $number ); - foreach ( $followers as $follower ) { - $meta = get_remote_metadata_by_actor( $follower->get_id(), false ); + foreach ( $actors as $actor ) { + $meta = get_remote_metadata_by_actor( $actor->guid, false ); if ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) { - Followers::add_error( $follower->get__id(), $meta ); + Actors::add_error( $actor->ID, 'Failed to fetch or parse metadata' ); } else { - $follower->from_array( $meta ); - $follower->update(); - - $follower->clear_errors(); + $id = Actors::upsert( $meta ); + if ( \is_wp_error( $id ) ) { + continue; + } + Actors::clear_errors( $id ); } } } /** - * Cleanup followers. + * Cleanup remote Actors. */ - public static function cleanup_followers() { + public static function cleanup_remote_actors() { $number = 5; if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) { @@ -184,31 +184,32 @@ public static function cleanup_followers() { } /** - * Filter the number of followers to clean up. + * Filter the number of remote Actors to clean up. * - * @param int $number The number of followers to clean up. + * @param int $number The number of remote Actors to clean up. */ - $number = apply_filters( 'activitypub_update_followers_number', $number ); - $followers = Followers::get_faulty_followers( $number ); + $number = apply_filters( 'activitypub_cleanup_remote_actors_number', $number ); + $actors = Actors::get_faulty( $number ); - foreach ( $followers as $follower ) { - $meta = get_remote_metadata_by_actor( $follower->get_url(), false ); + foreach ( $actors as $actor ) { + $meta = get_remote_metadata_by_actor( $actor->guid, false ); if ( is_tombstone( $meta ) ) { - $follower->delete(); - } elseif ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) { - if ( $follower->count_errors() >= 5 ) { - $follower->delete(); - \wp_schedule_single_event( - \time(), - 'activitypub_delete_actor_interactions', - array( $follower->get_id() ) - ); + \wp_delete_post( $actor->ID ); + } elseif ( empty( $meta ) || ! is_array( $meta ) || \is_wp_error( $meta ) ) { + if ( Actors::count_errors( $actor->ID ) >= 5 ) { + \wp_schedule_single_event( \time(), 'activitypub_delete_actor_interactions', array( $actor->guid ) ); + \wp_delete_post( $actor->ID ); } else { - Followers::add_error( $follower->get__id(), $meta ); + Actors::add_error( $actor->ID, $meta ); } } else { - $follower->reset_errors(); + $id = Actors::upsert( $meta ); + if ( \is_wp_error( $id ) ) { + Actors::add_error( $actor->ID, $id ); + } else { + Actors::clear_errors( $actor->ID ); + } } } } diff --git a/includes/class-shortcodes.php b/includes/class-shortcodes.php index 2bc06af5b..1554fa646 100644 --- a/includes/class-shortcodes.php +++ b/includes/class-shortcodes.php @@ -312,32 +312,12 @@ public static function image( $attributes, $content, $tag ) { /** * Generates output for the 'ap_hashcats' Shortcode. * + * @deprecated 7.0.0 + * * @return string The post categories as hashtags. */ public static function hashcats() { - $item = self::get_item(); - - if ( ! $item ) { - return ''; - } - - $categories = \get_the_category( $item->ID ); - - if ( ! $categories ) { - return ''; - } - - $hash_tags = array(); - - foreach ( $categories as $category ) { - $hash_tags[] = \sprintf( - '', - \esc_url( \get_category_link( $category ) ), - esc_hashtag( $category->name ) - ); - } - - return \implode( ' ', $hash_tags ); + return ''; } /** diff --git a/includes/class-signature.php b/includes/class-signature.php index af71d7629..a5420cc60 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -7,11 +7,9 @@ namespace Activitypub; -use WP_Error; -use DateTime; -use DateTimeZone; -use WP_REST_Request; use Activitypub\Collection\Actors; +use Activitypub\Signature\Http_Signature_Draft; +use Activitypub\Signature\Http_Message_Signature; /** * ActivityPub Signature Class. @@ -22,168 +20,240 @@ class Signature { /** - * Return the public key for a given user. + * Sign an HTTP Request. * - * @param int $user_id The WordPress User ID. - * @param bool $force Optional. Force the generation of a new key pair. Default false. + * @param array $args An array of HTTP request arguments. + * @param string $url The request URL. * - * @return mixed The public key. + * @return array Request arguments with signature headers. */ - public static function get_public_key_for( $user_id, $force = false ) { - if ( $force ) { - self::generate_key_pair_for( $user_id ); - } + public static function sign_request( $args, $url ) { + // Bail if there's nothing to sign with. + if ( ! isset( $args['key_id'], $args['private_key'] ) ) { + return $args; + } + + $args = \wp_parse_args( + $args, + array( + 'method' => 'GET', + 'headers' => array( + 'Date' => \gmdate( 'D, d M Y H:i:s T' ), + ), + ) + ); - $key_pair = self::get_keypair_for( $user_id ); + if ( '1' === \get_option( 'activitypub_rfc9421_signature' ) && self::could_support_rfc9421( $url ) ) { + $signature = new Http_Message_Signature(); + \add_filter( 'http_response', array( self::class, 'maybe_double_knock' ), 10, 3 ); + } else { + $signature = new Http_Signature_Draft(); + } - return $key_pair['public_key']; + return $signature->sign( $args, $url ); } /** - * Return the private key for a given user. + * Verifies the http signatures * - * @param int $user_id The WordPress User ID. - * @param bool $force Optional. Force the generation of a new key pair. Default false. + * @param \WP_REST_Request|array $request The request object or $_SERVER array. * - * @return mixed The private key. + * @return bool|\WP_Error A boolean or WP_Error. */ - public static function get_private_key_for( $user_id, $force = false ) { - if ( $force ) { - self::generate_key_pair_for( $user_id ); + public static function verify_http_signature( $request ) { + if ( is_object( $request ) ) { // REST Request object. + $body = $request->get_body(); + $headers = $request->get_headers(); + $headers['(request-target)'][0] = strtolower( $request->get_method() ) . ' ' . self::get_route( $request ); + } else { + $headers = self::format_server_request( $request ); + $headers['(request-target)'][0] = strtolower( $headers['request_method'][0] ) . ' ' . $headers['request_uri'][0]; } - $key_pair = self::get_keypair_for( $user_id ); + $signature = isset( $headers['signature_input'] ) ? new Http_Message_Signature() : new Http_Signature_Draft(); - return $key_pair['private_key']; + return $signature->verify( $headers, $body ?? null ); } /** - * Return the key pair for a given user. + * If a request with RFC-9421 signature fails, we try again with the Draft Cavage signature. * - * @param int $user_id The WordPress User ID. + * @param array $response HTTP response. + * @param array $parsed_args HTTP request arguments. + * @param string $url The request URL. * - * @return array The key pair. + * @return array The HTTP response. */ - public static function get_keypair_for( $user_id ) { - $option_key = self::get_signature_options_key_for( $user_id ); - $key_pair = \get_option( $option_key ); + public static function maybe_double_knock( $response, $parsed_args, $url ) { + // Remove this filter to prevent infinite recursion. + \remove_filter( 'http_response', array( self::class, 'maybe_double_knock' ) ); - if ( ! $key_pair ) { - $key_pair = self::generate_key_pair_for( $user_id ); + $response_code = \wp_remote_retrieve_response_code( $response ); + + // Fall back to Draft Cavage signature for any 4xx responses. + if ( $response_code >= 400 && $response_code < 500 ) { + unset( $parsed_args['headers']['Signature'], $parsed_args['headers']['Signature-Input'], $parsed_args['headers']['Content-Digest'] ); + self::rfc9421_add_unsupported_host( $url ); + + $parsed_args = ( new Http_Signature_Draft() )->sign( $parsed_args, $url ); + $response = \wp_remote_request( $url, $parsed_args ); } - return $key_pair; + return $response; } /** - * Generates the pair keys + * Formats the $_SERVER to resemble the WP_REST_REQUEST array, + * for use with verify_http_signature(). * - * @param int $user_id The WordPress User ID. + * @param array $server The $_SERVER array. * - * @return array The key pair. + * @return array $request The formatted request array. */ - protected static function generate_key_pair_for( $user_id ) { - $option_key = self::get_signature_options_key_for( $user_id ); - $key_pair = self::check_legacy_key_pair_for( $user_id ); + public static function format_server_request( $server ) { + $headers = array(); - if ( $key_pair ) { - \add_option( $option_key, $key_pair ); + foreach ( $server as $key => $value ) { + $key = \str_replace( 'http_', '', \strtolower( $key ) ); + $headers[ $key ][] = \wp_unslash( $value ); - return $key_pair; } - $config = array( - 'digest_alg' => 'sha512', - 'private_key_bits' => 2048, - 'private_key_type' => \OPENSSL_KEYTYPE_RSA, - ); + return $headers; + } - $key = \openssl_pkey_new( $config ); - $private_key = null; - $detail = array(); - if ( $key ) { - \openssl_pkey_export( $key, $private_key ); + /** + * Returns route. + * + * @param \WP_REST_Request $request The request object. + * + * @return string + */ + private static function get_route( $request ) { + // Check if the route starts with "index.php". + if ( str_starts_with( $request->get_route(), '/index.php' ) || ! rest_get_url_prefix() ) { + $route = $request->get_route(); + } else { + $route = '/' . rest_get_url_prefix() . '/' . ltrim( $request->get_route(), '/' ); + } + + // Fix route for subdirectory installations. + $path = \wp_parse_url( \get_home_url(), PHP_URL_PATH ); - $detail = \openssl_pkey_get_details( $key ); + if ( \is_string( $path ) ) { + $path = trim( $path, '/' ); } - // Check if keys are valid. - if ( - empty( $private_key ) || ! is_string( $private_key ) || - ! isset( $detail['key'] ) || ! is_string( $detail['key'] ) - ) { - return array( - 'private_key' => null, - 'public_key' => null, - ); + if ( $path ) { + $route = '/' . $path . $route; } - $key_pair = array( - 'private_key' => $private_key, - 'public_key' => $detail['key'], - ); + return $route; + } - // Persist keys. - \add_option( $option_key, $key_pair ); + /** + * Check if RFC-9421 signature could be supported. + * + * @param string $url The URL to check. + * + * @return bool True, if RFC-9421 signature could be supported, false otherwise. + */ + private static function could_support_rfc9421( $url ) { + $host = \wp_parse_url( $url, \PHP_URL_HOST ); + $list = \get_option( 'activitypub_rfc9421_unsupported', array() ); - return $key_pair; + if ( isset( $list[ $host ] ) ) { + if ( $list[ $host ] > \time() ) { + return false; + } + + unset( $list[ $host ] ); + \update_option( 'activitypub_rfc9421_unsupported', $list ); + } + + return true; } /** - * Return the option key for a given user. + * Set RFC-9421 signature unsupported for a given host. * - * @param int $user_id The WordPress User ID. + * @param string $url The URL to set. + */ + private static function rfc9421_add_unsupported_host( $url ) { + $list = \get_option( 'activitypub_rfc9421_unsupported', array() ); + $host = \wp_parse_url( $url, \PHP_URL_HOST ); + + $list[ $host ] = \time() + MONTH_IN_SECONDS; + \update_option( 'activitypub_rfc9421_unsupported', $list, false ); + } + + /** + * Return the public key for a given user. + * + * @deprecated 7.0.0 Use {@see Actors::get_public_key()}. + * + * @param int $user_id The WordPress User ID. + * @param bool $force Optional. Force the generation of a new key pair. Default false. * - * @return string The option key. + * @return string The public key. */ - protected static function get_signature_options_key_for( $user_id ) { - $id = $user_id; + public static function get_public_key_for( $user_id, $force = false ) { + \_deprecated_function( __METHOD__, '7.0.0', 'Activitypub\Collection\Actors::get_public_key' ); - if ( $user_id > 0 ) { - $user = \get_userdata( $user_id ); - // Sanitize username because it could include spaces and special chars. - $id = sanitize_title( $user->user_login ); - } + return Actors::get_public_key( $user_id, $force ); + } - return 'activitypub_keypair_for_' . $id; + /** + * Return the private key for a given user. + * + * @deprecated 7.0.0 Use {@see Actors::get_private_key()}. + * + * @param int $user_id The WordPress User ID. + * @param bool $force Optional. Force the generation of a new key pair. Default false. + * + * @return string The private key. + */ + public static function get_private_key_for( $user_id, $force = false ) { + \_deprecated_function( __METHOD__, '7.0.0', 'Activitypub\Collection\Actors::get_private_key' ); + + return Actors::get_private_key( $user_id, $force ); } /** - * Check if there is a legacy key pair + * Return the key pair for a given user. + * + * @deprecated 7.0.0 Use {@see Actors::get_keypair()}. * * @param int $user_id The WordPress User ID. * - * @return array|bool The key pair or false. + * @return array The key pair. */ - protected static function check_legacy_key_pair_for( $user_id ) { - switch ( $user_id ) { - case 0: - $public_key = \get_option( 'activitypub_blog_user_public_key' ); - $private_key = \get_option( 'activitypub_blog_user_private_key' ); - break; - case -1: - $public_key = \get_option( 'activitypub_application_user_public_key' ); - $private_key = \get_option( 'activitypub_application_user_private_key' ); - break; - default: - $public_key = \get_user_meta( $user_id, 'magic_sig_public_key', true ); - $private_key = \get_user_meta( $user_id, 'magic_sig_private_key', true ); - break; - } + public static function get_keypair_for( $user_id ) { + \_deprecated_function( __METHOD__, '7.0.0', 'Activitypub\Collection\Actors::get_keypair' ); - if ( ! empty( $public_key ) && is_string( $public_key ) && ! empty( $private_key ) && is_string( $private_key ) ) { - return array( - 'private_key' => $private_key, - 'public_key' => $public_key, - ); - } + return Actors::get_keypair( $user_id ); + } - return false; + /** + * Get public key from key_id. + * + * @deprecated 7.0.0 Use {@see Actors::get_remote_key()}. + * + * @param string $key_id The URL to the public key. + * + * @return resource|\WP_Error The public key resource or WP_Error. + */ + public static function get_remote_key( $key_id ) { + \_deprecated_function( __METHOD__, '7.0.0', 'Activitypub\Collection\Actors::get_remote_key()' ); + + return Actors::get_remote_key( $key_id ); } /** * Generates the Signature for an HTTP Request. * + * @deprecated 7.0.0 Use {@see Signature::sign_request()}. + * * @param int $user_id The WordPress User ID. * @param string $http_method The HTTP method. * @param string $url The URL to send the request to. @@ -193,8 +263,10 @@ protected static function check_legacy_key_pair_for( $user_id ) { * @return string The signature. */ public static function generate_signature( $user_id, $http_method, $url, $date, $digest = null ) { + \_deprecated_function( __METHOD__, 'unreleased', self::class . '::sign_request()' ); + $user = Actors::get_by_id( $user_id ); - $key = self::get_private_key_for( $user->get__id() ); + $key = Actors::get_private_key( $user_id ); $url_parts = \wp_parse_url( $url ); @@ -232,129 +304,19 @@ public static function generate_signature( $user_id, $http_method, $url, $date, } } - /** - * Verifies the http signatures - * - * @param WP_REST_Request|array $request The request object or $_SERVER array. - * - * @return bool|WP_Error A boolean or WP_Error. - */ - public static function verify_http_signature( $request ) { - if ( is_object( $request ) ) { // REST Request object. - // Check if route starts with "index.php". - if ( str_starts_with( $request->get_route(), '/index.php' ) || ! rest_get_url_prefix() ) { - $route = $request->get_route(); - } else { - $route = '/' . rest_get_url_prefix() . '/' . ltrim( $request->get_route(), '/' ); - } - - // Fix route for subdirectory installs. - $path = \wp_parse_url( \get_home_url(), PHP_URL_PATH ); - - if ( \is_string( $path ) ) { - $path = trim( $path, '/' ); - } - - if ( $path ) { - $route = '/' . $path . $route; - } - - $headers = $request->get_headers(); - $headers['(request-target)'][0] = strtolower( $request->get_method() ) . ' ' . $route; - } else { - $request = self::format_server_request( $request ); - $headers = $request['headers']; // $_SERVER array - $headers['(request-target)'][0] = strtolower( $headers['request_method'][0] ) . ' ' . $headers['request_uri'][0]; - } - - if ( array_key_exists( 'signature', $headers ) ) { - $signature_block = self::parse_signature_header( $headers['signature'][0] ); - } elseif ( array_key_exists( 'authorization', $headers ) ) { - $signature_block = self::parse_signature_header( $headers['authorization'][0] ); - } else { - return new WP_Error( 'activitypub_signature', __( 'Incompatible request signature. keyId and signature are required', 'activitypub' ), array( 'status' => 401 ) ); - } - - $signed_headers = $signature_block['headers']; - - $signed_data = self::get_signed_data( $signed_headers, $signature_block, $headers ); - if ( ! $signed_data ) { - return new WP_Error( 'activitypub_signature', __( 'Signed request date outside acceptable time window', 'activitypub' ), array( 'status' => 401 ) ); - } - - $algorithm = self::get_signature_algorithm( $signature_block ); - if ( ! $algorithm ) { - return new WP_Error( 'activitypub_signature', __( 'Unsupported signature algorithm (only rsa-sha256 and hs2019 are supported)', 'activitypub' ), array( 'status' => 401 ) ); - } - - if ( \in_array( 'digest', $signed_headers, true ) && isset( $body ) ) { - if ( is_array( $headers['digest'] ) ) { - $headers['digest'] = $headers['digest'][0]; - } - $algorithm = 'sha256'; - $digest = explode( '=', $headers['digest'], 2 ); - if ( 'SHA-512' === $digest[0] ) { - $algorithm = 'sha512'; - } - - if ( \base64_encode( \hash( $algorithm, $body, true ) ) !== $digest[1] ) { // phpcs:ignore - return new WP_Error( 'activitypub_signature', __( 'Invalid Digest header', 'activitypub' ), array( 'status' => 401 ) ); - } - } - - $public_key = self::get_remote_key( $signature_block['keyId'] ); - - if ( \is_wp_error( $public_key ) ) { - return $public_key; - } - - $verified = \openssl_verify( $signed_data, $signature_block['signature'], $public_key, $algorithm ) > 0; - if ( ! $verified ) { - return new WP_Error( 'activitypub_signature', __( 'Invalid signature', 'activitypub' ), array( 'status' => 401 ) ); - } - return $verified; - } - - /** - * Get public key from key_id. - * - * @param string $key_id The URL to the public key. - * - * @return resource|WP_Error The public key resource or WP_Error. - */ - public static function get_remote_key( $key_id ) { - $actor = get_remote_metadata_by_actor( strip_fragment_from_url( $key_id ) ); - if ( \is_wp_error( $actor ) ) { - return new WP_Error( - 'activitypub_no_remote_profile_found', - __( 'No Profile found or Profile not accessible', 'activitypub' ), - array( 'status' => 401 ) - ); - } - - if ( isset( $actor['publicKey']['publicKeyPem'] ) ) { - $key_resource = \openssl_pkey_get_public( \rtrim( $actor['publicKey']['publicKeyPem'] ) ); - if ( $key_resource ) { - return $key_resource; - } - } - - return new WP_Error( - 'activitypub_no_remote_key_found', - __( 'No Public-Key found', 'activitypub' ), - array( 'status' => 401 ) - ); - } - /** * Gets the signature algorithm from the signature header. * + * @deprecated 7.0.0 Use {@see Signature::verify()}. + * * @param array $signature_block The signature block. * - * @return string The signature algorithm. + * @return string|bool The signature algorithm or false if not found. */ - public static function get_signature_algorithm( $signature_block ) { - if ( $signature_block['algorithm'] ) { + public static function get_signature_algorithm( $signature_block ) { // phpcs:ignore + \_deprecated_function( __METHOD__, 'unreleased', self::class . '::verify' ); + + if ( ! empty( $signature_block['algorithm'] ) ) { switch ( $signature_block['algorithm'] ) { case 'rsa-sha-512': return 'sha512'; // hs2019 https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12. @@ -362,17 +324,22 @@ public static function get_signature_algorithm( $signature_block ) { return 'sha256'; } } + return false; } /** * Parses the Signature header. * + * @deprecated 7.0.0 Use {@see Signature::verify()}. + * * @param string $signature The signature header. * * @return array Signature parts. */ - public static function parse_signature_header( $signature ) { + public static function parse_signature_header( $signature ) { // phpcs:ignore + \_deprecated_function( __METHOD__, 'unreleased', self::class . '::verify' ); + $parsed_header = array(); $matches = array(); @@ -405,13 +372,17 @@ public static function parse_signature_header( $signature ) { /** * Gets the header data from the included pseudo headers. * + * @deprecated 7.0.0 Use {@see Signature::verify()}. + * * @param array $signed_headers The signed headers. * @param array $signature_block The signature block. * @param array $headers The HTTP headers. * * @return string signed headers for comparison */ - public static function get_signed_data( $signed_headers, $signature_block, $headers ) { + public static function get_signed_data( $signed_headers, $signature_block, $headers ) { // phpcs:ignore + \_deprecated_function( __METHOD__, 'unreleased', self::class . '::verify' ); + $signed_data = ''; // This also verifies time-based values by returning false if any of these are out of range. @@ -458,8 +429,8 @@ public static function get_signed_data( $signed_headers, $signature_block, $head } // Allow a bit of leeway for misconfigured clocks. - $d = new DateTime( $headers[ $header ][0] ); - $d->setTimeZone( new DateTimeZone( 'UTC' ) ); + $d = new \DateTime( $headers[ $header ][0] ); + $d->setTimeZone( new \DateTimeZone( 'UTC' ) ); $c = $d->format( 'U' ); $d_plus = time() + ( 3 * HOUR_IN_SECONDS ); @@ -475,44 +446,23 @@ public static function get_signed_data( $signed_headers, $signature_block, $head $signed_data .= $header . ': ' . $headers[ $header ][0] . "\n"; } } + return \rtrim( $signed_data, "\n" ); } /** * Generates the digest for an HTTP Request. * + * @deprecated 7.0.0 Use {@see Signature::sign_request()}. + * * @param string $body The body of the request. * * @return string The digest. */ public static function generate_digest( $body ) { + \_deprecated_function( __METHOD__, 'unreleased', self::class . '::sign_request' ); + $digest = \base64_encode( \hash( 'sha256', $body, true ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode return "SHA-256=$digest"; } - - /** - * Formats the $_SERVER to resemble the WP_REST_REQUEST array, - * for use with verify_http_signature(). - * - * @param array $server The $_SERVER array. - * - * @return array $request The formatted request array. - */ - public static function format_server_request( $server ) { - $request = array(); - foreach ( $server as $param_key => $param_val ) { - $req_param = strtolower( $param_key ); - if ( 'REQUEST_URI' === $req_param ) { - $request['headers']['route'][] = $param_val; - } else { - $header_key = str_replace( - 'http_', - '', - $req_param - ); - $request['headers'][ $header_key ][] = \wp_unslash( $param_val ); - } - } - return $request; - } } diff --git a/includes/class-webfinger.php b/includes/class-webfinger.php index b53aa2855..9f6057975 100644 --- a/includes/class-webfinger.php +++ b/includes/class-webfinger.php @@ -211,17 +211,17 @@ public static function get_data( $uri ) { $webfinger_url = sprintf( 'https://%s/.well-known/webfinger?resource=%s', $host, - rawurlencode( $identifier ) + \rawurlencode( $identifier ) ); - $response = wp_safe_remote_get( + $response = \wp_safe_remote_get( $webfinger_url, array( 'headers' => array( 'Accept' => 'application/jrd+json' ), ) ); - if ( is_wp_error( $response ) ) { + if ( \is_wp_error( $response ) || \wp_remote_retrieve_response_code( $response ) >= 400 ) { return new WP_Error( 'webfinger_url_not_accessible', __( 'The WebFinger Resource is not accessible.', 'activitypub' ), @@ -232,8 +232,8 @@ public static function get_data( $uri ) { ); } - $body = wp_remote_retrieve_body( $response ); - $data = json_decode( $body, true ); + $body = \wp_remote_retrieve_body( $response ); + $data = \json_decode( $body, true ); \set_transient( $transient_key, $data, WEEK_IN_SECONDS ); @@ -297,4 +297,16 @@ public static function generate_cache_key( $uri ) { return 'webfinger_' . md5( $uri ); } + + /** + * Infer a shortname from the Actor ID or URL. Used only for fallbacks, + * we will try to use what's supplied. + * + * @param string $uri The URI. + * + * @return string Hopefully the name of the Follower. + */ + public static function guess( $uri ) { + return extract_name_from_uri( $uri ) . '@' . \wp_parse_url( $uri, PHP_URL_HOST ); + } } diff --git a/includes/collection/class-actors.php b/includes/collection/class-actors.php index cf7c68a16..4d5b241da 100644 --- a/includes/collection/class-actors.php +++ b/includes/collection/class-actors.php @@ -7,12 +7,13 @@ namespace Activitypub\Collection; -use WP_Error; -use WP_User_Query; +use Activitypub\Http; use Activitypub\Model\User; use Activitypub\Model\Blog; use Activitypub\Model\Application; +use Activitypub\Activity\Actor; +use function Activitypub\get_remote_metadata_by_actor; use function Activitypub\object_to_uri; use function Activitypub\normalize_url; use function Activitypub\normalize_host; @@ -22,6 +23,8 @@ /** * Actors collection. + * + * Provides methods to retrieve, create, update, and manage ActivityPub actors (users, blogs, applications, and remote actors). */ class Actors { /** @@ -38,12 +41,19 @@ class Actors { */ const APPLICATION_USER_ID = -1; + /** + * Post type for storing remote actors. + * + * @var string + */ + const POST_TYPE = 'ap_actor'; + /** * Get the Actor by ID. * - * @param int $user_id The User-ID. + * @param int $user_id The user ID. * - * @return User|Blog|Application|WP_Error The Actor or WP_Error if user not found. + * @return Actor|User|Blog|Application|\WP_Error Actor object or WP_Error if not found or not permitted. */ public static function get_by_id( $user_id ) { if ( is_numeric( $user_id ) ) { @@ -51,7 +61,7 @@ public static function get_by_id( $user_id ) { } if ( ! user_can_activitypub( $user_id ) ) { - return new WP_Error( + return new \WP_Error( 'activitypub_user_not_found', \__( 'Actor not found', 'activitypub' ), array( 'status' => 404 ) @@ -71,9 +81,9 @@ public static function get_by_id( $user_id ) { /** * Get the Actor by username. * - * @param string $username Name of the Actor. + * @param string $username Name of the actor. * - * @return User|Blog|Application|WP_Error The Actor or WP_Error if user not found. + * @return User|Blog|Application|\WP_Error Actor object or WP_Error if not found. */ public static function get_by_username( $username ) { /** @@ -93,7 +103,7 @@ public static function get_by_username( $username ) { \get_option( 'activitypub_blog_identifier' ) === $username ) { if ( is_user_type_disabled( 'blog' ) ) { - return new WP_Error( + return new \WP_Error( 'activitypub_user_not_found', \__( 'Actor not found', 'activitypub' ), array( 'status' => 404 ) @@ -109,7 +119,7 @@ public static function get_by_username( $username ) { } // Check for 'activitypub_username' meta. - $user = new WP_User_Query( + $user = new \WP_User_Query( array( 'count_total' => false, 'number' => 1, @@ -137,7 +147,7 @@ public static function get_by_username( $username ) { $username = str_replace( array( '*', '%' ), '', $username ); // Check for login or nicename. - $user = new WP_User_Query( + $user = new \WP_User_Query( array( 'count_total' => false, 'search' => $username, @@ -155,7 +165,7 @@ public static function get_by_username( $username ) { } } - return new WP_Error( + return new \WP_Error( 'activitypub_user_not_found', \__( 'Actor not found', 'activitypub' ), array( 'status' => 404 ) @@ -163,17 +173,17 @@ public static function get_by_username( $username ) { } /** - * Get the Actor by resource. + * Get the Actor by resource URI (acct, http(s), etc). * - * @param string $uri The Actor resource. + * @param string $uri The actor resource URI. * - * @return User|Blog|Application|WP_Error The Actor or WP_Error if user not found. + * @return User|Blog|Application|\WP_Error Actor object or WP_Error if not found. */ public static function get_by_resource( $uri ) { $uri = object_to_uri( $uri ); if ( ! $uri ) { - return new WP_Error( + return new \WP_Error( 'activitypub_no_uri', \__( 'No URI provided', 'activitypub' ), array( 'status' => 404 ) @@ -195,6 +205,14 @@ public static function get_by_resource( $uri ) { // Check for http(s) URIs. case 'http': case 'https': + // Check locally stored remote Actor. + $post = self::get_remote_by_uri( $uri ); + + if ( ! \is_wp_error( $post ) ) { + return self::get_actor( $post ); + } + + // Check for http(s)://blog.example.com/@username. $resource_path = \wp_parse_url( $uri, PHP_URL_PATH ); if ( $resource_path ) { @@ -206,7 +224,6 @@ public static function get_by_resource( $uri ) { $resource_path = \trim( $resource_path, '/' ); - // Check for http(s)://blog.example.com/@username. if ( str_starts_with( $resource_path, '@' ) ) { $identifier = \str_replace( '@', '', $resource_path ); $identifier = \trim( $identifier, '/' ); @@ -232,7 +249,7 @@ public static function get_by_resource( $uri ) { return self::get_by_id( self::BLOG_USER_ID ); } - return new WP_Error( + return new \WP_Error( 'activitypub_no_user_found', \__( 'Actor not found', 'activitypub' ), array( 'status' => 404 ) @@ -245,7 +262,7 @@ public static function get_by_resource( $uri ) { $blog_host = normalize_host( \wp_parse_url( \home_url( '/' ), \PHP_URL_HOST ) ); if ( $blog_host !== $host && get_option( 'activitypub_old_host' ) !== $host ) { - return new WP_Error( + return new \WP_Error( 'activitypub_wrong_host', \__( 'Resource host does not match blog host', 'activitypub' ), array( 'status' => 404 ) @@ -259,7 +276,7 @@ public static function get_by_resource( $uri ) { return self::get_by_username( $identifier ); default: - return new WP_Error( + return new \WP_Error( 'activitypub_wrong_scheme', \__( 'Wrong scheme', 'activitypub' ), array( 'status' => 404 ) @@ -268,11 +285,11 @@ public static function get_by_resource( $uri ) { } /** - * Get the Actor by resource. + * Get the Actor by various identifier types (ID, URI, username, or email). * - * @param string $id The Actor resource. + * @param string|int $id Actor identifier (user ID, URI, username, or email). * - * @return User|Blog|Application|WP_Error The Actor or WP_Error if user not found. + * @return User|Blog|Application|\WP_Error Actor object or WP_Error if not found. */ public static function get_by_various( $id ) { if ( is_numeric( $id ) ) { @@ -294,9 +311,9 @@ public static function get_by_various( $id ) { } /** - * Get the Actor collection. + * Get the collection of all local user actors. * - * @return array The Actor collection. + * @return Actor[] Array of User actor objects. */ public static function get_collection() { if ( is_user_type_disabled( 'user' ) ) { @@ -325,9 +342,9 @@ public static function get_collection() { } /** - * Get all active Actors including the Blog Actor. + * Get all active actors, including the Blog actor if enabled. * - * @return array The actor collection. + * @return array Array of User and Blog actor objects. */ public static function get_all() { $return = array(); @@ -365,7 +382,8 @@ public static function get_all() { * Returns the actor type based on the user ID. * * @param int $user_id The user ID to check. - * @return string The user type. + * + * @return string Actor type: 'user', 'blog', or 'application'. */ public static function get_type_by_id( $user_id ) { $user_id = (int) $user_id; @@ -380,4 +398,552 @@ public static function get_type_by_id( $user_id ) { return 'user'; } + + /** + * Upsert (insert or update) a remote actor as a custom post type. + * + * @param array|Actor $actor ActivityPub actor object (array or actor, must include 'id'). + * + * @return int|\WP_Error Post ID on success, WP_Error on failure. + */ + public static function upsert( $actor ) { + if ( \is_array( $actor ) ) { + $actor = Actor::init_from_array( $actor ); + } + + $post = self::get_remote_by_uri( $actor->get_id() ); + + if ( ! \is_wp_error( $post ) ) { + return self::update( $post, $actor ); + } + + return self::create( $actor ); + } + + /** + * Create a remote actor as a custom post type. + * + * @param array|Actor $actor ActivityPub actor object (array or Actor, must include 'id'). + * + * @return int|\WP_Error Post ID on success, WP_Error on failure. + */ + public static function create( $actor ) { + if ( \is_array( $actor ) ) { + $actor = Actor::init_from_array( $actor ); + } + + $args = self::prepare_custom_post_type( $actor ); + + if ( \is_wp_error( $args ) ) { + return $args; + } + + $has_kses = false !== \has_filter( 'content_save_pre', 'wp_filter_post_kses' ); + if ( $has_kses ) { + // Prevent KSES from corrupting JSON in post_content. + \kses_remove_filters(); + } + + $post_id = \wp_insert_post( $args ); + + if ( $has_kses ) { + // Restore KSES filters. + \kses_init_filters(); + } + + return $post_id; + } + + /** + * Update a remote Actor object by actor URL (guid). + * + * @param int|\WP_Post $post The post ID or object. + * @param array|Actor $actor The ActivityPub actor object as associative array (must include 'id'). + * + * @return int|\WP_Error The post ID or WP_Error. + */ + public static function update( $post, $actor ) { + if ( \is_array( $actor ) ) { + $actor = Actor::init_from_array( $actor ); + } + + $post = \get_post( $post, ARRAY_A ); + + if ( ! $post ) { + return new \WP_Error( + 'activitypub_actor_not_found', + \__( 'Actor not found', 'activitypub' ), + array( 'status' => 404 ) + ); + } + + $args = self::prepare_custom_post_type( $actor ); + + if ( \is_wp_error( $args ) ) { + return $args; + } + + $args = \wp_parse_args( $args, $post ); + + $has_kses = false !== \has_filter( 'content_save_pre', 'wp_filter_post_kses' ); + if ( $has_kses ) { + // Prevent KSES from corrupting JSON in post_content. + \kses_remove_filters(); + } + + $post_id = \wp_update_post( $args ); + + if ( $has_kses ) { + // Restore KSES filters. + \kses_init_filters(); + } + + return $post_id; + } + + /** + * Delete a remote actor object by actor URL (guid). + * + * @param int $post_id The post ID. + * + * @return bool True on success, false on failure. + */ + public static function delete( $post_id ) { + return \wp_delete_post( $post_id ); + } + + /** + * Get a remote actor post by actor URI (guid). + * + * @param string $actor_uri The actor URI. + * + * @return \WP_Post|\WP_Error Post object or WP_Error if not found. + */ + public static function get_remote_by_uri( $actor_uri ) { + global $wpdb; + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + $post_id = $wpdb->get_var( + $wpdb->prepare( + "SELECT ID FROM $wpdb->posts WHERE guid=%s AND post_type=%s", + esc_sql( $actor_uri ), + esc_sql( self::POST_TYPE ) + ) + ); + + if ( ! $post_id ) { + return new \WP_Error( + 'activitypub_actor_not_found', + \__( 'Actor not found', 'activitypub' ), + array( 'status' => 404 ) + ); + } + + return \get_post( $post_id ); + } + + /** + * Lookup a remote actor post by actor URI (guid), fetching from remote if not found locally. + * + * @param string $actor_uri The actor URI. + * + * @return \WP_Post|\WP_Error Post object or WP_Error if not found. + */ + public static function fetch_remote_by_uri( $actor_uri ) { + $post = self::get_remote_by_uri( $actor_uri ); + + if ( ! \is_wp_error( $post ) ) { + return $post; + } + + $object = Http::get_remote_object( $actor_uri, false ); + + if ( \is_wp_error( $object ) ) { + return $object; + } + + $post_id = self::upsert( $object ); + + if ( \is_wp_error( $post_id ) ) { + return $post_id; + } + + return \get_post( $post_id ); + } + + /** + * Store an error that occurred when sending an ActivityPub message to a follower. + * + * The error will be stored in post meta. + * + * @param int $post_id The ID of the WordPress Custom-Post-Type. + * @param string|\WP_Error $error The error message. + * + * @return int|false The meta ID on success, false on failure. + */ + public static function add_error( $post_id, $error ) { + if ( \is_string( $error ) ) { + $error_message = $error; + } elseif ( \is_wp_error( $error ) ) { + $error_message = $error->get_error_message(); + } else { + $error_message = \__( + 'Unknown Error or misconfigured Error-Message', + 'activitypub' + ); + } + + return \add_post_meta( + $post_id, + '_activitypub_errors', + $error_message + ); + } + + /** + * Count the errors for an actor. + * + * @param int $post_id The ID of the WordPress Custom-Post-Type. + * + * @return int The number of errors. + */ + public static function count_errors( $post_id ) { + return \count( \get_post_meta( $post_id, '_activitypub_errors', false ) ); + } + + /** + * Get all error messages for an actor. + * + * @param int $post_id The post ID. + * + * @return string[] Array of error messages. + */ + public static function get_errors( $post_id ) { + return \get_post_meta( $post_id, '_activitypub_errors', false ); + } + + /** + * Clear all errors for an actor. + * + * @param int $post_id The ID of the WordPress Custom-Post-Type. + * + * @return bool True on success, false on failure. + */ + public static function clear_errors( $post_id ) { + return \delete_post_meta( $post_id, '_activitypub_errors' ); + } + + /** + * Get all remote actors (Custom Post Type) that had errors. + * + * @param int $number Optional. Number of actors to return. Default 20. + * + * @return \WP_Post[] Array of faulty actor posts. + */ + public static function get_faulty( $number = 20 ) { + $args = array( + 'post_type' => self::POST_TYPE, + 'posts_per_page' => $number, + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + 'meta_query' => array( + 'relation' => 'OR', + array( + 'key' => '_activitypub_errors', + 'compare' => 'EXISTS', + ), + array( + 'key' => '_activitypub_inbox', + 'compare' => 'NOT EXISTS', + ), + array( + 'key' => '_activitypub_inbox', + 'value' => '', + 'compare' => '=', + ), + ), + ); + + return ( new \WP_Query() )->query( $args ); + } + + /** + * Get all remote actor posts not updated for a given time. + * + * @param int $number Optional. Limits the result. Default 50. + * @param int $older_than Optional. The time in seconds. Default DAY_IN_SECONDS. + * + * @return \WP_Post[] The list of actors. + */ + public static function get_outdated( $number = 50, $older_than = DAY_IN_SECONDS ) { + $args = array( + 'post_type' => self::POST_TYPE, + 'posts_per_page' => $number, + 'orderby' => 'modified', + 'order' => 'ASC', + 'post_status' => 'any', // 'any' includes 'trash'. + 'date_query' => array( + array( + 'column' => 'post_modified_gmt', + 'before' => \gmdate( 'Y-m-d', \time() - $older_than ), + ), + ), + ); + + return ( new \WP_Query() )->query( $args ); + } + + /** + * Convert a custom post type input to an Activitypub\Activity\Actor. + * + * @param int|\WP_Post $post The post ID or object. + * + * @return Actor|\WP_Error The actor object or WP_Error on failure. + */ + public static function get_actor( $post ) { + $post = \get_post( $post ); + + if ( ! $post ) { + return new \WP_Error( + 'activitypub_actor_not_found', + \__( 'Actor not found', 'activitypub' ), + array( 'status' => 404 ) + ); + } + + $json = $post->post_content; + + if ( empty( $json ) ) { + $json = \get_post_meta( $post->ID, '_activitypub_actor_json', true ); + } + + $actor = Actor::init_from_json( $json ); + + if ( \is_wp_error( $actor ) ) { + self::add_error( $post->ID, $actor ); + } + + return $actor; + } + + /** + * Prepare actor object for insert or update as a custom post type. + * + * @param Actor $actor The actor data. + * + * @return array|\WP_Error Array of post arguments or WP_Error on failure. + */ + private static function prepare_custom_post_type( $actor ) { + if ( ! $actor instanceof Actor ) { + return new \WP_Error( + 'activitypub_invalid_actor_data', + \__( 'Invalid actor data', 'activitypub' ), + array( 'status' => 400 ) + ); + } + + if ( ! empty( $actor->get_endpoints()['sharedInbox'] ) ) { + $inbox = $actor->get_endpoints()['sharedInbox']; + } elseif ( ! empty( $actor->get_inbox() ) ) { + $inbox = $actor->get_inbox(); + } else { + return new \WP_Error( + 'activitypub_invalid_actor_data', + \__( 'Invalid actor data', 'activitypub' ), + array( 'status' => 400 ) + ); + } + + return array( + 'guid' => \esc_url_raw( $actor->get_id() ), + 'post_title' => \wp_strip_all_tags( \wp_slash( $actor->get_name() ?? $actor->get_preferred_username() ) ), + 'post_author' => 0, + 'post_type' => self::POST_TYPE, + 'post_content' => \wp_slash( $actor->to_json() ), + 'post_excerpt' => \wp_kses( \wp_slash( $actor->get_summary() ), 'user_description' ), + 'post_status' => 'publish', + 'meta_input' => array( + '_activitypub_inbox' => $inbox, + ), + ); + } + + /** + * Return the public key for a given actor. + * + * @param int $user_id The WordPress User ID. + * @param bool $force Optional. Force the generation of a new key pair. Default false. + * + * @return string The public key. + */ + public static function get_public_key( $user_id, $force = false ) { + if ( $force ) { + self::generate_key_pair( $user_id ); + } + + $key_pair = self::get_keypair( $user_id ); + + return $key_pair['public_key']; + } + + /** + * Return the private key for a given actor. + * + * @param int $user_id The WordPress User ID. + * @param bool $force Optional. Force the generation of a new key pair. Default false. + * + * @return string The private key. + */ + public static function get_private_key( $user_id, $force = false ) { + if ( $force ) { + self::generate_key_pair( $user_id ); + } + + $key_pair = self::get_keypair( $user_id ); + + return $key_pair['private_key']; + } + + /** + * Return the key pair for a given actor. + * + * @param int $user_id The WordPress User ID. + * + * @return array The key pair. + */ + public static function get_keypair( $user_id ) { + $option_key = self::get_signature_options_key( $user_id ); + $key_pair = \get_option( $option_key ); + + if ( ! $key_pair ) { + $key_pair = self::generate_key_pair( $user_id ); + } + + return $key_pair; + } + + /** + * Get public key from key_id. + * + * @param string $key_id The URL to the public key. + * + * @return resource|\WP_Error The public key resource or WP_Error. + */ + public static function get_remote_key( $key_id ) { + $actor = get_remote_metadata_by_actor( strip_fragment_from_url( $key_id ) ); + if ( \is_wp_error( $actor ) ) { + return new \WP_Error( 'activitypub_no_remote_profile_found', 'No Profile found or Profile not accessible', array( 'status' => 401 ) ); + } + + if ( isset( $actor['publicKey']['publicKeyPem'] ) ) { + $key_resource = \openssl_pkey_get_public( \rtrim( $actor['publicKey']['publicKeyPem'] ) ); + if ( $key_resource ) { + return $key_resource; + } + } + + return new \WP_Error( 'activitypub_no_remote_key_found', 'No Public-Key found', array( 'status' => 401 ) ); + } + + /** + * Generates the pair of keys. + * + * @param int $user_id The WordPress User ID. + * + * @return array The key pair. + */ + protected static function generate_key_pair( $user_id ) { + $option_key = self::get_signature_options_key( $user_id ); + $key_pair = self::check_legacy_key_pair( $user_id ); + + if ( $key_pair ) { + \add_option( $option_key, $key_pair ); + + return $key_pair; + } + + $config = array( + 'digest_alg' => 'sha512', + 'private_key_bits' => 2048, + 'private_key_type' => \OPENSSL_KEYTYPE_RSA, + ); + + $key = \openssl_pkey_new( $config ); + $private_key = null; + $detail = array(); + if ( $key ) { + \openssl_pkey_export( $key, $private_key ); + + $detail = \openssl_pkey_get_details( $key ); + } + + // Check if keys are valid. + if ( + empty( $private_key ) || ! is_string( $private_key ) || + ! isset( $detail['key'] ) || ! is_string( $detail['key'] ) + ) { + return array( + 'private_key' => null, + 'public_key' => null, + ); + } + + $key_pair = array( + 'private_key' => $private_key, + 'public_key' => $detail['key'], + ); + + // Persist keys. + \add_option( $option_key, $key_pair ); + + return $key_pair; + } + + /** + * Return the option key for a given user. + * + * @param int $user_id The WordPress User ID. + * + * @return string The option key. + */ + protected static function get_signature_options_key( $user_id ) { + if ( $user_id > 0 ) { + $user = \get_userdata( $user_id ); + // Sanitize username because it could include spaces and special chars. + $user_id = \sanitize_title( $user->user_login ); + } + + return 'activitypub_keypair_for_' . $user_id; + } + + /** + * Check if there is a legacy key pair + * + * @param int $user_id The WordPress User ID. + * + * @return array|bool The key pair or false. + */ + protected static function check_legacy_key_pair( $user_id ) { + switch ( $user_id ) { + case 0: + $public_key = \get_option( 'activitypub_blog_user_public_key' ); + $private_key = \get_option( 'activitypub_blog_user_private_key' ); + break; + case -1: + $public_key = \get_option( 'activitypub_application_user_public_key' ); + $private_key = \get_option( 'activitypub_application_user_private_key' ); + break; + default: + $public_key = \get_user_meta( $user_id, 'magic_sig_public_key', true ); + $private_key = \get_user_meta( $user_id, 'magic_sig_private_key', true ); + break; + } + + if ( ! empty( $public_key ) && is_string( $public_key ) && ! empty( $private_key ) && is_string( $private_key ) ) { + return array( + 'private_key' => $private_key, + 'public_key' => $public_key, + ); + } + + return false; + } } diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 5c3733901..7cd8ce515 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -7,10 +7,6 @@ namespace Activitypub\Collection; -use Activitypub\Model\Follower; -use WP_Error; -use WP_Query; - use function Activitypub\is_tombstone; use function Activitypub\get_remote_metadata_by_actor; @@ -21,16 +17,27 @@ * @author Matthias Pfefferle */ class Followers { - const POST_TYPE = 'ap_follower'; + /** + * Cache key for the followers inbox. + * + * @var string + */ const CACHE_KEY_INBOXES = 'follower_inboxes_%s'; + /** + * Meta key for the followers user ID. + * + * @var string + */ + const FOLLOWER_META_KEY = '_activitypub_following'; + /** * Add new Follower. * * @param int $user_id The ID of the WordPress User. * @param string $actor The Actor URL. * - * @return Follower|WP_Error The Follower (WP_Post array) or an WP_Error. + * @return int|\WP_Error The Follower ID or an WP_Error. */ public static function add_follower( $user_id, $actor ) { $meta = get_remote_metadata_by_actor( $actor ); @@ -39,57 +46,73 @@ public static function add_follower( $user_id, $actor ) { return $meta; } - if ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) { - return new WP_Error( 'activitypub_invalid_follower', __( 'Invalid Follower', 'activitypub' ), array( 'status' => 400 ) ); + if ( empty( $meta ) || ! \is_array( $meta ) || \is_wp_error( $meta ) ) { + return new \WP_Error( 'activitypub_invalid_follower', __( 'Invalid Follower', 'activitypub' ), array( 'status' => 400 ) ); } - $follower = new Follower(); - $follower->from_array( $meta ); - - $id = $follower->upsert(); + $post_id = Actors::upsert( $meta ); + if ( \is_wp_error( $post_id ) ) { + return $post_id; + } - if ( is_wp_error( $id ) ) { - return $id; + $post_meta = \get_post_meta( $post_id, self::FOLLOWER_META_KEY, false ); + if ( \is_array( $post_meta ) && ! \in_array( (string) $user_id, $post_meta, true ) ) { + \add_post_meta( $post_id, self::FOLLOWER_META_KEY, $user_id ); + \wp_cache_delete( \sprintf( self::CACHE_KEY_INBOXES, $user_id ), 'activitypub' ); } - $post_meta = get_post_meta( $id, '_activitypub_user_id', false ); + return $post_id; + } + + /** + * Remove a Follower. + * + * @param int $post_id The ID of the remote Actor. + * @param int $user_id The ID of the WordPress User. + * + * @return bool True on success, false on failure. + */ + public static function remove( $post_id, $user_id ) { + $post = \get_post( $post_id ); - // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict - if ( is_array( $post_meta ) && ! in_array( $user_id, $post_meta ) ) { - add_post_meta( $id, '_activitypub_user_id', $user_id ); - wp_cache_delete( sprintf( self::CACHE_KEY_INBOXES, $user_id ), 'activitypub' ); + if ( ! $post ) { + return false; } - return $follower; + \wp_cache_delete( \sprintf( self::CACHE_KEY_INBOXES, $user_id ), 'activitypub' ); + + /** + * Fires before a Follower is removed. + * + * @param \WP_Post $post The remote Actor object. + * @param int $user_id The ID of the WordPress User. + * @param \Activitypub\Activity\Actor $actor The remote Actor object. + */ + \do_action( 'activitypub_followers_pre_remove_follower', $post, $user_id, Actors::get_actor( $post ) ); + + return \delete_post_meta( $post_id, self::FOLLOWER_META_KEY, $user_id ); } /** * Remove a Follower. * + * @deprecated Use Activitypub\Collection\Followers::remove instead. + * * @param int $user_id The ID of the WordPress User. * @param string $actor The Actor URL. * * @return bool True on success, false on failure. */ public static function remove_follower( $user_id, $actor ) { - wp_cache_delete( sprintf( self::CACHE_KEY_INBOXES, $user_id ), 'activitypub' ); + _deprecated_function( __METHOD__, 'unreleased', 'Activitypub\Collection\Followers::remove' ); - $follower = self::get_follower( $user_id, $actor ); + $remote_actor = self::get_follower( $user_id, $actor ); - if ( ! $follower ) { + if ( \is_wp_error( $remote_actor ) ) { return false; } - /** - * Fires before a Follower is removed. - * - * @param Follower $follower The Follower object. - * @param int $user_id The ID of the WordPress User. - * @param string $actor The Actor URL. - */ - do_action( 'activitypub_followers_pre_remove_follower', $follower, $user_id, $actor ); - - return delete_post_meta( $follower->get__id(), '_activitypub_user_id', $user_id ); + return self::remove( $remote_actor->ID, $user_id ); } /** @@ -98,29 +121,33 @@ public static function remove_follower( $user_id, $actor ) { * @param int $user_id The ID of the WordPress User. * @param string $actor The Actor URL. * - * @return Follower|false|null The Follower object or null + * @return \WP_Post|\WP_Error The Follower object or WP_Error on failure. */ public static function get_follower( $user_id, $actor ) { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery - $post_id = $wpdb->get_var( + $id = $wpdb->get_var( $wpdb->prepare( - "SELECT DISTINCT p.ID FROM $wpdb->posts p INNER JOIN $wpdb->postmeta pm ON p.ID = pm.post_id WHERE p.post_type = %s AND pm.meta_key = '_activitypub_user_id' AND pm.meta_value = %d AND p.guid = %s", + "SELECT DISTINCT p.ID FROM $wpdb->posts p INNER JOIN $wpdb->postmeta pm ON p.ID = pm.post_id WHERE p.post_type = %s AND pm.meta_key = %s AND pm.meta_value = %d AND p.guid = %s", array( - esc_sql( self::POST_TYPE ), - esc_sql( $user_id ), - esc_sql( $actor ), + \esc_sql( Actors::POST_TYPE ), + \esc_sql( self::FOLLOWER_META_KEY ), + \esc_sql( $user_id ), + \esc_sql( $actor ), ) ) ); - if ( $post_id ) { - $post = get_post( $post_id ); - return Follower::init_from_cpt( $post ); + if ( ! $id ) { + return new \WP_Error( + 'activitypub_follower_not_found', + \__( 'Follower not found', 'activitypub' ), + array( 'status' => 404 ) + ); } - return null; + return \get_post( $id ); } /** @@ -128,25 +155,12 @@ public static function get_follower( $user_id, $actor ) { * * @param string $actor The Actor URL. * - * @return Follower|false|null The Follower object or false on failure. + * @return \WP_Post|\WP_Error The Follower object or WP_Error on failure. */ public static function get_follower_by_actor( $actor ) { - global $wpdb; + _deprecated_function( __METHOD__, '7.0.0', 'Activitypub\Collection\Actors::get_remote_by_uri' ); - // phpcs:ignore WordPress.DB.DirectDatabaseQuery - $post_id = $wpdb->get_var( - $wpdb->prepare( - "SELECT ID FROM $wpdb->posts WHERE guid=%s", - esc_sql( $actor ) - ) - ); - - if ( $post_id ) { - $post = get_post( $post_id ); - return Follower::init_from_cpt( $post ); - } - - return null; + return Actors::get_remote_by_uri( $actor ); } /** @@ -157,10 +171,11 @@ public static function get_follower_by_actor( $actor ) { * @param int $page Page number. * @param array $args The WP_Query arguments. * - * @return Follower[] List of `Follower` objects. + * @return \WP_Post[] List of `Follower` objects. */ public static function get_followers( $user_id, $number = -1, $page = null, $args = array() ) { $data = self::get_followers_with_count( $user_id, $number, $page, $args ); + return $data['followers']; } @@ -175,19 +190,25 @@ public static function get_followers( $user_id, $number = -1, $page = null, $arg * @return array { * Data about the followers. * - * @type Follower[] $followers List of `Follower` objects. + * @type \WP_Post[] $followers List of `Follower` objects. * @type int $total Total number of followers. * } */ public static function get_followers_with_count( $user_id, $number = -1, $page = null, $args = array() ) { $defaults = array( - 'post_type' => self::POST_TYPE, + 'post_type' => Actors::POST_TYPE, 'posts_per_page' => $number, 'paged' => $page, 'orderby' => 'ID', 'order' => 'DESC', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'meta_query' => array( + 'relation' => 'OR', + array( + 'key' => self::FOLLOWER_META_KEY, + 'value' => $user_id, + ), + // for backwards compatibility. array( 'key' => '_activitypub_user_id', 'value' => $user_id, @@ -195,37 +216,12 @@ public static function get_followers_with_count( $user_id, $number = -1, $page = ), ); - $args = wp_parse_args( $args, $defaults ); - $query = new WP_Query( $args ); + $args = \wp_parse_args( $args, $defaults ); + $query = new \WP_Query( $args ); $total = $query->found_posts; - $followers = array_map( array( Follower::class, 'init_from_cpt' ), $query->get_posts() ); - $followers = array_filter( $followers ); + $followers = \array_filter( $query->posts ); - return compact( 'followers', 'total' ); - } - - /** - * Get all Followers. - * - * @return Follower[] The Term list of Followers. - */ - public static function get_all_followers() { - $args = array( - 'nopaging' => true, - // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query - 'meta_query' => array( - 'relation' => 'AND', - array( - 'key' => '_activitypub_inbox', - 'compare' => 'EXISTS', - ), - array( - 'key' => '_activitypub_actor_json', - 'compare' => 'EXISTS', - ), - ), - ); - return self::get_followers( null, null, null, $args ); + return \compact( 'followers', 'total' ); } /** @@ -236,30 +232,7 @@ public static function get_all_followers() { * @return int The number of Followers */ public static function count_followers( $user_id ) { - $query = new WP_Query( - array( - 'post_type' => self::POST_TYPE, - 'fields' => 'ids', - // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query - 'meta_query' => array( - 'relation' => 'AND', - array( - 'key' => '_activitypub_user_id', - 'value' => $user_id, - ), - array( - 'key' => '_activitypub_inbox', - 'compare' => 'EXISTS', - ), - array( - 'key' => '_activitypub_actor_json', - 'compare' => 'EXISTS', - ), - ), - ) - ); - - return $query->found_posts; + return self::get_followers_with_count( $user_id )['total']; } /** @@ -270,18 +243,18 @@ public static function count_followers( $user_id ) { * @return array The list of Inboxes. */ public static function get_inboxes( $user_id ) { - $cache_key = sprintf( self::CACHE_KEY_INBOXES, $user_id ); - $inboxes = wp_cache_get( $cache_key, 'activitypub' ); + $cache_key = \sprintf( self::CACHE_KEY_INBOXES, $user_id ); + $inboxes = \wp_cache_get( $cache_key, 'activitypub' ); if ( $inboxes ) { return $inboxes; } // Get all Followers of an ID of the WordPress User. - $posts = new WP_Query( + $posts = new \WP_Query( array( 'nopaging' => true, - 'post_type' => self::POST_TYPE, + 'post_type' => Actors::POST_TYPE, 'fields' => 'ids', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'meta_query' => array( @@ -291,7 +264,7 @@ public static function get_inboxes( $user_id ) { 'compare' => 'EXISTS', ), array( - 'key' => '_activitypub_user_id', + 'key' => self::FOLLOWER_META_KEY, 'value' => $user_id, ), array( @@ -303,9 +276,7 @@ public static function get_inboxes( $user_id ) { ) ); - $posts = $posts->get_posts(); - - if ( ! $posts ) { + if ( ! $posts->posts ) { return array(); } @@ -314,15 +285,15 @@ public static function get_inboxes( $user_id ) { $results = $wpdb->get_col( $wpdb->prepare( "SELECT DISTINCT meta_value FROM {$wpdb->postmeta} - WHERE post_id IN (" . implode( ', ', array_fill( 0, count( $posts ), '%d' ) ) . ") + WHERE post_id IN (" . \implode( ', ', \array_fill( 0, \absint( $posts->post_count ), '%d' ) ) . ") AND meta_key = '_activitypub_inbox' AND meta_value IS NOT NULL", - $posts + $posts->posts ) ); - $inboxes = array_filter( $results ); - wp_cache_set( $cache_key, $inboxes, 'activitypub' ); + $inboxes = \array_filter( $results ); + \wp_cache_set( $cache_key, $inboxes, 'activitypub' ); return $inboxes; } @@ -341,14 +312,14 @@ public static function get_inboxes_for_activity( $json, $actor_id, $batch_size = $inboxes = self::get_inboxes( $actor_id ); if ( self::maybe_add_inboxes_of_blog_user( $json, $actor_id ) ) { - $inboxes = array_fill_keys( $inboxes, 1 ); + $inboxes = \array_fill_keys( $inboxes, 1 ); foreach ( self::get_inboxes( Actors::BLOG_USER_ID ) as $inbox ) { $inboxes[ $inbox ] = 1; } - $inboxes = array_keys( $inboxes ); + $inboxes = \array_keys( $inboxes ); } - return array_slice( $inboxes, $offset, $batch_size ); + return \array_slice( $inboxes, $offset, $batch_size ); } /** @@ -368,9 +339,9 @@ public static function maybe_add_inboxes_of_blog_user( $json, $actor_id ) { return false; } - $activity = json_decode( $json, true ); + $activity = \json_decode( $json, true ); // Only if this is an Update or Delete. Create handles its own "Announce" in dual user mode. - if ( ! in_array( $activity['type'] ?? null, array( 'Update', 'Delete' ), true ) ) { + if ( ! \in_array( $activity['type'] ?? null, array( 'Update', 'Delete' ), true ) ) { return false; } @@ -378,77 +349,58 @@ public static function maybe_add_inboxes_of_blog_user( $json, $actor_id ) { } /** - * Get all Followers that have not been updated for a given time. + * Get all Followers. * - * @param int $number Optional. Limits the result. Default 50. - * @param int $older_than Optional. The time in seconds. Default 86400 (1 day). + * @deprecated unreleased Use Activitypub\Collection\Actors::get_all() instead. * - * @return Follower[] The Term list of Followers. + * @return \WP_Post[] The list of Followers. */ - public static function get_outdated_followers( $number = 50, $older_than = 86400 ) { + public static function get_all_followers() { + _deprecated_function( __METHOD__, 'unreleased', 'Activitypub\Collection\Actors::get_all' ); + $args = array( - 'post_type' => self::POST_TYPE, - 'posts_per_page' => $number, - 'orderby' => 'modified', - 'order' => 'ASC', - 'post_status' => 'any', // 'any' includes 'trash'. - 'date_query' => array( + 'nopaging' => true, + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + 'meta_query' => array( + 'relation' => 'AND', array( - 'column' => 'post_modified_gmt', - 'before' => gmdate( 'Y-m-d', \time() - $older_than ), + 'key' => '_activitypub_inbox', + 'compare' => 'EXISTS', ), ), ); + return self::get_followers( null, null, null, $args ); + } - $posts = new WP_Query( $args ); - $items = array_map( array( Follower::class, 'init_from_cpt' ), $posts->get_posts() ); + /** + * Get all Followers that have not been updated for a given time. + * + * @deprecated 7.0.0 Use Activitypub\Collection\Actors::get_outdated() instead. + * + * @param int $number Optional. Limits the result. Default 50. + * @param int $older_than Optional. The time in seconds. Default 86400 (1 day). + * + * @return \WP_Post[] The list of Actors. + */ + public static function get_outdated_followers( $number = 50, $older_than = 86400 ) { + _deprecated_function( __METHOD__, '7.0.0', 'Activitypub\Collection\Actors::get_outdated' ); - return array_filter( $items ); + return Actors::get_outdated( $number, $older_than ); } /** * Get all Followers that had errors. * + * @deprecated 7.0.0 Use Activitypub\Collection\Actors::get_faulty() instead. + * * @param int $number Optional. The number of Followers to return. Default 20. * - * @return Follower[] The Term list of Followers. + * @return \WP_Post[] The list of Actors. */ public static function get_faulty_followers( $number = 20 ) { - $args = array( - 'post_type' => self::POST_TYPE, - 'posts_per_page' => $number, - // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query - 'meta_query' => array( - 'relation' => 'OR', - array( - 'key' => '_activitypub_errors', - 'compare' => 'EXISTS', - ), - array( - 'key' => '_activitypub_inbox', - 'compare' => 'NOT EXISTS', - ), - array( - 'key' => '_activitypub_actor_json', - 'compare' => 'NOT EXISTS', - ), - array( - 'key' => '_activitypub_inbox', - 'value' => '', - 'compare' => '=', - ), - array( - 'key' => '_activitypub_actor_json', - 'value' => '', - 'compare' => '=', - ), - ), - ); + _deprecated_function( __METHOD__, '7.0.0', 'Activitypub\Collection\Actors::get_faulty' ); - $posts = new WP_Query( $args ); - $items = array_map( array( Follower::class, 'init_from_cpt' ), $posts->get_posts() ); - - return array_filter( $items ); + return Actors::get_faulty( $number ); } /** @@ -457,38 +409,46 @@ public static function get_faulty_followers( $number = 20 ) { * * The error will be stored in post meta. * + * @deprecated 7.0.0 Use Activitypub\Collection\Actors::add_error() instead. + * * @param int $post_id The ID of the WordPress Custom-Post-Type. * @param mixed $error The error message. Can be a string or a WP_Error. * * @return int|false The meta ID on success, false on failure. */ public static function add_error( $post_id, $error ) { - if ( is_string( $error ) ) { - $error_message = $error; - } elseif ( is_wp_error( $error ) ) { - $error_message = $error->get_error_message(); - } else { - $error_message = __( - 'Unknown Error or misconfigured Error-Message', - 'activitypub' - ); - } + _deprecated_function( __METHOD__, '7.0.0', 'Activitypub\Collection\Actors::add_error' ); - return add_post_meta( - $post_id, - '_activitypub_errors', - $error_message - ); + return Actors::add_error( $post_id, $error ); } /** * Clear the errors for a Follower. * + * @deprecated 7.0.0 Use Activitypub\Collection\Actors::clear_errors() instead. + * * @param int $post_id The ID of the WordPress Custom-Post-Type. * * @return bool True on success, false on failure. */ public static function clear_errors( $post_id ) { - return \delete_post_meta( $post_id, '_activitypub_errors' ); + _deprecated_function( __METHOD__, '7.0.0', 'Activitypub\Collection\Actors::clear_errors' ); + + return Actors::clear_errors( $post_id ); + } + + /** + * Check the status of a given following. + * + * @param int $post_id The ID of the Post. + * @param int $user_id The ID of the WordPress User. + * + * @return bool The status of the following. + */ + public static function follows( $post_id, $user_id ) { + $all_meta = \get_post_meta( $post_id ); + $following = $all_meta[ self::FOLLOWER_META_KEY ] ?? array(); + + return \in_array( (string) $user_id, $following, true ); } } diff --git a/includes/collection/class-following.php b/includes/collection/class-following.php new file mode 100644 index 000000000..29074386e --- /dev/null +++ b/includes/collection/class-following.php @@ -0,0 +1,435 @@ +ID ); + $following = $all_meta[ self::FOLLOWING_META_KEY ] ?? array(); + $pending = $all_meta[ self::PENDING_META_KEY ] ?? array(); + + if ( ! \in_array( (string) $user_id, $following, true ) && ! \in_array( (string) $user_id, $pending, true ) ) { + $actor = Actors::get_by_id( $user_id ); + + if ( \is_wp_error( $actor ) ) { + return $actor; + } + + \add_post_meta( $post->ID, self::PENDING_META_KEY, (string) $user_id ); + + $follow = new Activity(); + $follow->set_type( 'Follow' ); + $follow->set_actor( $actor->get_id() ); + $follow->set_object( $post->guid ); + $follow->set_to( array( $post->guid ) ); + + return add_to_outbox( $follow, null, $user_id ); + } + + return $post; + } + + /** + * Accept a follow request. + * + * @param \WP_Post|int $post The ID of the remote Actor. + * @param int $user_id The ID of the WordPress User. + * + * @return \WP_Post|\WP_Error The ID of the Actor or a WP_Error. + */ + public static function accept( $post, $user_id ) { + $post = \get_post( $post ); + + if ( ! $post ) { + return new \WP_Error( 'activitypub_remote_actor_not_found', 'Remote actor not found' ); + } + + $following = \get_post_meta( $post->ID, self::PENDING_META_KEY, false ); + + if ( ! \is_array( $following ) || ! \in_array( (string) $user_id, $following, true ) ) { + return new \WP_Error( 'activitypub_following_not_found', 'Follow request not found' ); + } + + \add_post_meta( $post->ID, self::FOLLOWING_META_KEY, $user_id ); + \delete_post_meta( $post->ID, self::PENDING_META_KEY, $user_id ); + + return $post; + } + + /** + * Reject a follow request. + * + * @param \WP_Post|int $post The ID of the remote Actor. + * @param int $user_id The ID of the WordPress User. + * + * @return \WP_Post|\WP_Error The ID of the Actor or a WP_Error. + */ + public static function reject( $post, $user_id ) { + $post = \get_post( $post ); + + if ( ! $post ) { + return new \WP_Error( 'activitypub_remote_actor_not_found', 'Remote actor not found' ); + } + + \delete_post_meta( $post->ID, self::PENDING_META_KEY, $user_id ); + \delete_post_meta( $post->ID, self::FOLLOWING_META_KEY, $user_id ); + + return $post; + } + + /** + * Remove a follow request. + * + * Please do not use this method directly, use `Activitypub\unfollow` instead. + * + * @see Activitypub\unfollow + * + * @param \WP_Post|int $post The ID of the remote Actor. + * @param int $user_id The ID of the WordPress User. + * + * @return \WP_Post|\WP_Error The Actor post or a WP_Error. + */ + public static function unfollow( $post, $user_id ) { + $post = \get_post( $post ); + + if ( ! $post ) { + return new \WP_Error( 'activitypub_remote_actor_not_found', __( 'Remote actor not found', 'activitypub' ) ); + } + + $actor_type = Actors::get_type_by_id( $user_id ); + + \delete_post_meta( $post->ID, self::FOLLOWING_META_KEY, $user_id ); + \delete_post_meta( $post->ID, self::PENDING_META_KEY, $user_id ); + + // Get Post-ID of the Follow Outbox Activity. + $post_id_query = new \WP_Query( + array( + 'post_type' => Outbox::POST_TYPE, + 'nopaging' => true, + 'posts_per_page' => 1, + 'author' => \max( $user_id, 0 ), + 'fields' => 'ids', + 'number' => 1, + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + 'meta_query' => array( + array( + 'key' => '_activitypub_object_id', + 'value' => $post->guid, + ), + array( + 'key' => '_activitypub_activity_type', + 'value' => 'Follow', + ), + array( + 'key' => '_activitypub_activity_actor', + 'value' => $actor_type, + ), + ), + ) + ); + + if ( $post_id_query->posts ) { + Outbox::undo( $post_id_query->posts[0] ); + } + + return $post; + } + + /** + * Get the Followings of a given user, along with a total count for pagination purposes. + * + * @param int|null $user_id The ID of the WordPress User. + * @param int $number Maximum number of results to return. + * @param int $page Page number. + * @param array $args The WP_Query arguments. + * + * @return array { + * Data about the followings. + * + * @type \WP_Post[] $followings List of `Following` objects. + * @type int $total Total number of followings. + * } + */ + public static function get_following_with_count( $user_id, $number = -1, $page = null, $args = array() ) { + $defaults = array( + 'post_type' => Actors::POST_TYPE, + 'posts_per_page' => $number, + 'paged' => $page, + 'orderby' => 'ID', + 'order' => 'DESC', + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + 'meta_query' => array( + array( + 'key' => self::FOLLOWING_META_KEY, + 'value' => $user_id, + ), + ), + ); + + $args = \wp_parse_args( $args, $defaults ); + $query = new \WP_Query( $args ); + $total = $query->found_posts; + $following = \array_filter( $query->posts ); + + return \compact( 'following', 'total' ); + } + + /** + * Get the Followings of a given user. + * + * @param int|null $user_id The ID of the WordPress User. + * @param int $number Maximum number of results to return. + * @param int $page Page number. + * @param array $args The WP_Query arguments. + * + * @return \WP_Post[] List of `Following` objects. + */ + public static function get_following( $user_id, $number = -1, $page = null, $args = array() ) { + $data = self::get_following_with_count( $user_id, $number, $page, $args ); + + return $data['following']; + } + + /** + * Get the total number of followings of a given user. + * + * @param int|null $user_id The ID of the WordPress User. + * + * @return int The total number of followings. + */ + public static function count_following( $user_id ) { + return self::get_following_with_count( $user_id, -1, null, array() )['total']; + } + + /** + * Get the Followings of a given user, along with a total count for pagination purposes. + * + * @param int|null $user_id The ID of the WordPress User. + * @param int $number Maximum number of results to return. + * @param int $page Page number. + * @param array $args The WP_Query arguments. + * + * @return array { + * Data about the followings. + * + * @type \WP_Post[] $followings List of `Following` objects. + * @type int $total Total number of followings. + * } + */ + public static function get_pending_with_count( $user_id, $number = -1, $page = null, $args = array() ) { + $defaults = array( + 'post_type' => Actors::POST_TYPE, + 'posts_per_page' => $number, + 'paged' => $page, + 'orderby' => 'ID', + 'order' => 'DESC', + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + 'meta_query' => array( + array( + 'key' => self::PENDING_META_KEY, + 'value' => $user_id, + ), + ), + ); + + $args = \wp_parse_args( $args, $defaults ); + $query = new \WP_Query( $args ); + $total = $query->found_posts; + $following = \array_filter( $query->posts ); + + return \compact( 'following', 'total' ); + } + + /** + * Get the pending followings of a given user. + * + * @param int|null $user_id The ID of the WordPress User. + * @param int $number Maximum number of results to return. + * @param int $page Page number. + * @param array $args The WP_Query arguments. + * + * @return \WP_Post[] List of `Following` objects. + */ + public static function get_pending( $user_id, $number = -1, $page = null, $args = array() ) { + $data = self::get_pending_with_count( $user_id, $number, $page, $args ); + + return $data['following']; + } + + /** + * Get the total number of pending followings of a given user. + * + * @param int|null $user_id The ID of the WordPress User. + * + * @return int The total number of pending followings. + */ + public static function count_pending( $user_id ) { + return self::get_pending_with_count( $user_id, -1, null, array() )['total']; + } + + /** + * Get all followings of a given user. + * + * @param int|null $user_id The ID of the WordPress User. + * @param int $number Maximum number of results to return. + * @param int $page Page number. + * @param array $args The WP_Query arguments. + * + * @return \WP_Post[] List of `Following` objects. + */ + public static function get_all_with_count( $user_id, $number = -1, $page = null, $args = array() ) { + $defaults = array( + 'post_type' => Actors::POST_TYPE, + 'posts_per_page' => $number, + 'paged' => $page, + 'orderby' => 'ID', + 'order' => 'DESC', + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + 'meta_query' => array( + 'relation' => 'OR', + array( + 'key' => self::FOLLOWING_META_KEY, + 'value' => $user_id, + ), + array( + 'key' => self::PENDING_META_KEY, + 'value' => $user_id, + ), + ), + ); + + $args = \wp_parse_args( $args, $defaults ); + $query = new \WP_Query( $args ); + $total = $query->found_posts; + $following = \array_filter( $query->posts ); + + return \compact( 'following', 'total' ); + } + + /** + * Get all followings of a given user. + * + * @param int|null $user_id The ID of the WordPress User. + * + * @return \WP_Post[] List of `Following` objects. + */ + public static function get_all( $user_id ) { + return self::get_all_with_count( $user_id, -1, null, array() )['following']; + } + + /** + * Get the total number of all followings of a given user. + * + * @param int|null $user_id The ID of the WordPress User. + * + * @return int The total number of all followings. + */ + public static function count_all( $user_id ) { + return self::get_all_with_count( $user_id, -1, null, array() )['total']; + } + + /** + * Get the total number of followings of a given user. + * + * @param int|null $user_id The ID of the WordPress User. + * + * @return array Total number of followings and pending followings. + */ + public static function count( $user_id ) { + return array( + self::ALL => self::get_all_with_count( $user_id, -1, null, array() )['total'], + self::ACCEPTED => self::get_following_with_count( $user_id, -1, null, array() )['total'], + self::PENDING => self::get_pending_with_count( $user_id, -1, null, array() )['total'], + ); + } + + /** + * Check the status of a given following. + * + * @param int $user_id The ID of the WordPress User. + * @param int $post_id The ID of the Post. + * + * @return string|false The status of the following. + */ + public static function check_status( $user_id, $post_id ) { + $all_meta = get_post_meta( $post_id ); + $following = $all_meta[ self::FOLLOWING_META_KEY ] ?? array(); + $pending = $all_meta[ self::PENDING_META_KEY ] ?? array(); + + if ( \in_array( (string) $user_id, $following, true ) ) { + return self::ACCEPTED; + } + + if ( \in_array( (string) $user_id, $pending, true ) ) { + return self::PENDING; + } + + return false; + } +} diff --git a/includes/collection/class-interactions.php b/includes/collection/class-interactions.php index 968c63e1d..9847d5423 100644 --- a/includes/collection/class-interactions.php +++ b/includes/collection/class-interactions.php @@ -89,15 +89,9 @@ public static function update_comment( $activity ) { * * @param array $activity Activity array. * - * @return array|false Comment data or `false` on failure. + * @return array|false Comment data or `false` on failure. */ public static function add_reaction( $activity ) { - $comment_data = self::activity_to_comment( $activity ); - - if ( ! $comment_data ) { - return false; - } - $url = object_to_uri( $activity['object'] ); $comment_post_id = \url_to_postid( $url ); $parent_comment_id = url_to_commentid( $url ); @@ -113,16 +107,19 @@ public static function add_reaction( $activity ) { } $comment_type = Comment::get_comment_type_by_activity_type( $activity['type'] ); - if ( ! $comment_type ) { // Not a valid comment type. return false; } - $comment_content = $comment_type['excerpt']; + $comment_data = self::activity_to_comment( $activity ); + if ( ! $comment_data ) { + return false; + } $comment_data['comment_post_ID'] = $comment_post_id; - $comment_data['comment_content'] = \esc_html( $comment_content ); + $comment_data['comment_parent'] = $parent_comment_id ? $parent_comment_id : 0; + $comment_data['comment_content'] = \esc_html( $comment_type['excerpt'] ); $comment_data['comment_type'] = \esc_attr( $comment_type['type'] ); $comment_data['comment_meta']['source_id'] = \esc_url_raw( $activity['id'] ); @@ -238,20 +235,19 @@ public static function activity_to_comment( $activity ) { } // Check Actor-Name. - if ( isset( $actor['name'] ) ) { + $comment_author = null; + if ( ! empty( $actor['name'] ) ) { $comment_author = $actor['name']; - } elseif ( isset( $actor['preferredUsername'] ) ) { + } elseif ( ! empty( $actor['preferredUsername'] ) ) { $comment_author = $actor['preferredUsername']; - } else { + } + + if ( empty( $comment_author ) && \get_option( 'require_name_email' ) ) { return false; } $url = object_to_uri( $actor['url'] ?? $actor['id'] ); - if ( ! $url ) { - $url = object_to_uri( $actor['id'] ); - } - if ( isset( $activity['object']['content'] ) ) { $comment_content = \addslashes( $activity['object']['content'] ); } @@ -264,7 +260,7 @@ public static function activity_to_comment( $activity ) { } $comment_data = array( - 'comment_author' => \esc_attr( $comment_author ), + 'comment_author' => $comment_author ?? __( 'Anonymous', 'activitypub' ), 'comment_author_url' => \esc_url_raw( $url ), 'comment_content' => $comment_content, 'comment_type' => 'comment', diff --git a/includes/collection/class-outbox.php b/includes/collection/class-outbox.php index a5546ecde..d27190d64 100644 --- a/includes/collection/class-outbox.php +++ b/includes/collection/class-outbox.php @@ -7,8 +7,8 @@ namespace Activitypub\Collection; -use Activitypub\Dispatcher; use Activitypub\Scheduler; +use Activitypub\Webfinger; use Activitypub\Activity\Activity; use Activitypub\Activity\Base_Object; @@ -40,6 +40,17 @@ public static function add( Activity $activity, $user_id, $visibility = ACTIVITY $activity->set_actor( Actors::get_by_id( $user_id )->get_id() ); } + if ( ! \filter_var( $object_id, FILTER_VALIDATE_URL ) ) { + $object_id = Webfinger::resolve( $object_id ); + } + + if ( \is_wp_error( $object_id ) ) { + return $object_id; + } + + // Save activity in the context of an activitypub request. + \add_filter( 'activitypub_is_activitypub_request', '__return_true' ); + $outbox_item = array( 'post_type' => self::POST_TYPE, 'post_title' => sprintf( @@ -60,6 +71,8 @@ public static function add( Activity $activity, $user_id, $visibility = ACTIVITY ), ); + \remove_filter( 'activitypub_is_activitypub_request', '__return_true' ); + $has_kses = false !== \has_filter( 'content_save_pre', 'wp_filter_post_kses' ); if ( $has_kses ) { // Prevent KSES from corrupting JSON in post_content. @@ -150,12 +163,16 @@ private static function invalidate_existing_items( $object_id, $activity_type, $ * * @param int|\WP_Post $outbox_item The Outbox post or post ID. * - * @return int|bool The ID of the outbox item or false on failure. + * @return int|bool|\WP_Error The ID of the outbox item or false on failure. */ public static function undo( $outbox_item ) { - $outbox_item = get_post( $outbox_item ); + $outbox_item = \get_post( $outbox_item ); $activity = self::get_activity( $outbox_item ); + if ( \is_wp_error( $activity ) ) { + return $activity; + } + $type = 'Undo'; if ( 'Create' === $activity->get_type() ) { $type = 'Delete'; @@ -166,6 +183,35 @@ public static function undo( $outbox_item ) { return add_to_outbox( $activity, $type, $outbox_item->post_author ); } + /** + * Get an outbox item by its GUID. + * + * @param string $guid The GUID of the outbox item. + * + * @return \WP_Post|\WP_Error The outbox item or WP_Error. + */ + public static function get_by_guid( $guid ) { + global $wpdb; + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + $post_id = $wpdb->get_var( + $wpdb->prepare( + "SELECT ID FROM $wpdb->posts WHERE guid=%s AND post_type=%s", + \esc_url( $guid ), + self::POST_TYPE + ) + ); + + if ( ! $post_id ) { + return new \WP_Error( + 'activitypub_outbox_item_not_found', + \__( 'Outbox item not found', 'activitypub' ), + array( 'status' => 404 ) + ); + } + + return \get_post( $post_id ); + } + /** * Reschedule an activity. * @@ -193,7 +239,7 @@ public static function reschedule( $outbox_item ) { * @return Activity|\WP_Error The Activity object or WP_Error. */ public static function get_activity( $outbox_item ) { - $outbox_item = get_post( $outbox_item ); + $outbox_item = \get_post( $outbox_item ); $actor = self::get_actor( $outbox_item ); if ( is_wp_error( $actor ) ) { return $actor; diff --git a/includes/collection/class-users.php b/includes/collection/class-users.php deleted file mode 100644 index 121949031..000000000 --- a/includes/collection/class-users.php +++ /dev/null @@ -1,78 +0,0 @@ - 400 ) @@ -508,11 +491,15 @@ function site_supports_blocks() { /** * Check if data is valid JSON. * + * @deprecated unreleased Use {@see \json_decode} instead. + * * @param string $data The data to check. * * @return boolean True if the data is JSON, false otherwise. */ function is_json( $data ) { + \_deprecated_function( __FUNCTION__, 'unreleased', 'json_decode' ); + return \is_array( \json_decode( $data, true ) ); } @@ -1208,16 +1195,6 @@ function generate_post_summary( $post, $length = 500 ) { return ''; } - $content = \sanitize_post_field( 'post_excerpt', $post->post_excerpt, $post->ID ); - - if ( $content ) { - /** This filter is documented in wp-includes/post-template.php */ - return \apply_filters( 'the_excerpt', $content ); - } - - $content = \sanitize_post_field( 'post_content', $post->post_content, $post->ID ); - $content_parts = \get_extended( $content ); - /** * Filters the excerpt more value. * @@ -1226,15 +1203,26 @@ function generate_post_summary( $post, $length = 500 ) { $excerpt_more = \apply_filters( 'activitypub_excerpt_more', '[…]' ); $length = $length - strlen( $excerpt_more ); - // Check for the tag. - if ( - ! empty( $content_parts['extended'] ) && - ! empty( $content_parts['main'] ) - ) { - $content = $content_parts['main'] . ' ' . $excerpt_more; - $length = null; + $content = \sanitize_post_field( 'post_excerpt', $post->post_excerpt, $post->ID ); + + if ( $content ) { + // Ignore length if excerpt is set. + $length = null; + } else { + $content = \sanitize_post_field( 'post_content', $post->post_content, $post->ID ); + $content_parts = \get_extended( $content ); + + // Check for the tag. + if ( + ! empty( $content_parts['extended'] ) && + ! empty( $content_parts['main'] ) + ) { + $content = \trim( $content_parts['main'] ) . ' ' . $excerpt_more; + $length = null; + } } + $content = \strip_shortcodes( $content ); $content = \html_entity_decode( $content ); $content = \wp_strip_all_tags( $content ); $content = \trim( $content ); @@ -1247,12 +1235,8 @@ function generate_post_summary( $post, $length = 500 ) { $content = $content[0] . ' ' . $excerpt_more; } - /* - Removed until this is merged: https://github.com/mastodon/mastodon/pull/28629 - /** This filter is documented in wp-includes/post-template.php + // This filter is documented in wp-includes/post-template.php. return \apply_filters( 'the_excerpt', $content ); - */ - return $content; } /** @@ -1286,7 +1270,7 @@ function get_content_warning( $post_id ) { function get_user_id( $id ) { $user = Actors::get_by_id( $id ); - if ( ! $user ) { + if ( \is_wp_error( $user ) ) { return false; } @@ -1508,12 +1492,34 @@ function add_to_outbox( $data, $activity_type = null, $user_id = 0, $content_vis } if ( ! $activity || \is_wp_error( $activity ) ) { + /** + * Action triggered when adding an object to the outbox fails. + * + * @param \WP_Error $activity The error object or false. + * @param mixed $data The object that failed to be added to the outbox. + * @param string|null $activity_type The type of the Activity or null if `$data` is an Activity. + * @param int $user_id The User ID. + * @param string $content_visibility The visibility of the content. See `constants.php` for possible values: `ACTIVITYPUB_CONTENT_VISIBILITY_*`. + */ + \do_action( 'activitypub_add_to_outbox_failed', $activity, $data, $activity_type, $user_id, $content_visibility ); + return false; } $outbox_activity_id = Outbox::add( $activity, $user_id, $content_visibility ); - if ( ! $outbox_activity_id ) { + if ( ! $outbox_activity_id || \is_wp_error( $outbox_activity_id ) ) { + /** + * Action triggered when adding an object to the outbox fails. + * + * @param false|\WP_Error $outbox_activity_id The error object or false. + * @param mixed $data The object that failed to be added to the outbox. + * @param string|null $activity_type The type of the Activity or null if `$data` is an Activity. + * @param int $user_id The User ID. + * @param string $content_visibility The visibility of the content. See `constants.php` for possible values: `ACTIVITYPUB_CONTENT_VISIBILITY_*`. + */ + \do_action( 'activitypub_add_to_outbox_failed', $outbox_activity_id, $data, $activity_type, $user_id, $content_visibility ); + return false; } @@ -1532,6 +1538,66 @@ function add_to_outbox( $data, $activity_type = null, $user_id = 0, $content_vis return $outbox_activity_id; } +/** + * Follow a user. + * + * @param string|int $remote_actor The Actor URL, WebFinger Resource or Post-ID of the remote Actor. + * @param int $user_id The ID of the WordPress User. + * + * @return \WP_Post|\WP_Error The ID of the Outbox item or a WP_Error. + */ +function follow( $remote_actor, $user_id ) { + if ( \is_numeric( $remote_actor ) ) { + return Following::follow( $remote_actor, $user_id ); + } + + if ( ! \filter_var( $remote_actor, FILTER_VALIDATE_URL ) ) { + $remote_actor = Webfinger::resolve( $remote_actor ); + } + + if ( \is_wp_error( $remote_actor ) ) { + return $remote_actor; + } + + $remote_actor_post = Actors::fetch_remote_by_uri( $remote_actor ); + + if ( \is_wp_error( $remote_actor_post ) ) { + return $remote_actor_post; + } + + return Following::follow( $remote_actor_post, $user_id ); +} + +/** + * Unfollow a user. + * + * @param string|int $remote_actor The Actor URL, WebFinger Resource or Post-ID of the remote Actor. + * @param int $user_id The ID of the WordPress User. + * + * @return \WP_Post|\WP_Error The ID of the Outbox item or a WP_Error. + */ +function unfollow( $remote_actor, $user_id ) { + if ( \is_numeric( $remote_actor ) ) { + return Following::unfollow( $remote_actor, $user_id ); + } + + if ( ! \filter_var( $remote_actor, FILTER_VALIDATE_URL ) ) { + $remote_actor = Webfinger::resolve( $remote_actor ); + } + + if ( \is_wp_error( $remote_actor ) ) { + return $remote_actor; + } + + $remote_actor_post = Actors::fetch_remote_by_uri( $remote_actor ); + + if ( \is_wp_error( $remote_actor_post ) ) { + return $remote_actor_post; + } + + return Following::unfollow( $remote_actor_post, $user_id ); +} + /** * Check if an `$data` is an Activity. * @@ -1627,3 +1693,44 @@ function _is_type_of( $data, $types ) { function get_embed_html( $url, $inline_css = true ) { return Embed::get_html( $url, $inline_css ); } + +/** + * Infer a shortname from the Actor ID or URL. Used only for fallbacks, + * we will try to use what's supplied. + * + * @param string $uri The URI. + * + * @return string Hopefully the name of the Follower. + */ +function extract_name_from_uri( $uri ) { + $name = $uri; + + if ( \filter_var( $name, FILTER_VALIDATE_URL ) ) { + $name = \rtrim( $name, '/' ); + $path = \wp_parse_url( $name, PHP_URL_PATH ); + if ( $path ) { + if ( \strpos( $name, '@' ) !== false ) { + // Expected: https://example.com/@user (default URL pattern). + $name = \preg_replace( '|^/@?|', '', $path ); + } else { + // Expected: https://example.com/users/user (default ID pattern). + $parts = \explode( '/', $path ); + $name = \array_pop( $parts ); + } + } + } elseif ( + \is_email( $name ) || + \strpos( $name, 'acct' ) === 0 || + \strpos( $name, '@' ) === 0 + ) { + // Expected: user@example.com or acct:user@example (WebFinger). + $name = \ltrim( $name, '@' ); + if ( str_starts_with( $name, 'acct:' ) ) { + $name = \substr( $name, 5 ); + } + $parts = \explode( '@', $name ); + $name = $parts[0]; + } + + return $name; +} diff --git a/includes/handler/class-accept.php b/includes/handler/class-accept.php new file mode 100644 index 000000000..c3b090994 --- /dev/null +++ b/includes/handler/class-accept.php @@ -0,0 +1,120 @@ +ID, '_activitypub_activity_type', true ) + ) { + return; + } + + $actor_post = Actors::get_remote_by_uri( object_to_uri( $accept['object']['object'] ) ); + + if ( \is_wp_error( $actor_post ) ) { + return; + } + + Following::accept( $actor_post, $user_id ); + + // Send notification. + $notification = new Notification( + 'accept', + $actor_post->guid, + $accept, + $user_id + ); + $notification->send(); + } + + /** + * Validate the object. + * + * @param bool $valid The validation state. + * @param string $param The object parameter. + * @param \WP_REST_Request $request The request object. + * + * @return bool The validation state: true if valid, false if not. + */ + public static function validate_object( $valid, $param, $request ) { + $json_params = $request->get_json_params(); + + if ( empty( $json_params['type'] ) ) { + return false; + } + + if ( + 'Accept' !== $json_params['type'] || + \is_wp_error( $request ) + ) { + return $valid; + } + + $required_attributes = array( + 'actor', + 'object', + ); + + if ( ! empty( \array_diff( $required_attributes, \array_keys( $json_params ) ) ) ) { + return false; + } + + $required_object_attributes = array( + 'id', + 'type', + 'actor', + 'object', + ); + + if ( ! empty( \array_diff( $required_object_attributes, \array_keys( $json_params['object'] ) ) ) ) { + return false; + } + + return $valid; + } +} diff --git a/includes/handler/class-delete.php b/includes/handler/class-delete.php index 1aed89523..8f04d5cd1 100644 --- a/includes/handler/class-delete.php +++ b/includes/handler/class-delete.php @@ -8,7 +8,7 @@ namespace Activitypub\Handler; use Activitypub\Http; -use Activitypub\Collection\Followers; +use Activitypub\Collection\Actors; use Activitypub\Collection\Interactions; use function Activitypub\object_to_uri; @@ -101,11 +101,11 @@ public static function handle_delete( $activity ) { * @param array $activity The delete activity. */ public static function maybe_delete_follower( $activity ) { - $follower = Followers::get_follower_by_actor( $activity['actor'] ); + $follower = Actors::get_remote_by_uri( $activity['actor'] ); // Verify that Actor is deleted. - if ( $follower && Http::is_tombstone( $activity['actor'] ) ) { - $follower->delete(); + if ( ! is_wp_error( $follower ) && Http::is_tombstone( $activity['actor'] ) ) { + Actors::delete( $follower->ID ); self::maybe_delete_interactions( $activity ); } } diff --git a/includes/handler/class-follow.php b/includes/handler/class-follow.php index a19422113..ec38afe0b 100644 --- a/includes/handler/class-follow.php +++ b/includes/handler/class-follow.php @@ -51,25 +51,31 @@ public static function handle_follow( $activity ) { $user_id = $user->get__id(); // Save follower. - $follower = Followers::add_follower( + $remote_actor = Followers::add_follower( $user_id, $activity['actor'] ); + if ( \is_wp_error( $remote_actor ) ) { + return $remote_actor; + } + + $remote_actor = \get_post( $remote_actor ); + /** * Fires after a new follower has been added. * - * @param string $actor The URL of the actor (follower) who initiated the follow. - * @param array $activity The complete activity data of the follow request. - * @param int $user_id The ID of the WordPress user being followed. - * @param \Activitypub\Model\Follower|\WP_Error $follower The Follower object containing the new follower's data. + * @param string $actor The URL of the actor (follower) who initiated the follow. + * @param array $activity The complete activity data of the follow request. + * @param int $user_id The ID of the WordPress user being followed. + * @param \WP_Post|\WP_Error $remote_actor The Actor object containing the new follower's data. */ - do_action( 'activitypub_followers_post_follow', $activity['actor'], $activity, $user_id, $follower ); + do_action( 'activitypub_followers_post_follow', $activity['actor'], $activity, $user_id, $remote_actor ); // Send notification. $notification = new Notification( 'follow', - $activity['actor'], + $remote_actor->guid, $activity, $user_id ); @@ -79,13 +85,13 @@ public static function handle_follow( $activity ) { /** * Send Accept response. * - * @param string $actor The Actor URL. - * @param array $activity_object The Activity object. - * @param int $user_id The ID of the WordPress User. - * @param \Activitypub\Model\Follower|\WP_Error $follower The Follower object. + * @param string $actor The Actor URL. + * @param array $activity_object The Activity object. + * @param int $user_id The ID of the WordPress User. + * @param \WP_Post|\WP_Error $remote_actor The Actor object. */ - public static function queue_accept( $actor, $activity_object, $user_id, $follower ) { - if ( \is_wp_error( $follower ) ) { + public static function queue_accept( $actor, $activity_object, $user_id, $remote_actor ) { + if ( \is_wp_error( $remote_actor ) ) { // Impossible to send a "Reject" because we can not get the Remote-Inbox. return; } diff --git a/includes/handler/class-move.php b/includes/handler/class-move.php index 71032a457..b990f67a6 100644 --- a/includes/handler/class-move.php +++ b/includes/handler/class-move.php @@ -8,6 +8,7 @@ namespace Activitypub\Handler; use Activitypub\Http; +use Activitypub\Collection\Actors; use Activitypub\Collection\Followers; use function Activitypub\object_to_uri; @@ -34,30 +35,30 @@ public static function init() { * @param array $activity The JSON "Move" Activity. */ public static function handle_move( $activity ) { - $target = self::extract_target( $activity ); - $origin = self::extract_origin( $activity ); + $target_uri = self::extract_target( $activity ); + $origin_uri = self::extract_origin( $activity ); - if ( ! $target || ! $origin ) { + if ( ! $target_uri || ! $origin_uri ) { return; } - $target_object = Http::get_remote_object( $target ); - $origin_object = Http::get_remote_object( $origin ); + $target_json = Http::get_remote_object( $target_uri ); + $origin_json = Http::get_remote_object( $origin_uri ); - $verified = self::verify_move( $target_object, $origin_object ); + $verified = self::verify_move( $target_json, $origin_json ); if ( ! $verified ) { return; } - $target_follower = Followers::get_follower_by_actor( $target ); - $origin_follower = Followers::get_follower_by_actor( $origin ); + $target_object = Actors::get_remote_by_uri( $target_uri ); + $origin_object = Actors::get_remote_by_uri( $origin_uri ); /* * If the new target is followed, but the origin is not, * everything is fine, so we can return. */ - if ( $target_follower && ! $origin_follower ) { + if ( ! \is_wp_error( $target_object ) && \is_wp_error( $origin_object ) ) { return; } @@ -65,21 +66,20 @@ public static function handle_move( $activity ) { * If the new target is not followed, but the origin is, * update the origin follower to the new target. */ - if ( ! $target_follower && $origin_follower ) { - $origin_follower->from_array( $target_object ); - $origin_follower->set_id( $target ); - $origin_id = $origin_follower->upsert(); - + if ( \is_wp_error( $target_object ) && ! \is_wp_error( $origin_object ) ) { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $wpdb->update( $wpdb->posts, - array( 'guid' => sanitize_url( $target ) ), - array( 'ID' => sanitize_key( $origin_id ) ) + array( 'guid' => sanitize_url( $target_uri ) ), + array( 'ID' => sanitize_key( $origin_object->ID ) ) ); // Clear the cache. - wp_cache_delete( $origin_id, 'posts' ); + \wp_cache_delete( $origin_object->ID, 'posts' ); + + Actors::upsert( $target_json ); + return; } @@ -87,18 +87,18 @@ public static function handle_move( $activity ) { * If the new target is followed, and the origin is followed, * move users and delete the origin follower. */ - if ( $target_follower && $origin_follower ) { - $origin_users = \get_post_meta( $origin_follower->get__id(), '_activitypub_user_id', false ); - $target_users = \get_post_meta( $target_follower->get__id(), '_activitypub_user_id', false ); + if ( ! \is_wp_error( $target_object ) && ! \is_wp_error( $origin_object ) ) { + $origin_users = \get_post_meta( $origin_object->ID, Followers::FOLLOWER_META_KEY, false ); + $target_users = \get_post_meta( $target_object->ID, Followers::FOLLOWER_META_KEY, false ); // Get all user ids from $origin_users that are not in $target_users. $users = \array_diff( $origin_users, $target_users ); foreach ( $users as $user_id ) { - \add_post_meta( $target_follower->get__id(), '_activitypub_user_id', $user_id ); + \add_post_meta( $target_object->ID, Followers::FOLLOWER_META_KEY, $user_id ); } - $origin_follower->delete(); + \wp_delete_post( $origin_object->ID ); } } diff --git a/includes/handler/class-reject.php b/includes/handler/class-reject.php new file mode 100644 index 000000000..478e0516b --- /dev/null +++ b/includes/handler/class-reject.php @@ -0,0 +1,119 @@ +ID, '_activitypub_activity_type', true ) ) { + case 'Follow': + self::reject_follow( $reject, $user_id ); + break; + default: + break; + } + } + + /** + * Reject a "Follow" request. + * + * @param array $reject The activity-object. + * @param int $user_id The id of the local blog-user. + */ + private static function reject_follow( $reject, $user_id ) { + $actor_uri = $reject['object']['actor'] ?? ''; + $actor_post = Actors::get_remote_by_uri( object_to_uri( $actor_uri ) ); + + if ( \is_wp_error( $actor_post ) ) { + return; + } + + Following::reject( $actor_post, $user_id ); + + // Send notification. + $notification = new Notification( + 'reject', + $actor_post->guid, + $reject, + $user_id + ); + $notification->send(); + } + + /** + * Validate the object. + * + * @param bool $valid The validation state. + * @param string $param The object parameter. + * @param \WP_REST_Request $request The request object. + * + * @return bool The validation state: true if valid, false if not. + */ + public static function validate_object( $valid, $param, $request ) { + $json_params = $request->get_json_params(); + + if ( empty( $json_params['type'] ) ) { + return false; + } + + if ( + 'Reject' !== $json_params['type'] || + \is_wp_error( $request ) + ) { + return $valid; + } + + if ( empty( $json_params['actor'] ) || empty( $json_params['object'] ) ) { + return false; + } + + return $valid; + } +} diff --git a/includes/handler/class-update.php b/includes/handler/class-update.php index 035bfed78..68fdcc41d 100644 --- a/includes/handler/class-update.php +++ b/includes/handler/class-update.php @@ -7,7 +7,7 @@ namespace Activitypub\Handler; -use Activitypub\Collection\Followers; +use Activitypub\Collection\Actors; use Activitypub\Collection\Interactions; use function Activitypub\get_remote_metadata_by_actor; @@ -113,13 +113,6 @@ public static function update_actor( $activity ) { return; } - $follower = Followers::get_follower_by_actor( $actor['id'] ); - - if ( ! $follower ) { - return; - } - - $follower->from_array( $actor ); - $follower->upsert(); + Actors::upsert( $actor ); } } diff --git a/includes/model/class-application.php b/includes/model/class-application.php index 0309941ed..12a7563c1 100644 --- a/includes/model/class-application.php +++ b/includes/model/class-application.php @@ -7,8 +7,6 @@ namespace Activitypub\Model; -use WP_Query; -use Activitypub\Signature; use Activitypub\Activity\Actor; use Activitypub\Collection\Actors; @@ -48,11 +46,18 @@ class Application extends Actor { protected $indexable = false; /** - * The WebFinger Resource. + * List of software capabilities implemented by the Application. * - * @var string + * @see https://codeberg.org/silverpill/feps/src/branch/main/844e/fep-844e.md + * + * @var array */ - protected $webfinger; + protected $implements = array( + array( + 'href' => 'https://datatracker.ietf.org/doc/html/rfc9421', + 'name' => 'RFC-9421: HTTP Message Signatures', + ), + ); /** * Returns the type of the object. @@ -173,7 +178,7 @@ public function get_header_image() { * @return string The published date. */ public function get_published() { - $first_post = new WP_Query( + $first_post = new \WP_Query( array( 'orderby' => 'date', 'order' => 'ASC', @@ -226,7 +231,7 @@ public function get_public_key() { return array( 'id' => $this->get_id() . '#main-key', 'owner' => $this->get_id(), - 'publicKeyPem' => Signature::get_public_key_for( Actors::APPLICATION_USER_ID ), + 'publicKeyPem' => Actors::get_public_key( Actors::APPLICATION_USER_ID ), ); } diff --git a/includes/model/class-blog.php b/includes/model/class-blog.php index e46e5b025..7e2fc1418 100644 --- a/includes/model/class-blog.php +++ b/includes/model/class-blog.php @@ -10,8 +10,6 @@ use Activitypub\Activity\Actor; use Activitypub\Collection\Actors; use Activitypub\Collection\Extra_Fields; -use Activitypub\Signature; -use WP_Query; use function Activitypub\esc_hashtag; use function Activitypub\is_single_user; @@ -25,29 +23,6 @@ * @method int get__id() Gets the internal user ID for the blog (always returns BLOG_USER_ID). */ class Blog extends Actor { - /** - * The Featured-Posts. - * - * @see https://docs.joinmastodon.org/spec/activitypub/#featured - * - * @context { - * "@id": "http://joinmastodon.org/ns#featured", - * "@type": "@id" - * } - * - * @var string - */ - protected $featured; - - /** - * Moderators endpoint. - * - * @see https://join-lemmy.org/docs/contributors/05-federation.html - * - * @var string - */ - protected $moderators; - /** * The User-ID * @@ -56,40 +31,20 @@ class Blog extends Actor { protected $_id = Actors::BLOG_USER_ID; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore /** - * If the User is indexable. - * - * @context http://joinmastodon.org/ns#indexable - * - * @var boolean - */ - protected $indexable; - - /** - * The WebFinger Resource. + * The generator of the object. * - * @var string - */ - protected $webfinger; - - /** - * Whether the User is discoverable. - * - * @see https://docs.joinmastodon.org/spec/activitypub/#discoverable - * - * @context http://joinmastodon.org/ns#discoverable - * - * @var boolean - */ - protected $discoverable; - - /** - * Restrict posting to mods. + * @see https://www.w3.org/TR/activitypub/#generator + * @see https://codeberg.org/fediverse/fep/src/branch/main/fep/844e/fep-844e.md#discovery-through-an-actor * - * @see https://join-lemmy.org/docs/contributors/05-federation.html - * - * @var boolean + * @var array */ - protected $posting_restricted_to_mods; + protected $generator = array( + 'type' => 'Application', + 'implements' => array( + 'href' => 'https://datatracker.ietf.org/doc/html/rfc9421', + 'name' => 'RFC-9421: HTTP Message Signatures', + ), + ); /** * Constructor. @@ -314,7 +269,7 @@ public function get_image() { * @return string The published date. */ public function get_published() { - $first_post = new WP_Query( + $first_post = new \WP_Query( array( 'orderby' => 'date', 'order' => 'ASC', @@ -375,7 +330,7 @@ public function get_public_key() { return array( 'id' => $this->get_id() . '#main-key', 'owner' => $this->get_id(), - 'publicKeyPem' => Signature::get_public_key_for( $this->get__id() ), + 'publicKeyPem' => Actors::get_public_key( $this->get__id() ), ); } @@ -463,6 +418,15 @@ public function get_featured() { return get_rest_url_by_path( sprintf( 'actors/%d/collections/featured', $this->get__id() ) ); } + /** + * Returns the Featured-Tags-API-Endpoint. + * + * @return string The Featured-Tags-Endpoint. + */ + public function get_featured_tags() { + return get_rest_url_by_path( sprintf( 'actors/%d/collections/tags', $this->get__id() ) ); + } + /** * Returns whether the site is indexable. * diff --git a/includes/model/class-follower.php b/includes/model/class-follower.php index cec0600d5..89b21fe7f 100644 --- a/includes/model/class-follower.php +++ b/includes/model/class-follower.php @@ -7,10 +7,12 @@ namespace Activitypub\Model; -use WP_Error; use Activitypub\Activity\Actor; +use Activitypub\Collection\Actors; use Activitypub\Collection\Followers; +use function Activitypub\extract_name_from_uri; + /** * ActivityPub Follower Class. * @@ -42,13 +44,22 @@ class Follower extends Actor { */ protected $_id; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore + /** + * Constructor. + * + * @deprecated Use Actor instead. + */ + public function __construct() { + \_deprecated_class( __CLASS__, 'unreleased', Actor::class ); + } + /** * Get the errors. * * @return mixed */ public function get_errors() { - return get_post_meta( $this->_id, '_activitypub_errors', false ); + return Actors::get_errors( $this->_id ); } /** @@ -57,13 +68,7 @@ public function get_errors() { * @return bool True on success, false on failure. */ public function clear_errors() { - if ( ! $this->_id ) { - \_doing_it_wrong( __METHOD__, 'Follower ID is not set.', 'unreleased' ); - - return false; - } - - return Followers::clear_errors( $this->_id ); + return Actors::clear_errors( $this->_id ); } /** @@ -97,9 +102,11 @@ public function get_url() { /** * Reset (delete) all errors. + * + * @return bool True on success, false on failure. */ public function reset_errors() { - delete_post_meta( $this->_id, '_activitypub_errors' ); + return Actors::clear_errors( $this->_id ); } /** @@ -108,13 +115,7 @@ public function reset_errors() { * @return int The number of errors. */ public function count_errors() { - $errors = $this->get_errors(); - - if ( is_array( $errors ) && ! empty( $errors ) ) { - return count( $errors ); - } - - return 0; + return Actors::count_errors( $this->_id ); } /** @@ -125,8 +126,8 @@ public function count_errors() { public function get_latest_error_message() { $errors = $this->get_errors(); - if ( is_array( $errors ) && ! empty( $errors ) ) { - return reset( $errors ); + if ( \is_array( $errors ) && ! empty( $errors ) ) { + return \reset( $errors ); } return ''; @@ -166,61 +167,26 @@ public function is_valid() { /** * Save the current Follower object. * - * @return int|WP_Error The post ID or an WP_Error. + * @return int|\WP_Error The post ID or an WP_Error. */ public function save() { if ( ! $this->is_valid() ) { - return new WP_Error( 'activitypub_invalid_follower', __( 'Invalid Follower', 'activitypub' ), array( 'status' => 400 ) ); + return new \WP_Error( 'activitypub_invalid_follower', __( 'Invalid Follower', 'activitypub' ), array( 'status' => 400 ) ); } - if ( ! $this->get__id() ) { - global $wpdb; - - // phpcs:ignore WordPress.DB.DirectDatabaseQuery - $post_id = $wpdb->get_var( - $wpdb->prepare( - "SELECT ID FROM $wpdb->posts WHERE guid=%s", - esc_sql( $this->get_id() ) - ) - ); - - if ( $post_id ) { - $post = get_post( $post_id ); - $this->set__id( $post->ID ); - } - } - - $post_id = $this->get__id(); - - $args = array( - 'ID' => $post_id, - 'guid' => esc_url_raw( $this->get_id() ), - 'post_title' => wp_strip_all_tags( sanitize_text_field( $this->get_name() ) ), - 'post_author' => 0, - 'post_type' => Followers::POST_TYPE, - 'post_name' => esc_url_raw( $this->get_id() ), - 'post_excerpt' => sanitize_text_field( wp_kses( $this->get_summary(), 'user_description' ) ), - 'post_status' => 'publish', - 'meta_input' => $this->get_post_meta_input(), - ); - - if ( ! empty( $post_id ) ) { - // If this is an update, prevent the "followed" date from being overwritten by the current date. - $post = get_post( $post_id ); - $args['post_date'] = $post->post_date; - $args['post_date_gmt'] = $post->post_date_gmt; + $id = Actors::upsert( $this ); + if ( \is_wp_error( $id ) ) { + return $id; } - $post_id = wp_insert_post( $args ); - $this->_id = $post_id; - - return $post_id; + $this->set__id( $id ); + return $id; } /** * Upsert the current Follower object. * - * @return int|WP_Error The post ID or an WP_Error. + * @return int|\WP_Error The post ID or an WP_Error. */ public function upsert() { return $this->save(); @@ -236,18 +202,7 @@ public function upsert() { * @see \Activitypub\Rest\Followers::remove_follower() */ public function delete() { - wp_delete_post( $this->_id ); - } - - /** - * Update the post meta. - */ - protected function get_post_meta_input() { - $meta_input = array(); - $meta_input['_activitypub_inbox'] = $this->get_shared_inbox(); - $meta_input['_activitypub_actor_json'] = wp_slash( $this->to_json() ); - - return $meta_input; + Followers::remove_follower( $this->_id, $this->get_id() ); } /** @@ -313,7 +268,7 @@ public function get_icon_url() { return ''; } - if ( is_array( $icon ) ) { + if ( \is_array( $icon ) ) { return $icon['url']; } @@ -332,7 +287,7 @@ public function get_image_url() { return ''; } - if ( is_array( $image ) ) { + if ( \is_array( $image ) ) { return $image['url']; } @@ -361,12 +316,16 @@ public function get_shared_inbox() { * @return Follower|false The Follower object or false on failure. */ public static function init_from_cpt( $post ) { - $actor_json = get_post_meta( $post->ID, '_activitypub_actor_json', true ); + if ( empty( $post->post_content ) ) { + $json = \get_post_meta( $post->ID, '_activitypub_actor_json', true ); + } else { + $json = $post->post_content; + } /* @var Follower $object Follower object. */ - $object = self::init_from_json( $actor_json ); + $object = self::init_from_json( $json ); - if ( is_wp_error( $object ) ) { + if ( \is_wp_error( $object ) ) { return false; } @@ -389,39 +348,11 @@ public static function init_from_cpt( $post ) { protected function extract_name_from_uri() { // prefer the URL, but fall back to the ID. if ( $this->url ) { - $name = $this->url; + $uri = $this->url; } else { - $name = $this->id; - } - - if ( \filter_var( $name, FILTER_VALIDATE_URL ) ) { - $name = \rtrim( $name, '/' ); - $path = \wp_parse_url( $name, PHP_URL_PATH ); - - if ( $path ) { - if ( \strpos( $name, '@' ) !== false ) { - // Expected: https://example.com/@user (default URL pattern). - $name = \preg_replace( '|^/@?|', '', $path ); - } else { - // Expected: https://example.com/users/user (default ID pattern). - $parts = \explode( '/', $path ); - $name = \array_pop( $parts ); - } - } - } elseif ( - \is_email( $name ) || - \strpos( $name, 'acct' ) === 0 || - \strpos( $name, '@' ) === 0 - ) { - // Expected: user@example.com or acct:user@example (WebFinger). - $name = \ltrim( $name, '@' ); - if ( str_starts_with( $name, 'acct:' ) ) { - $name = \substr( $name, 5 ); - } - $parts = \explode( '@', $name ); - $name = $parts[0]; + $uri = $this->id; } - return $name; + return extract_name_from_uri( $uri ); } } diff --git a/includes/model/class-user.php b/includes/model/class-user.php index db37928b9..0facb74d3 100644 --- a/includes/model/class-user.php +++ b/includes/model/class-user.php @@ -8,8 +8,8 @@ namespace Activitypub\Model; use Activitypub\Activity\Actor; +use Activitypub\Collection\Actors; use Activitypub\Collection\Extra_Fields; -use Activitypub\Signature; use function Activitypub\is_blog_public; use function Activitypub\get_rest_url_by_path; @@ -29,20 +29,6 @@ class User extends Actor { */ protected $_id; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore - /** - * The Featured-Posts. - * - * @see https://docs.joinmastodon.org/spec/activitypub/#featured - * - * @context { - * "@id": "http://joinmastodon.org/ns#featured", - * "@type": "@id" - * } - * - * @var string - */ - protected $featured; - /** * Whether the User is discoverable. * @@ -55,20 +41,20 @@ class User extends Actor { protected $discoverable = true; /** - * Whether the User is indexable. + * The generator of the object. * - * @context http://joinmastodon.org/ns#indexable - * - * @var boolean - */ - protected $indexable; - - /** - * The WebFinger Resource. + * @see https://www.w3.org/TR/activitypub/#generator + * @see https://codeberg.org/fediverse/fep/src/branch/main/fep/844e/fep-844e.md#discovery-through-an-actor * - * @var string + * @var array */ - protected $webfinger; + protected $generator = array( + 'type' => 'Application', + 'implements' => array( + 'href' => 'https://datatracker.ietf.org/doc/html/rfc9421', + 'name' => 'RFC-9421: HTTP Message Signatures', + ), + ); /** * Constructor. @@ -258,7 +244,7 @@ public function get_public_key() { return array( 'id' => $this->get_id() . '#main-key', 'owner' => $this->get_id(), - 'publicKeyPem' => Signature::get_public_key_for( $this->get__id() ), + 'publicKeyPem' => Actors::get_public_key( $this->get__id() ), ); } @@ -307,6 +293,15 @@ public function get_featured() { return get_rest_url_by_path( sprintf( 'actors/%d/collections/featured', $this->get__id() ) ); } + /** + * Returns the Featured-Tags-API-Endpoint. + * + * @return string The Featured-Tags-Endpoint. + */ + public function get_featured_tags() { + return get_rest_url_by_path( sprintf( 'actors/%d/collections/tags', $this->get__id() ) ); + } + /** * Returns the endpoints. * diff --git a/includes/rest/class-actors-controller.php b/includes/rest/class-actors-controller.php index ba3afc5f2..959fb8ec2 100644 --- a/includes/rest/class-actors-controller.php +++ b/includes/rest/class-actors-controller.php @@ -349,6 +349,28 @@ public function get_item_schema() { 'type' => 'boolean', 'readonly' => true, ), + 'generator' => array( + 'description' => 'The generator of the object.', + 'type' => 'object', + 'properties' => array( + 'type' => array( + 'type' => 'string', + ), + 'implements' => array( + 'type' => 'object', + 'properties' => array( + 'href' => array( + 'type' => 'string', + 'format' => 'uri', + ), + 'name' => array( + 'type' => 'string', + ), + ), + ), + ), + 'readonly' => true, + ), ), ); diff --git a/includes/rest/class-application-controller.php b/includes/rest/class-application-controller.php index ad3c1b80c..f577437c9 100644 --- a/includes/rest/class-application-controller.php +++ b/includes/rest/class-application-controller.php @@ -157,6 +157,21 @@ public function get_item_schema() { 'indexable' => array( 'type' => 'boolean', ), + 'implements' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'href' => array( + 'type' => 'string', + 'format' => 'uri', + ), + 'name' => array( + 'type' => 'string', + ), + ), + ), + ), 'webfinger' => array( 'type' => 'string', ), diff --git a/includes/rest/class-followers-controller.php b/includes/rest/class-followers-controller.php index 1f09ad453..d398391c9 100644 --- a/includes/rest/class-followers-controller.php +++ b/includes/rest/class-followers-controller.php @@ -112,9 +112,9 @@ public function get_items( $request ) { 'orderedItems' => array_map( function ( $item ) use ( $context ) { if ( 'full' === $context ) { - return $item->to_array( false ); + return Actors::get_actor( $item )->to_array( false ); } - return $item->get_id(); + return $item->guid; }, $data['followers'] ), diff --git a/includes/rest/class-following-controller.php b/includes/rest/class-following-controller.php index 19c9aedee..f11d11857 100644 --- a/includes/rest/class-following-controller.php +++ b/includes/rest/class-following-controller.php @@ -8,6 +8,7 @@ namespace Activitypub\Rest; use Activitypub\Collection\Actors; +use Activitypub\Collection\Following; use function Activitypub\get_context; use function Activitypub\is_single_user; @@ -24,13 +25,6 @@ class Following_Controller extends Actors_Controller { use Collection; - /** - * Initialize the class, registering WordPress hooks. - */ - public function __construct() { - \add_filter( 'activitypub_rest_following', array( self::class, 'default_following' ), 10, 2 ); - } - /** * Register routes. */ @@ -65,6 +59,18 @@ public function register_routes() { 'minimum' => 1, 'maximum' => 100, ), + 'order' => array( + 'description' => 'Order sort attribute ascending or descending.', + 'type' => 'string', + 'default' => 'desc', + 'enum' => array( 'asc', 'desc' ), + ), + 'context' => array( + 'description' => 'The context in which the request is made.', + 'type' => 'string', + 'default' => 'simple', + 'enum' => array( 'simple', 'full' ), + ), ), ), 'schema' => array( $this, 'get_public_item_schema' ), @@ -91,24 +97,45 @@ public function get_items( $request ) { */ \do_action( 'activitypub_rest_following_pre' ); + $order = $request->get_param( 'order' ); + $per_page = $request->get_param( 'per_page' ); + $page = $request->get_param( 'page' ) ?? 1; + $context = $request->get_param( 'context' ); + + $data = Following::get_following_with_count( $user_id, $per_page, $page, array( 'order' => \ucwords( $order ) ) ); + $response = array( - '@context' => get_context(), - 'id' => get_rest_url_by_path( \sprintf( 'actors/%d/following', $user->get__id() ) ), - 'generator' => 'https://wordpress.org/?v=' . get_masked_wp_version(), - 'actor' => $user->get_id(), - 'type' => 'OrderedCollection', + '@context' => get_context(), + 'id' => get_rest_url_by_path( \sprintf( 'actors/%d/following', $user->get__id() ) ), + 'generator' => 'https://wordpress.org/?v=' . get_masked_wp_version(), + 'actor' => $user->get_id(), + 'type' => 'OrderedCollection', + 'totalItems' => $data['total'], + 'orderedItems' => array_map( + function ( $item ) use ( $context ) { + if ( 'full' === $context ) { + return Actors::get_actor( $item )->to_array( false ); + } + return $item->guid; + }, + $data['following'] + ), ); /** - * Filter the list of following urls. + * Filter the list of following urls * * @param array $items The array of following urls. * @param \Activitypub\Model\User $user The user object. + * + * @deprecated unreleased Please migrate your Followings to the new internal Following structure. */ - $items = \apply_filters( 'activitypub_rest_following', array(), $user ); + $items = \apply_filters_deprecated( 'activitypub_rest_following', array( array(), $user ), 'unreleased', 'Please migrate your Followings to the new internal Following structure.' ); - $response['totalItems'] = \is_countable( $items ) ? \count( $items ) : 0; - $response['orderedItems'] = $items; + if ( ! empty( $items ) ) { + $response['totalItems'] = count( $items ); + $response['orderedItems'] = $items; + } $response = $this->prepare_collection_response( $response, $request ); if ( is_wp_error( $response ) ) { @@ -121,29 +148,6 @@ public function get_items( $request ) { return $response; } - /** - * Add the Blog Authors to the following list of the Blog Actor - * if Blog not in single mode. - * - * @param array $follow_list The array of following urls. - * @param \Activitypub\Model\User $user The user object. - * - * @return array The array of following urls. - */ - public static function default_following( $follow_list, $user ) { - if ( 0 !== $user->get__id() || is_single_user() ) { - return $follow_list; - } - - $users = Actors::get_collection(); - - foreach ( $users as $user ) { - $follow_list[] = $user->get_id(); - } - - return $follow_list; - } - /** * Retrieves the following schema, conforming to JSON Schema. * @@ -154,9 +158,65 @@ public function get_item_schema() { return $this->add_additional_fields_schema( $this->schema ); } + // Define the schema for items in the following collection. $item_schema = array( - 'type' => 'string', - 'format' => 'uri', + 'oneOf' => array( + array( + 'type' => 'string', + 'format' => 'uri', + ), + array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'type' => 'string', + 'format' => 'uri', + ), + 'type' => array( + 'type' => 'string', + ), + 'name' => array( + 'type' => 'string', + ), + 'icon' => array( + 'type' => 'object', + 'properties' => array( + 'type' => array( + 'type' => 'string', + ), + 'mediaType' => array( + 'type' => 'string', + ), + 'url' => array( + 'type' => 'string', + 'format' => 'uri', + ), + ), + ), + 'published' => array( + 'type' => 'string', + 'format' => 'date-time', + ), + 'summary' => array( + 'type' => 'string', + ), + 'updated' => array( + 'type' => 'string', + 'format' => 'date-time', + ), + 'url' => array( + 'type' => 'string', + 'format' => 'uri', + ), + 'streams' => array( + 'type' => 'array', + ), + 'preferredUsername' => array( + 'type' => 'string', + ), + ), + ), + ), ); $schema = $this->get_collection_schema( $item_schema ); diff --git a/includes/rest/class-interaction-controller.php b/includes/rest/class-interaction-controller.php index f4ba270a4..ef95afe88 100644 --- a/includes/rest/class-interaction-controller.php +++ b/includes/rest/class-interaction-controller.php @@ -41,10 +41,10 @@ public function register_routes() { 'permission_callback' => '__return_true', 'args' => array( 'uri' => array( - 'description' => 'The URI of the object to interact with.', - 'type' => 'string', - 'format' => 'uri', - 'required' => true, + 'description' => 'The URI or webfinger ID of the object to interact with.', + 'type' => 'string', + 'required' => true, + 'sanitize_callback' => array( $this, 'sanitize_uri' ), ), ), ), @@ -52,6 +52,29 @@ public function register_routes() { ); } + /** + * Sanitize the URI parameter. + * + * @param string $uri The URI or webfinger ID of the object to interact with. + * + * @return string Sanitized URI. + */ + public function sanitize_uri( $uri ) { + // Remove "acct:" prefix if present. + if ( str_starts_with( $uri, 'acct:' ) ) { + $uri = \substr( $uri, 5 ); + } + + // Remove "@" prefix if present. + $uri = \ltrim( $uri, '@' ); + + if ( is_email( $uri ) ) { + return \sanitize_text_field( $uri ); + } + + return \sanitize_url( $uri ); + } + /** * Retrieves the interaction URL for a given URI. * @@ -134,6 +157,6 @@ public function get_item( $request ) { ); } - return new \WP_REST_Response( null, 302, array( 'Location' => \esc_url( $redirect_url ) ) ); + return new \WP_REST_Response( null, 302, array( 'Location' => $redirect_url ) ); } } diff --git a/includes/rest/class-post-controller.php b/includes/rest/class-post-controller.php index 459ebbc45..bb7cbe8b8 100644 --- a/includes/rest/class-post-controller.php +++ b/includes/rest/class-post-controller.php @@ -98,6 +98,7 @@ public function get_reactions( $request ) { 'post_id' => $post_id, 'type' => $type_object['type'], 'status' => 'approve', + 'parent' => 0, ) ); @@ -123,9 +124,9 @@ public function get_reactions( $request ) { 'items' => \array_map( function ( $comment ) { return array( - 'name' => $comment->comment_author, + 'name' => html_entity_decode( $comment->comment_author ), 'url' => $comment->comment_author_url, - 'avatar' => \get_comment_meta( $comment->comment_ID, 'avatar_url', true ), + 'avatar' => \get_avatar_url( $comment ), ); }, $comments diff --git a/includes/scheduler/class-post.php b/includes/scheduler/class-post.php index 8ae694f3e..408f5ad17 100644 --- a/includes/scheduler/class-post.php +++ b/includes/scheduler/class-post.php @@ -55,7 +55,11 @@ public static function schedule_post_activity( $post_id, $post, $update, $post_b switch ( $new_status ) { case 'publish': - $type = ( 'publish' === $old_status ) ? 'Update' : 'Create'; + if ( $update ) { + $type = ( 'publish' === $old_status ) ? 'Update' : 'Create'; + } else { + $type = 'Create'; + } break; case 'draft': @@ -75,6 +79,11 @@ public static function schedule_post_activity( $post_id, $post, $update, $post_b return; } + // If the post was not federated before but is an Update activity, it should be a Create activity. + if ( get_wp_object_state( $post ) !== 'federated' && 'Update' === $type ) { + $type = 'Create'; + } + // Add the post to the outbox. add_to_outbox( $post, $type, $post->post_author ); } diff --git a/includes/signature/class-http-message-signature.php b/includes/signature/class-http-message-signature.php new file mode 100644 index 000000000..ec3007fbd --- /dev/null +++ b/includes/signature/class-http-message-signature.php @@ -0,0 +1,456 @@ + array( + 'type' => OPENSSL_KEYTYPE_RSA, + 'algo' => OPENSSL_ALGO_SHA256, + ), + 'rsa-v1_5-sha384' => array( + 'type' => OPENSSL_KEYTYPE_RSA, + 'algo' => OPENSSL_ALGO_SHA384, + ), + 'rsa-v1_5-sha512' => array( + 'type' => OPENSSL_KEYTYPE_RSA, + 'algo' => OPENSSL_ALGO_SHA512, + ), + + // RSA PSS (note: not supported in openssl_verify() until PHP 8.1). + 'rsa-pss-sha256' => array( + 'type' => OPENSSL_KEYTYPE_RSA, + 'algo' => OPENSSL_ALGO_SHA256, + ), + 'rsa-pss-sha384' => array( + 'type' => OPENSSL_KEYTYPE_RSA, + 'algo' => OPENSSL_ALGO_SHA384, + ), + 'rsa-pss-sha512' => array( + 'type' => OPENSSL_KEYTYPE_RSA, + 'algo' => OPENSSL_ALGO_SHA512, + ), + + // ECDSA. + 'ecdsa-p256-sha256' => array( + 'type' => OPENSSL_KEYTYPE_EC, + 'algo' => OPENSSL_ALGO_SHA256, + ), + 'ecdsa-p384-sha384' => array( + 'type' => OPENSSL_KEYTYPE_EC, + 'algo' => OPENSSL_ALGO_SHA384, + ), + 'ecdsa-p521-sha512' => array( + 'type' => OPENSSL_KEYTYPE_EC, + 'algo' => OPENSSL_ALGO_SHA512, + ), + ); + + /** + * Digest algorithms. + * + * @var string[] + */ + private $digest_algorithms = array( + 'sha-256' => 'sha256', + 'sha-512' => 'sha512', + ); + + /** + * Generate RFC-9421 compliant Signature-Input and Signature headers for an outgoing HTTP request. + * + * @param array $args The request arguments. + * @param string $url The request URL. + * + * @return array Request arguments with signature headers. + */ + public function sign( $args, $url ) { + // Standard components to sign. + $components = array( + '"@method"' => \strtoupper( $args['method'] ), + '"@target-uri"' => $url, + '"@authority"' => \wp_parse_url( $url, PHP_URL_HOST ), + ); + $identifiers = \array_keys( $components ); + + // Add digest if provided. + if ( isset( $args['body'] ) ) { + $components['"content-digest"'] = $this->generate_digest( $args['body'] ); + $identifiers = \array_keys( $components ); + + $args['headers']['Content-Digest'] = $components['"content-digest"']; + } + + $params = array( + 'created' => \strtotime( $args['headers']['Date'] ), + 'keyid' => $args['key_id'], + 'alg' => 'rsa-v1_5-sha256', + ); + + // Build the signature base string as per RFC-9421. + $signature_base = $this->get_signature_base_string( $components, $params ); + + $signature = null; + \openssl_sign( $signature_base, $signature, $args['private_key'], \OPENSSL_ALGO_SHA256 ); + $signature = \base64_encode( $signature ); + + $args['headers']['Signature-Input'] = 'wp=(' . \implode( ' ', $identifiers ) . ')' . $this->get_params_string( $params ); + $args['headers']['Signature'] = 'wp=:' . $signature . ':'; + + return $args; + } + + /** + * Verify the HTTP Signature against a request. + * + * @param array $headers The HTTP headers. + * @param string|null $body The request body, if applicable. + * @return bool|\WP_Error True, if the signature is valid, WP_Error on failure. + */ + public function verify( array $headers, $body = null ) { + $parsed = $this->parse_signature_labels( $headers ); + if ( \is_wp_error( $parsed ) ) { + return $parsed; + } + + $errors = new \WP_Error(); + foreach ( $parsed as $data ) { + $result = $this->verify_signature_label( $data, $headers, $body ); + if ( true === $result ) { + return true; + } + + if ( \is_wp_error( $result ) ) { + $errors->add( $result->get_error_code(), $result->get_error_message() ); + } + } + + // No valid signature found. + $errors->add_data( array( 'status' => 401 ) ); + + return $errors; + } + + /** + * Generate a digest for the request body. + * + * @param string $body The request body. + * + * @return string The digest. + */ + public function generate_digest( $body ) { + return 'sha-256=:' . \base64_encode( \hash( 'sha256', $body, true ) ) . ':'; + } + + /** + * Parse the Signature-Input and Signature headers. + * + * @param array $headers The HTTP headers. + * @return array|\WP_Error Parsed signature labels or WP_Error on failure. + */ + private function parse_signature_labels( array $headers ) { + $parsed_inputs = array(); + \preg_match_all( '/(?P