Compare commits

..

No commits in common. "dev/extendable-transformers" and "add/extendable-transformers" have entirely different histories.

75 changed files with 803 additions and 2352 deletions

View file

@ -14,7 +14,6 @@ The WordPress plugin largely follows ActivityPub's server-to-server specificatio
- [FEP-f1d5: NodeInfo in Fediverse Software](https://codeberg.org/fediverse/fep/src/branch/main/fep/f1d5/fep-f1d5.md) - [FEP-f1d5: NodeInfo in Fediverse Software](https://codeberg.org/fediverse/fep/src/branch/main/fep/f1d5/fep-f1d5.md)
- [FEP-67ff: FEDERATION.md](https://codeberg.org/fediverse/fep/src/branch/main/fep/67ff/fep-67ff.md) - [FEP-67ff: FEDERATION.md](https://codeberg.org/fediverse/fep/src/branch/main/fep/67ff/fep-67ff.md)
- [FEP-5feb: Search indexing consent for actors](https://codeberg.org/fediverse/fep/src/branch/main/fep/5feb/fep-5feb.md) - [FEP-5feb: Search indexing consent for actors](https://codeberg.org/fediverse/fep/src/branch/main/fep/5feb/fep-5feb.md)
- [FEP-2677: Identifying the Application Actor](https://codeberg.org/fediverse/fep/src/branch/main/fep/2677/fep-2677.md)
Partially supported FEPs Partially supported FEPs

View file

@ -3,7 +3,7 @@
**Tags:** OStatus, fediverse, activitypub, activitystream **Tags:** OStatus, fediverse, activitypub, activitystream
**Requires at least:** 4.7 **Requires at least:** 4.7
**Tested up to:** 6.4 **Tested up to:** 6.4
**Stable tag:** 1.3.0 **Stable tag:** 1.2.0
**Requires PHP:** 5.6 **Requires PHP:** 5.6
**License:** MIT **License:** MIT
**License URI:** http://opensource.org/licenses/MIT **License URI:** http://opensource.org/licenses/MIT
@ -68,10 +68,10 @@ Implemented:
* share posts * share posts
* receive comments/reactions * receive comments/reactions
* signature verification * signature verification
* threaded comments support
To implement: To implement:
* threaded comments support
* replace shortcodes with blocks for layout * replace shortcodes with blocks for layout
### What is "ActivityPub for WordPress" ### ### What is "ActivityPub for WordPress" ###
@ -105,25 +105,6 @@ Where 'blog' is the path to the subdirectory at which your blog resides.
Project maintained on GitHub at [automattic/wordpress-activitypub](https://github.com/automattic/wordpress-activitypub). Project maintained on GitHub at [automattic/wordpress-activitypub](https://github.com/automattic/wordpress-activitypub).
### 2.0.0 ###
* Removed: Deprecated Classes
* Fixed: Normalize attributes that can have mixed value types
* Added: URL support for WebFinger
* Added: Make Post-Template filterable
* Added: CSS class for ActivityPub comments to allow custom designs
* Added: FEP-2677: Identifying the Application Actor
* Improved: WebFinger endpoints
### 1.3.0 ###
* Added: Threaded-Comments support
* Improved: alt text for avatars in Follow Me/Followers blocks
* Improved: `Delete`, `Update` and `Follow` Activities
* Improved: better/more effective handling of `Delete` Activities
* Improved: allow `<p />` and `<br />` for Comments
* Fixed: removed default limit of WP_Query to send updates to all Inboxes and not only to the first 10
### 1.2.0 ### ### 1.2.0 ###
* Add: Search and order followerer lists * Add: Search and order followerer lists

View file

@ -3,7 +3,7 @@
* Plugin Name: ActivityPub * Plugin Name: ActivityPub
* Plugin URI: https://github.com/pfefferle/wordpress-activitypub/ * Plugin URI: https://github.com/pfefferle/wordpress-activitypub/
* Description: The ActivityPub protocol is a decentralized social networking protocol based upon the ActivityStreams 2.0 data format. * Description: The ActivityPub protocol is a decentralized social networking protocol based upon the ActivityStreams 2.0 data format.
* Version: 1.3.0 * Version: 1.2.0
* Author: Matthias Pfefferle & Automattic * Author: Matthias Pfefferle & Automattic
* Author URI: https://automattic.com/ * Author URI: https://automattic.com/
* License: MIT * License: MIT
@ -34,7 +34,7 @@ require_once __DIR__ . '/includes/functions.php';
\defined( 'ACTIVITYPUB_AUTHORIZED_FETCH' ) || \define( 'ACTIVITYPUB_AUTHORIZED_FETCH', false ); \defined( 'ACTIVITYPUB_AUTHORIZED_FETCH' ) || \define( 'ACTIVITYPUB_AUTHORIZED_FETCH', false );
\defined( 'ACTIVITYPUB_DISABLE_REWRITES' ) || \define( 'ACTIVITYPUB_DISABLE_REWRITES', false ); \defined( 'ACTIVITYPUB_DISABLE_REWRITES' ) || \define( 'ACTIVITYPUB_DISABLE_REWRITES', false );
\defined( 'ACTIVITYPUB_DEFAULT_TRANSFORMER' ) || \define( 'ACTIVITYPUB_DEFAULT_TRANSFORMER', 'activitypub/default' ); \defined( 'ACTIVITYPUB_DEFAULT_TRANSFORMER' ) || \define( 'ACTIVITYPUB_DEFAULT_TRANSFORMER', 'activitypub/default' );
\defined( 'ACTIVITYPUB_JSON_FORMAT_OPTIONS' ) || \define( 'ACTIVITYPUB_JSON_FORMAT_OPTIONS', \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT | \JSON_UNESCAPED_SLASHES);
\define( 'ACTIVITYPUB_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); \define( 'ACTIVITYPUB_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
\define( 'ACTIVITYPUB_PLUGIN_BASENAME', plugin_basename( __FILE__ ) ); \define( 'ACTIVITYPUB_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
@ -69,7 +69,7 @@ function plugin_init() {
\add_action( 'init', array( __NAMESPACE__ . '\Migration', 'init' ) ); \add_action( 'init', array( __NAMESPACE__ . '\Migration', 'init' ) );
\add_action( 'init', array( __NAMESPACE__ . '\Activitypub', 'init' ) ); \add_action( 'init', array( __NAMESPACE__ . '\Activitypub', 'init' ) );
\add_action( 'init', array( __NAMESPACE__ . '\Activity_Dispatcher', 'init' ) ); \add_action( 'init', array( __NAMESPACE__ . '\Activity_Dispatcher', 'init' ) );
\add_action( 'init', array( __NAMESPACE__ . '\Handler', 'init' ) ); \add_action( 'init', array( __NAMESPACE__ . '\Collection\Followers', 'init' ) );
\add_action( 'init', array( __NAMESPACE__ . '\Admin', 'init' ) ); \add_action( 'init', array( __NAMESPACE__ . '\Admin', 'init' ) );
\add_action( 'init', array( __NAMESPACE__ . '\Hashtag', 'init' ) ); \add_action( 'init', array( __NAMESPACE__ . '\Hashtag', 'init' ) );
\add_action( 'init', array( __NAMESPACE__ . '\Mention', 'init' ) ); \add_action( 'init', array( __NAMESPACE__ . '\Mention', 'init' ) );
@ -174,7 +174,7 @@ function plugin_settings_link( $actions ) {
*/ */
add_action( add_action(
'bp_include', 'bp_include',
function () { function() {
require_once __DIR__ . '/integration/class-buddypress.php'; require_once __DIR__ . '/integration/class-buddypress.php';
Integration\Buddypress::init(); Integration\Buddypress::init();
}, },

View file

@ -139,13 +139,10 @@ recreate_db() {
} }
create_db() { create_db() {
mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS" $EXTRA mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA
} }
install_db() { install_db() {
# the mariadb commands are not available on some self-hosted runner images
apt update && apt-get install mariadb-server mariadb-client -y
service mariadb start
if [ ${SKIP_DB_CREATE} = "true" ]; then if [ ${SKIP_DB_CREATE} = "true" ]; then
return 0 return 0

View file

@ -1 +1 @@
<?php return array( 'dependencies' => array( 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives' ), 'version' => '6aeec6336fd28aa836a7' ); <?php return array('dependencies' => array('wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '2a185b1c488886051601');

File diff suppressed because one or more lines are too long

View file

@ -1 +1 @@
<?php return array( 'dependencies' => array( 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-primitives' ), 'version' => '5b48281e37700a970a66' ); <?php return array('dependencies' => array('wp-api-fetch', 'wp-components', 'wp-compose', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '17a158ceced1355cc8ea');

File diff suppressed because one or more lines are too long

View file

@ -1 +1 @@
<?php return array( 'dependencies' => array( 'react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives', 'wp-url' ), 'version' => '59d9702e06860a6d13e4' ); <?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives', 'wp-url'), 'version' => '1cbd9cbfcbd7fc813429');

View file

@ -1,3 +1,3 @@
(()=>{var e={184:(e,t)=>{var a;!function(){"use strict";var n={}.hasOwnProperty;function l(){for(var e=[],t=0;t<arguments.length;t++){var a=arguments[t];if(a){var r=typeof a;if("string"===r||"number"===r)e.push(a);else if(Array.isArray(a)){if(a.length){var o=l.apply(null,a);o&&e.push(o)}}else if("object"===r){if(a.toString!==Object.prototype.toString&&!a.toString.toString().includes("[native code]")){e.push(a.toString());continue}for(var i in a)n.call(a,i)&&a[i]&&e.push(i)}}}return e.join(" ")}e.exports?(l.default=l,e.exports=l):void 0===(a=function(){return l}.apply(t,[]))||(e.exports=a)}()}},t={};function a(n){var l=t[n];if(void 0!==l)return l.exports;var r=t[n]={exports:{}};return e[n](r,r.exports,a),r.exports}a.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return a.d(t,{a:t}),t},a.d=(e,t)=>{for(var n in t)a.o(t,n)&&!a.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},a.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{"use strict";const e=window.wp.blocks,t=window.wp.element,n=window.wp.primitives,l=(0,t.createElement)(n.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},(0,t.createElement)(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"}));function r(){return r=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e},r.apply(this,arguments)}const o=window.wp.components,i=window.wp.blockEditor,c=window.wp.i18n,s=window.React,p=window.wp.apiFetch;var u=a.n(p);const v=window.wp.url;var m=a(184),b=a.n(m);function w(e){let{active:a,children:n,page:l,pageClick:r,className:o}=e;const i=b()("wp-block activitypub-pager",o,{current:a});return(0,t.createElement)("a",{className:i,onClick:e=>{e.preventDefault(),!a&&r(l)}},n)}const d={outlined:"outlined",minimal:"minimal"};function f(e){let{compact:a,nextLabel:n,page:l,pageClick:r,perPage:o,prevLabel:i,total:c,variant:s=d.outlined}=e;const p=((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,n)=>e>=1&&e<=t&&n.lastIndexOf(e)===a));for(let e=a.length-2;e>=0;e--)a[e]===a[e+1]&&a.splice(e+1,1);return a})(l,Math.ceil(c/o)),u=b()("alignwide wp-block-query-pagination is-content-justification-space-between is-layout-flex wp-block-query-pagination-is-layout-flex",`is-${s}`,{"is-compact":a});return(0,t.createElement)("nav",{className:u},i&&(0,t.createElement)(w,{key:"prev",page:l-1,pageClick:r,active:1===l,"aria-label":i,className:"wp-block-query-pagination-previous block-editor-block-list__block"},i),!a&&(0,t.createElement)("div",{className:"block-editor-block-list__block wp-block wp-block-query-pagination-numbers"},p.map((e=>(0,t.createElement)(w,{key:e,page:e,pageClick:r,active:e===l,className:"page-numbers"},e)))),n&&(0,t.createElement)(w,{key:"next",page:l+1,pageClick:r,active:l===Math.ceil(c/o),"aria-label":n,className:"wp-block-query-pagination-next block-editor-block-list__block"},n))}const{namespace:g}=window._activityPubOptions;function y(e){let{selectedUser:a,per_page:n,order:l,title:o,page:i,setPage:p,className:m="",followLinks:b=!0,followerData:w=!1}=e;const d="site"===a?0:a,[y,k]=(0,s.useState)([]),[E,_]=(0,s.useState)(0),[x,C]=(0,s.useState)(0),[S,O]=function(){const[e,t]=(0,s.useState)(1);return[e,t]}(),N=i||S,P=p||O,L=(0,t.createInterpolateElement)(/* translators: arrow for previous followers link */ (()=>{var e={184:(e,t)=>{var a;!function(){"use strict";var n={}.hasOwnProperty;function l(){for(var e=[],t=0;t<arguments.length;t++){var a=arguments[t];if(a){var r=typeof a;if("string"===r||"number"===r)e.push(a);else if(Array.isArray(a)){if(a.length){var o=l.apply(null,a);o&&e.push(o)}}else if("object"===r){if(a.toString!==Object.prototype.toString&&!a.toString.toString().includes("[native code]")){e.push(a.toString());continue}for(var i in a)n.call(a,i)&&a[i]&&e.push(i)}}}return e.join(" ")}e.exports?(l.default=l,e.exports=l):void 0===(a=function(){return l}.apply(t,[]))||(e.exports=a)}()}},t={};function a(n){var l=t[n];if(void 0!==l)return l.exports;var r=t[n]={exports:{}};return e[n](r,r.exports,a),r.exports}a.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return a.d(t,{a:t}),t},a.d=(e,t)=>{for(var n in t)a.o(t,n)&&!a.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},a.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{"use strict";const e=window.wp.blocks,t=window.wp.element,n=window.wp.primitives,l=(0,t.createElement)(n.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},(0,t.createElement)(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"}));function r(){return r=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e},r.apply(this,arguments)}const o=window.wp.components,i=window.wp.blockEditor,c=window.wp.i18n,s=window.React,p=window.wp.apiFetch;var u=a.n(p);const v=window.wp.url;var m=a(184),b=a.n(m);function w(e){let{active:a,children:n,page:l,pageClick:r,className:o}=e;const i=b()("wp-block activitypub-pager",o,{current:a});return(0,t.createElement)("a",{className:i,onClick:e=>{e.preventDefault(),!a&&r(l)}},n)}const d={outlined:"outlined",minimal:"minimal"};function f(e){let{compact:a,nextLabel:n,page:l,pageClick:r,perPage:o,prevLabel:i,total:c,variant:s=d.outlined}=e;const p=((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,n)=>e>=1&&e<=t&&n.lastIndexOf(e)===a));for(let e=a.length-2;e>=0;e--)a[e]===a[e+1]&&a.splice(e+1,1);return a})(l,Math.ceil(c/o)),u=b()("alignwide wp-block-query-pagination is-content-justification-space-between is-layout-flex wp-block-query-pagination-is-layout-flex",`is-${s}`,{"is-compact":a});return(0,t.createElement)("nav",{className:u},i&&(0,t.createElement)(w,{key:"prev",page:l-1,pageClick:r,active:1===l,"aria-label":i,className:"wp-block-query-pagination-previous block-editor-block-list__block"},i),!a&&(0,t.createElement)("div",{className:"block-editor-block-list__block wp-block wp-block-query-pagination-numbers"},p.map((e=>(0,t.createElement)(w,{key:e,page:e,pageClick:r,active:e===l,className:"page-numbers"},e)))),n&&(0,t.createElement)(w,{key:"next",page:l+1,pageClick:r,active:l===Math.ceil(c/o),"aria-label":n,className:"wp-block-query-pagination-next block-editor-block-list__block"},n))}const{namespace:g}=window._activityPubOptions;function y(e){let{selectedUser:a,per_page:n,order:l,title:o,page:i,setPage:p,className:m="",followLinks:b=!0,followerData:w=!1}=e;const d="site"===a?0:a,[y,k]=(0,s.useState)([]),[E,_]=(0,s.useState)(0),[x,C]=(0,s.useState)(0),[S,O]=function(){const[e,t]=(0,s.useState)(1);return[e,t]}(),N=i||S,P=p||O,L=(0,t.createInterpolateElement)(/* translators: arrow for previous followers link */
(0,c.__)("<span>←</span> Less","activitypub"),{span:(0,t.createElement)("span",{class:"wp-block-query-pagination-previous-arrow is-arrow-arrow","aria-hidden":"true"})}),j=(0,t.createInterpolateElement)(/* translators: arrow for next followers link */ (0,c.__)("<span>←</span> Less","activitypub"),{span:(0,t.createElement)("span",{class:"wp-block-query-pagination-previous-arrow is-arrow-arrow","aria-hidden":"true"})}),j=(0,t.createInterpolateElement)(/* translators: arrow for next followers link */
(0,c.__)("More <span>→</span>","activitypub"),{span:(0,t.createElement)("span",{class:"wp-block-query-pagination-next-arrow is-arrow-arrow","aria-hidden":"true"})}),M=(e,t)=>{k(e),C(t),_(Math.ceil(t/n))};return(0,s.useEffect)((()=>{if(w&&1===N)return M(w.followers,w.total);const e=function(e,t,a,n){const l=`/${g}/users/${e}/followers`,r={per_page:t,order:a,page:n,context:"full"};return(0,v.addQueryArgs)(l,r)}(d,n,l,N);u()({path:e}).then((e=>M(e.orderedItems,e.totalItems))).catch((()=>{}))}),[d,n,l,N,w]),(0,t.createElement)("div",{className:"activitypub-follower-block "+m},(0,t.createElement)("h3",null,o),(0,t.createElement)("ul",null,y&&y.map((e=>(0,t.createElement)("li",{key:e.url},(0,t.createElement)(h,r({},e,{followLinks:b})))))),E>1&&(0,t.createElement)(f,{page:N,perPage:n,total:x,pageClick:P,nextLabel:j,prevLabel:L,compact:"is-style-compact"===m}))}function h(e){let{name:a,icon:n,url:l,preferredUsername:i,followLinks:c=!0}=e;const s=`@${i}`,p={};return c||(p.onClick=e=>e.preventDefault()),(0,t.createElement)(o.ExternalLink,r({className:"activitypub-link",href:l,title:s},p),(0,t.createElement)("img",{width:"40",height:"40",src:n.url,class:"avatar activitypub-avatar",alt:a}),(0,t.createElement)("span",{class:"activitypub-actor"},(0,t.createElement)("strong",{className:"activitypub-name"},a),(0,t.createElement)("span",{class:"sep"},"/"),(0,t.createElement)("span",{class:"activitypub-handle"},s)))}const k=window.wp.data,E=window._activityPubOptions?.enabled;(0,e.registerBlockType)("activitypub/followers",{edit:function(e){let{attributes:a,setAttributes:n}=e;const{order:l,per_page:s,selectedUser:p,title:u}=a,v=(0,i.useBlockProps)(),[m,b]=(0,t.useState)(1),w=[{label:(0,c.__)("New to old","activitypub"),value:"desc"},{label:(0,c.__)("Old to new","activitypub"),value:"asc"}],d=function(){const e=E?.users?(0,k.useSelect)((e=>e("core").getUsers({who:"authors"}))):[];return(0,t.useMemo)((()=>{if(!e)return[];const t=E?.site?[{label:(0,c.__)("Whole Site","activitypub"),value:"site"}]:[];return e.reduce(((e,t)=>(e.push({label:t.name,value:`${t.id}`}),e)),t)}),[e])}(),f=e=>t=>{b(1),n({[e]:t})};return(0,t.useEffect)((()=>{d.length&&(d.find((e=>{let{value:t}=e;return t===p}))||n({selectedUser:d[0].value}))}),[p,d]),(0,t.createElement)("div",v,(0,t.createElement)(i.InspectorControls,{key:"setting"},(0,t.createElement)(o.PanelBody,{title:(0,c.__)("Followers Options","activitypub")},(0,t.createElement)(o.TextControl,{label:(0,c.__)("Title","activitypub"),help:(0,c.__)("Title to display above the list of followers. Blank for none.","activitypub"),value:u,onChange:e=>n({title:e})}),d.length>1&&(0,t.createElement)(o.SelectControl,{label:(0,c.__)("Select User","activitypub"),value:p,options:d,onChange:f("selectedUser")}),(0,t.createElement)(o.SelectControl,{label:(0,c.__)("Sort","activitypub"),value:l,options:w,onChange:f("order")}),(0,t.createElement)(o.RangeControl,{label:(0,c.__)("Number of Followers","activitypub"),value:s,onChange:f("per_page"),min:1,max:10}))),(0,t.createElement)(y,r({},a,{page:m,setPage:b,followLinks:!1})))},save:()=>null,icon:l})})()})(); (0,c.__)("More <span>→</span>","activitypub"),{span:(0,t.createElement)("span",{class:"wp-block-query-pagination-next-arrow is-arrow-arrow","aria-hidden":"true"})}),M=(e,t)=>{k(e),C(t),_(Math.ceil(t/n))};return(0,s.useEffect)((()=>{if(w&&1===N)return M(w.followers,w.total);const e=function(e,t,a,n){const l=`/${g}/users/${e}/followers`,r={per_page:t,order:a,page:n,context:"full"};return(0,v.addQueryArgs)(l,r)}(d,n,l,N);u()({path:e}).then((e=>M(e.orderedItems,e.totalItems))).catch((()=>{}))}),[d,n,l,N,w]),(0,t.createElement)("div",{className:"activitypub-follower-block "+m},(0,t.createElement)("h3",null,o),(0,t.createElement)("ul",null,y&&y.map((e=>(0,t.createElement)("li",{key:e.url},(0,t.createElement)(h,r({},e,{followLinks:b})))))),E>1&&(0,t.createElement)(f,{page:N,perPage:n,total:x,pageClick:P,nextLabel:j,prevLabel:L,compact:"is-style-compact"===m}))}function h(e){let{name:a,icon:n,url:l,preferredUsername:i,followLinks:c=!0}=e;const s=`@${i}`,p={};return c||(p.onClick=e=>e.preventDefault()),(0,t.createElement)(o.ExternalLink,r({className:"activitypub-link",href:l,title:s},p),(0,t.createElement)("img",{width:"40",height:"40",src:n.url,class:"avatar activitypub-avatar"}),(0,t.createElement)("span",{class:"activitypub-actor"},(0,t.createElement)("strong",{className:"activitypub-name"},a),(0,t.createElement)("span",{class:"sep"},"/"),(0,t.createElement)("span",{class:"activitypub-handle"},s)))}const k=window.wp.data,E=window._activityPubOptions?.enabled;(0,e.registerBlockType)("activitypub/followers",{edit:function(e){let{attributes:a,setAttributes:n}=e;const{order:l,per_page:s,selectedUser:p,title:u}=a,v=(0,i.useBlockProps)(),[m,b]=(0,t.useState)(1),w=[{label:(0,c.__)("New to old","activitypub"),value:"desc"},{label:(0,c.__)("Old to new","activitypub"),value:"asc"}],d=function(){const e=E?.users?(0,k.useSelect)((e=>e("core").getUsers({who:"authors"}))):[];return(0,t.useMemo)((()=>{if(!e)return[];const t=E?.site?[{label:(0,c.__)("Whole Site","activitypub"),value:"site"}]:[];return e.reduce(((e,t)=>(e.push({label:t.name,value:`${t.id}`}),e)),t)}),[e])}(),f=e=>t=>{b(1),n({[e]:t})};return(0,t.useEffect)((()=>{d.length&&(d.find((e=>{let{value:t}=e;return t===p}))||n({selectedUser:d[0].value}))}),[p,d]),(0,t.createElement)("div",v,(0,t.createElement)(i.InspectorControls,{key:"setting"},(0,t.createElement)(o.PanelBody,{title:(0,c.__)("Followers Options","activitypub")},(0,t.createElement)(o.TextControl,{label:(0,c.__)("Title","activitypub"),help:(0,c.__)("Title to display above the list of followers. Blank for none.","activitypub"),value:u,onChange:e=>n({title:e})}),d.length>1&&(0,t.createElement)(o.SelectControl,{label:(0,c.__)("Select User","activitypub"),value:p,options:d,onChange:f("selectedUser")}),(0,t.createElement)(o.SelectControl,{label:(0,c.__)("Sort","activitypub"),value:l,options:w,onChange:f("order")}),(0,t.createElement)(o.RangeControl,{label:(0,c.__)("Number of Followers","activitypub"),value:s,onChange:f("per_page"),min:1,max:10}))),(0,t.createElement)(y,r({},a,{page:m,setPage:b,followLinks:!1})))},save:()=>null,icon:l})})()})();

View file

@ -1 +1 @@
<?php return array( 'dependencies' => array( 'react', 'wp-api-fetch', 'wp-components', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-url' ), 'version' => '04e51e7562fe28b0b2c3' ); <?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-components', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-url'), 'version' => 'f0e21057f7ec615290d6');

View file

@ -1,3 +1,3 @@
(()=>{var e,t={189:(e,t,a)=>{"use strict";const r=window.wp.element;function n(){return n=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var r in a)Object.prototype.hasOwnProperty.call(a,r)&&(e[r]=a[r])}return e},n.apply(this,arguments)}const l=window.React,o=window.wp.apiFetch;var i=a.n(o);const c=window.wp.url,s=window.wp.i18n;var p=a(184),u=a.n(p);function m(e){let{active:t,children:a,page:n,pageClick:l,className:o}=e;const i=u()("wp-block activitypub-pager",o,{current:t});return(0,r.createElement)("a",{className:i,onClick:e=>{e.preventDefault(),!t&&l(n)}},a)}const v={outlined:"outlined",minimal:"minimal"};function f(e){let{compact:t,nextLabel:a,page:n,pageClick:l,perPage:o,prevLabel:i,total:c,variant:s=v.outlined}=e;const p=((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})(n,Math.ceil(c/o)),f=u()("alignwide wp-block-query-pagination is-content-justification-space-between is-layout-flex wp-block-query-pagination-is-layout-flex",`is-${s}`,{"is-compact":t});return(0,r.createElement)("nav",{className:f},i&&(0,r.createElement)(m,{key:"prev",page:n-1,pageClick:l,active:1===n,"aria-label":i,className:"wp-block-query-pagination-previous block-editor-block-list__block"},i),!t&&(0,r.createElement)("div",{className:"block-editor-block-list__block wp-block wp-block-query-pagination-numbers"},p.map((e=>(0,r.createElement)(m,{key:e,page:e,pageClick:l,active:e===n,className:"page-numbers"},e)))),a&&(0,r.createElement)(m,{key:"next",page:n+1,pageClick:l,active:n===Math.ceil(c/o),"aria-label":a,className:"wp-block-query-pagination-next block-editor-block-list__block"},a))}const b=window.wp.components,{namespace:d}=window._activityPubOptions;function w(e){let{selectedUser:t,per_page:a,order:o,title:p,page:u,setPage:m,className:v="",followLinks:b=!0,followerData:w=!1}=e;const y="site"===t?0:t,[k,h]=(0,l.useState)([]),[E,O]=(0,l.useState)(0),[x,_]=(0,l.useState)(0),[N,j]=function(){const[e,t]=(0,l.useState)(1);return[e,t]}(),S=u||N,C=m||j,L=(0,r.createInterpolateElement)(/* translators: arrow for previous followers link */ (()=>{var e,t={189:(e,t,a)=>{"use strict";const r=window.wp.element;function n(){return n=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var r in a)Object.prototype.hasOwnProperty.call(a,r)&&(e[r]=a[r])}return e},n.apply(this,arguments)}const l=window.React,o=window.wp.apiFetch;var i=a.n(o);const c=window.wp.url,s=window.wp.i18n;var p=a(184),u=a.n(p);function m(e){let{active:t,children:a,page:n,pageClick:l,className:o}=e;const i=u()("wp-block activitypub-pager",o,{current:t});return(0,r.createElement)("a",{className:i,onClick:e=>{e.preventDefault(),!t&&l(n)}},a)}const v={outlined:"outlined",minimal:"minimal"};function f(e){let{compact:t,nextLabel:a,page:n,pageClick:l,perPage:o,prevLabel:i,total:c,variant:s=v.outlined}=e;const p=((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})(n,Math.ceil(c/o)),f=u()("alignwide wp-block-query-pagination is-content-justification-space-between is-layout-flex wp-block-query-pagination-is-layout-flex",`is-${s}`,{"is-compact":t});return(0,r.createElement)("nav",{className:f},i&&(0,r.createElement)(m,{key:"prev",page:n-1,pageClick:l,active:1===n,"aria-label":i,className:"wp-block-query-pagination-previous block-editor-block-list__block"},i),!t&&(0,r.createElement)("div",{className:"block-editor-block-list__block wp-block wp-block-query-pagination-numbers"},p.map((e=>(0,r.createElement)(m,{key:e,page:e,pageClick:l,active:e===n,className:"page-numbers"},e)))),a&&(0,r.createElement)(m,{key:"next",page:n+1,pageClick:l,active:n===Math.ceil(c/o),"aria-label":a,className:"wp-block-query-pagination-next block-editor-block-list__block"},a))}const b=window.wp.components,{namespace:d}=window._activityPubOptions;function w(e){let{selectedUser:t,per_page:a,order:o,title:p,page:u,setPage:m,className:v="",followLinks:b=!0,followerData:w=!1}=e;const y="site"===t?0:t,[k,h]=(0,l.useState)([]),[E,O]=(0,l.useState)(0),[x,_]=(0,l.useState)(0),[N,j]=function(){const[e,t]=(0,l.useState)(1);return[e,t]}(),S=u||N,C=m||j,L=(0,r.createInterpolateElement)(/* translators: arrow for previous followers link */
(0,s.__)("<span>←</span> Less","activitypub"),{span:(0,r.createElement)("span",{class:"wp-block-query-pagination-previous-arrow is-arrow-arrow","aria-hidden":"true"})}),q=(0,r.createInterpolateElement)(/* translators: arrow for next followers link */ (0,s.__)("<span>←</span> Less","activitypub"),{span:(0,r.createElement)("span",{class:"wp-block-query-pagination-previous-arrow is-arrow-arrow","aria-hidden":"true"})}),q=(0,r.createInterpolateElement)(/* translators: arrow for next followers link */
(0,s.__)("More <span>→</span>","activitypub"),{span:(0,r.createElement)("span",{class:"wp-block-query-pagination-next-arrow is-arrow-arrow","aria-hidden":"true"})}),P=(e,t)=>{h(e),_(t),O(Math.ceil(t/a))};return(0,l.useEffect)((()=>{if(w&&1===S)return P(w.followers,w.total);const e=function(e,t,a,r){const n=`/${d}/users/${e}/followers`,l={per_page:t,order:a,page:r,context:"full"};return(0,c.addQueryArgs)(n,l)}(y,a,o,S);i()({path:e}).then((e=>P(e.orderedItems,e.totalItems))).catch((()=>{}))}),[y,a,o,S,w]),(0,r.createElement)("div",{className:"activitypub-follower-block "+v},(0,r.createElement)("h3",null,p),(0,r.createElement)("ul",null,k&&k.map((e=>(0,r.createElement)("li",{key:e.url},(0,r.createElement)(g,n({},e,{followLinks:b})))))),E>1&&(0,r.createElement)(f,{page:S,perPage:a,total:x,pageClick:C,nextLabel:q,prevLabel:L,compact:"is-style-compact"===v}))}function g(e){let{name:t,icon:a,url:l,preferredUsername:o,followLinks:i=!0}=e;const c=`@${o}`,s={};return i||(s.onClick=e=>e.preventDefault()),(0,r.createElement)(b.ExternalLink,n({className:"activitypub-link",href:l,title:c},s),(0,r.createElement)("img",{width:"40",height:"40",src:a.url,class:"avatar activitypub-avatar",alt:t}),(0,r.createElement)("span",{class:"activitypub-actor"},(0,r.createElement)("strong",{className:"activitypub-name"},t),(0,r.createElement)("span",{class:"sep"},"/"),(0,r.createElement)("span",{class:"activitypub-handle"},c)))}const y=window.wp.domReady;a.n(y)()((()=>{[].forEach.call(document.querySelectorAll(".activitypub-follower-block"),(e=>{const t=JSON.parse(e.dataset.attrs);(0,r.render)((0,r.createElement)(w,t),e)}))}))},184:(e,t)=>{var a;!function(){"use strict";var r={}.hasOwnProperty;function n(){for(var e=[],t=0;t<arguments.length;t++){var a=arguments[t];if(a){var l=typeof a;if("string"===l||"number"===l)e.push(a);else if(Array.isArray(a)){if(a.length){var o=n.apply(null,a);o&&e.push(o)}}else if("object"===l){if(a.toString!==Object.prototype.toString&&!a.toString.toString().includes("[native code]")){e.push(a.toString());continue}for(var i in a)r.call(a,i)&&a[i]&&e.push(i)}}}return e.join(" ")}e.exports?(n.default=n,e.exports=n):void 0===(a=function(){return n}.apply(t,[]))||(e.exports=a)}()}},a={};function r(e){var n=a[e];if(void 0!==n)return n.exports;var l=a[e]={exports:{}};return t[e](l,l.exports,r),l.exports}r.m=t,e=[],r.O=(t,a,n,l)=>{if(!a){var o=1/0;for(p=0;p<e.length;p++){for(var[a,n,l]=e[p],i=!0,c=0;c<a.length;c++)(!1&l||o>=l)&&Object.keys(r.O).every((e=>r.O[e](a[c])))?a.splice(c--,1):(i=!1,l<o&&(o=l));if(i){e.splice(p--,1);var s=n();void 0!==s&&(t=s)}}return t}l=l||0;for(var p=e.length;p>0&&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={638:0,962:0};r.O.j=t=>0===e[t];var t=(t,a)=>{var n,l,[o,i,c]=a,s=0;if(o.some((t=>0!==e[t]))){for(n in i)r.o(i,n)&&(r.m[n]=i[n]);if(c)var p=c(r)}for(t&&t(a);s<o.length;s++)l=o[s],r.o(e,l)&&e[l]&&e[l][0](),e[l]=0;return r.O(p)},a=globalThis.webpackChunkwordpress_activitypub=globalThis.webpackChunkwordpress_activitypub||[];a.forEach(t.bind(null,0)),a.push=t.bind(null,a.push.bind(a))})();var n=r.O(void 0,[962],(()=>r(189)));n=r.O(n)})(); (0,s.__)("More <span>→</span>","activitypub"),{span:(0,r.createElement)("span",{class:"wp-block-query-pagination-next-arrow is-arrow-arrow","aria-hidden":"true"})}),P=(e,t)=>{h(e),_(t),O(Math.ceil(t/a))};return(0,l.useEffect)((()=>{if(w&&1===S)return P(w.followers,w.total);const e=function(e,t,a,r){const n=`/${d}/users/${e}/followers`,l={per_page:t,order:a,page:r,context:"full"};return(0,c.addQueryArgs)(n,l)}(y,a,o,S);i()({path:e}).then((e=>P(e.orderedItems,e.totalItems))).catch((()=>{}))}),[y,a,o,S,w]),(0,r.createElement)("div",{className:"activitypub-follower-block "+v},(0,r.createElement)("h3",null,p),(0,r.createElement)("ul",null,k&&k.map((e=>(0,r.createElement)("li",{key:e.url},(0,r.createElement)(g,n({},e,{followLinks:b})))))),E>1&&(0,r.createElement)(f,{page:S,perPage:a,total:x,pageClick:C,nextLabel:q,prevLabel:L,compact:"is-style-compact"===v}))}function g(e){let{name:t,icon:a,url:l,preferredUsername:o,followLinks:i=!0}=e;const c=`@${o}`,s={};return i||(s.onClick=e=>e.preventDefault()),(0,r.createElement)(b.ExternalLink,n({className:"activitypub-link",href:l,title:c},s),(0,r.createElement)("img",{width:"40",height:"40",src:a.url,class:"avatar activitypub-avatar"}),(0,r.createElement)("span",{class:"activitypub-actor"},(0,r.createElement)("strong",{className:"activitypub-name"},t),(0,r.createElement)("span",{class:"sep"},"/"),(0,r.createElement)("span",{class:"activitypub-handle"},c)))}const y=window.wp.domReady;a.n(y)()((()=>{[].forEach.call(document.querySelectorAll(".activitypub-follower-block"),(e=>{const t=JSON.parse(e.dataset.attrs);(0,r.render)((0,r.createElement)(w,t),e)}))}))},184:(e,t)=>{var a;!function(){"use strict";var r={}.hasOwnProperty;function n(){for(var e=[],t=0;t<arguments.length;t++){var a=arguments[t];if(a){var l=typeof a;if("string"===l||"number"===l)e.push(a);else if(Array.isArray(a)){if(a.length){var o=n.apply(null,a);o&&e.push(o)}}else if("object"===l){if(a.toString!==Object.prototype.toString&&!a.toString.toString().includes("[native code]")){e.push(a.toString());continue}for(var i in a)r.call(a,i)&&a[i]&&e.push(i)}}}return e.join(" ")}e.exports?(n.default=n,e.exports=n):void 0===(a=function(){return n}.apply(t,[]))||(e.exports=a)}()}},a={};function r(e){var n=a[e];if(void 0!==n)return n.exports;var l=a[e]={exports:{}};return t[e](l,l.exports,r),l.exports}r.m=t,e=[],r.O=(t,a,n,l)=>{if(!a){var o=1/0;for(p=0;p<e.length;p++){for(var[a,n,l]=e[p],i=!0,c=0;c<a.length;c++)(!1&l||o>=l)&&Object.keys(r.O).every((e=>r.O[e](a[c])))?a.splice(c--,1):(i=!1,l<o&&(o=l));if(i){e.splice(p--,1);var s=n();void 0!==s&&(t=s)}}return t}l=l||0;for(var p=e.length;p>0&&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={638:0,962:0};r.O.j=t=>0===e[t];var t=(t,a)=>{var n,l,[o,i,c]=a,s=0;if(o.some((t=>0!==e[t]))){for(n in i)r.o(i,n)&&(r.m[n]=i[n]);if(c)var p=c(r)}for(t&&t(a);s<o.length;s++)l=o[s],r.o(e,l)&&e[l]&&e[l][0](),e[l]=0;return r.O(p)},a=globalThis.webpackChunkwordpress_activitypub=globalThis.webpackChunkwordpress_activitypub||[];a.forEach(t.bind(null,0)),a.push=t.bind(null,a.push.bind(a))})();var n=r.O(void 0,[962],(()=>r(189)));n=r.O(n)})();

View file

@ -15,8 +15,7 @@
"yoast/phpunit-polyfills": "^2.0", "yoast/phpunit-polyfills": "^2.0",
"dealerdirect/phpcodesniffer-composer-installer": "^1.0.0", "dealerdirect/phpcodesniffer-composer-installer": "^1.0.0",
"sirbrillig/phpcs-variable-analysis": "^2.11", "sirbrillig/phpcs-variable-analysis": "^2.11",
"phpcsstandards/phpcsextra": "^1.1.0", "phpcsstandards/phpcsextra": "^1.1.0"
"dms/phpunit-arraysubset-asserts": "^0.5.0"
}, },
"config": { "config": {
"allow-plugins": true "allow-plugins": true

View file

@ -194,12 +194,6 @@ class Activity extends Base_Object {
* @return void * @return void
*/ */
public function set_object( $object ) { public function set_object( $object ) {
// convert array to object
if ( is_array( $object ) ) {
$object = self::init_from_array( $object );
}
// set object
$this->set( 'object', $object ); $this->set( 'object', $object );
if ( ! is_object( $object ) ) { if ( ! is_object( $object ) ) {

View file

@ -19,7 +19,7 @@ class Actor extends Base_Object {
/** /**
* @var string * @var string
*/ */
protected $type = 'Group'; protected $type = 'Person';
/** /**
* A reference to an ActivityStreams OrderedCollection comprised of * A reference to an ActivityStreams OrderedCollection comprised of

View file

@ -26,20 +26,6 @@ use function Activitypub\snake_to_camel_case;
* @see https://www.w3.org/TR/activitystreams-core/#object * @see https://www.w3.org/TR/activitystreams-core/#object
*/ */
class Base_Object { class Base_Object {
/**
* if this is set, the next setter call doesn't change anything
*
* @var bool
*/
protected $_skip_next;
/**
* if this is set, the language maps will be set automatically with this language if not set yet todo nicer description
*
* @var string
*/
protected $_default_language_code_for_language_maps;
/** /**
* The object's unique global identifier * The object's unique global identifier
* *
@ -471,11 +457,11 @@ class Base_Object {
} }
if ( \strncasecmp( $method, 'set', 3 ) === 0 ) { if ( \strncasecmp( $method, 'set', 3 ) === 0 ) {
return $this->set( $var, $params[0] ); $this->set( $var, $params[0] );
} }
if ( \strncasecmp( $method, 'add', 3 ) === 0 ) { if ( \strncasecmp( $method, 'add', 3 ) === 0 ) {
return $this->add( $var, $params[0] ); $this->add( $var, $params[0] );
} }
} }
@ -536,54 +522,9 @@ class Base_Object {
return new WP_Error( 'invalid_key', __( 'Invalid key', 'activitypub' ), array( 'status' => 404 ) ); return new WP_Error( 'invalid_key', __( 'Invalid key', 'activitypub' ), array( 'status' => 404 ) );
} }
if ( $this->_skip_next ) {
$this->_skip_next = false;
return $this;
}
if ( 'content' === $key || 'summary' === $key || 'name' === $key ) { // todo nicer?
$this->set_basic_language_map( $key, $value ); // todo skip setter if language map is already set
}
$this->$key = $value; $this->$key = $value;
return $this; return $this->$key;
}
/**
* Setter for the languageMap default language
*
* @param string $languagecode languageocde for default
*
*/
public function set_default_language_for_maps( $languagecode ) {
$this->_default_language_code_for_language_maps = $languagecode;
return $this;
}
/**
* Generic languageMap setter.
*
* @param string $key The key for the language map.
* @param string $value The value to set.
*
*/
public function set_basic_language_map( $key, $value ) {
// todo better name, implement usage for content, summary and name
$mapkey = $key . '_map';
if ( ! isset( $this->_default_language_code_for_language_maps ) ) {
return new WP_Error( '_default_language_code_for_language_maps must be set before calling set_languageMap', __( '_default_language_code_for_language_maps must be set before calling set_languageMap', 'activitypub' ), array( 'status' => 404 ) );
}
if ( ! $this->has( $mapkey ) ) {
return new WP_Error( 'invalid_key', __( 'Invalid key', 'activitypub' ), array( 'status' => 404 ) );
}
$this->$mapkey = array(
$this->_default_language_code_for_language_maps => $value,
);
return $this;
} }
/** /**
@ -608,7 +549,7 @@ class Base_Object {
$this->$key = $attributes; $this->$key = $attributes;
return $this; return $this->$key;
} }
/** /**
@ -644,7 +585,7 @@ class Base_Object {
foreach ( $array as $key => $value ) { foreach ( $array as $key => $value ) {
$key = camel_to_snake_case( $key ); $key = camel_to_snake_case( $key );
call_user_func( array( $object, 'set_' . $key ), $value ); $object->set( $key, $value );
} }
return $object; return $object;
@ -670,7 +611,7 @@ class Base_Object {
foreach ( $array as $key => $value ) { foreach ( $array as $key => $value ) {
if ( $value ) { if ( $value ) {
$key = camel_to_snake_case( $key ); $key = camel_to_snake_case( $key );
call_user_func( array( $this, 'set_' . $key ), $value ); $this->set( $key, $value );
} }
} }
} }
@ -698,7 +639,7 @@ class Base_Object {
$value = call_user_func( array( $this, 'get_' . $key ) ); $value = call_user_func( array( $this, 'get_' . $key ) );
} }
if ( is_object( $value ) && is_subclass_of( $value, self::class ) ) { if ( is_object( $value ) ) {
$value = $value->to_array(); $value = $value->to_array();
} }
@ -734,49 +675,4 @@ class Base_Object {
return \wp_json_encode( $array, \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT ); return \wp_json_encode( $array, \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT );
} }
/**
* skips next setter if( false );
*
* @param bool $b
*
*/
public function if( $b ) {
$this->_skip_next = ! $b;
return $this;
}
public function check_jsonld_completeness() {
// Just a debugging helper function
$vars = get_object_vars( $this );
$total_var_count = count( $vars );
$unset_var_array = array();
foreach ( $vars as $key => $value ) {
// ignore all _prefixed keys.
if ( '_' === substr( $key, 0, 1 ) ) {
--$total_var_count;
continue;
}
// if value is empty, try to get it from a getter.
if ( ! $value ) {
$value = call_user_func( array( $this, 'get_' . $key ) );
}
// if value is still empty, ignore it for the array and continue.
if ( ! isset( $value ) ) {
$unset_var_array[] = $key;
}
}
$unset_var_count = count( $unset_var_array );
return array(
'total_var_count' => $total_var_count,
'unset_var_count' => $unset_var_count,
'set_var_count' => $total_var_count - $unset_var_count,
'unset_vars' => $unset_var_array,
);
}
} }

View file

@ -0,0 +1,23 @@
<?php
/**
* Inspired by the PHP ActivityPub Library by @Landrok
*
* @link https://github.com/landrok/activitypub
*/
namespace Activitypub\Activity;
use Activitypub\Activity\Base_Object;
/**
* Event is an implementation of one of the
* Activity Streams Event object type
*
* The Object is the primary base type for the Activity Streams
* vocabulary.
*
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-event
*/
class Note extends Base_Object {
protected $type = 'Event';
}

View file

@ -0,0 +1,23 @@
<?php
/**
* Inspired by the PHP ActivityPub Library by @Landrok
*
* @link https://github.com/landrok/activitypub
*/
namespace Activitypub\Activity;
use Activitypub\Activity\Base_Object;
/**
* Note is an implementation of one of the
* Activity Streams Note object type
*
* The Object is the primary base type for the Activity Streams
* vocabulary.
*
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-note
*/
class Note extends Base_Object {
protected $type = 'Note';
}

View file

@ -5,7 +5,7 @@ use WP_Post;
use Activitypub\Activity\Activity; use Activitypub\Activity\Activity;
use Activitypub\Collection\Users; use Activitypub\Collection\Users;
use Activitypub\Collection\Followers; use Activitypub\Collection\Followers;
use Activitypub\Transformer\Transformer_Factory; use Activitypub\Transformer\Transformers_Manager;
use function Activitypub\is_single_user; use function Activitypub\is_single_user;
use function Activitypub\is_user_disabled; use function Activitypub\is_user_disabled;
@ -65,7 +65,8 @@ class Activity_Dispatcher {
return; return;
} }
$object = Transformer_Factory::instance()->transform( $wp_post ); $transformer = Transformers_Manager::instance()->get_transformer( $wp_post );
$object = $transformer->to_object();
$activity = new Activity(); $activity = new Activity();
$activity->set_type( $type ); $activity->set_type( $type );
@ -101,7 +102,8 @@ class Activity_Dispatcher {
return; return;
} }
$object = Transformer_Factory::instance()->transform( $wp_post ); $transformer = Transformers_Manager::instance()->get_transformer( $wp_post );
$object = $transformer->to_object();
$activity = new Activity(); $activity = new Activity();
$activity->set_type( 'Announce' ); $activity->set_type( 'Announce' );

View file

@ -1,12 +1,8 @@
<?php <?php
namespace Activitypub; namespace Activitypub;
use Exception;
use Activitypub\Signature; use Activitypub\Signature;
use Activitypub\Collection\Users; use Activitypub\Collection\Users;
use Activitypub\Collection\Followers;
use function Activitypub\sanitize_url;
/** /**
* ActivityPub Class * ActivityPub Class
@ -38,11 +34,6 @@ class Activitypub {
\add_action( 'after_setup_theme', array( self::class, 'theme_compat' ), 99 ); \add_action( 'after_setup_theme', array( self::class, 'theme_compat' ), 99 );
\add_action( 'in_plugin_update_message-' . ACTIVITYPUB_PLUGIN_BASENAME, array( self::class, 'plugin_update_message' ) ); \add_action( 'in_plugin_update_message-' . ACTIVITYPUB_PLUGIN_BASENAME, array( self::class, 'plugin_update_message' ) );
\add_filter( 'comment_class', array( self::class, 'comment_class' ), 10, 3 );
// register several post_types
self::register_post_types();
} }
/** /**
@ -63,6 +54,7 @@ class Activitypub {
*/ */
public static function deactivate() { public static function deactivate() {
self::flush_rewrite_rules(); self::flush_rewrite_rules();
Scheduler::deregister_schedules(); Scheduler::deregister_schedules();
} }
@ -336,98 +328,4 @@ class Activitypub {
) )
); );
} }
/**
* Register the "Followers" Taxonomy
*
* @return void
*/
private static function register_post_types() {
register_post_type(
Followers::POST_TYPE,
array(
'labels' => array(
'name' => _x( 'Followers', 'post_type plural name', 'activitypub' ),
'singular_name' => _x( 'Follower', 'post_type single name', 'activitypub' ),
),
'public' => false,
'hierarchical' => false,
'rewrite' => false,
'query_var' => false,
'delete_with_user' => false,
'can_export' => true,
'supports' => array(),
)
);
register_post_meta(
Followers::POST_TYPE,
'activitypub_inbox',
array(
'type' => 'string',
'single' => true,
'sanitize_callback' => 'sanitize_url',
)
);
register_post_meta(
Followers::POST_TYPE,
'activitypub_errors',
array(
'type' => 'string',
'single' => false,
'sanitize_callback' => function ( $value ) {
if ( ! is_string( $value ) ) {
throw new Exception( 'Error message is no valid string' );
}
return esc_sql( $value );
},
)
);
register_post_meta(
Followers::POST_TYPE,
'activitypub_user_id',
array(
'type' => 'string',
'single' => false,
'sanitize_callback' => function ( $value ) {
return esc_sql( $value );
},
)
);
register_post_meta(
Followers::POST_TYPE,
'activitypub_actor_json',
array(
'type' => 'string',
'single' => true,
'sanitize_callback' => function ( $value ) {
return sanitize_text_field( $value );
},
)
);
do_action( 'activitypub_after_register_post_type' );
}
/**
* Filters the CSS classes to add an ActivityPub class.
*
* @param string[] $classes An array of comment classes.
* @param string[] $css_class An array of additional classes added to the list.
* @param string $comment_id The comment ID as a numeric string.
*
* @return string[] An array of classes.
*/
public static function comment_class( $classes, $css_class, $comment_id ) {
// check if ActivityPub comment
if ( 'activitypub' === get_comment_meta( $comment_id, 'protocol', true ) ) {
$classes[] = 'activitypub-comment';
}
return $classes;
}
} }

View file

@ -25,7 +25,7 @@ class Admin {
} }
add_filter( add_filter(
'activitypub_transformers_is_transformer_enabled', 'activitypub/transformers/is_transformer_enabled',
function( $should_register, Transformer_Base $transformer_instance ) { function( $should_register, Transformer_Base $transformer_instance ) {
return ! Options::is_transformer_disabled( $transformer_instance->get_name() ); return ! Options::is_transformer_disabled( $transformer_instance->get_name() );
}, },
@ -215,7 +215,7 @@ class Admin {
'description' => \esc_html__( 'The Identifier of the Blog-User', 'activitypub' ), 'description' => \esc_html__( 'The Identifier of the Blog-User', 'activitypub' ),
'show_in_rest' => true, 'show_in_rest' => true,
'default' => Blog_User::get_default_username(), 'default' => Blog_User::get_default_username(),
'sanitize_callback' => function ( $value ) { 'sanitize_callback' => function( $value ) {
// hack to allow dots in the username // hack to allow dots in the username
$parts = explode( '.', $value ); $parts = explode( '.', $value );
$sanitized = array(); $sanitized = array();

View file

@ -3,9 +3,7 @@ namespace Activitypub;
use Activitypub\Collection\Followers; use Activitypub\Collection\Followers;
use Activitypub\Collection\Users as User_Collection; use Activitypub\Collection\Users as User_Collection;
use Activitypub\is_user_type_disabled;
use function Activitypub\object_to_uri;
use function Activitypub\is_user_type_disabled;
class Blocks { class Blocks {
public static function init() { public static function init() {
@ -96,7 +94,7 @@ class Blocks {
$attrs['followerData']['total'] = $follower_data['total']; $attrs['followerData']['total'] = $follower_data['total'];
$attrs['followerData']['followers'] = array_map( $attrs['followerData']['followers'] = array_map(
function ( $follower ) { function( $follower ) {
return self::filter_array_by_keys( return self::filter_array_by_keys(
$follower->to_array(), $follower->to_array(),
array( 'icon', 'name', 'preferredUsername', 'url' ) array( 'icon', 'name', 'preferredUsername', 'url' )
@ -142,7 +140,7 @@ class Blocks {
return sprintf( return sprintf(
$template, $template,
esc_url( object_to_uri( $data['url'] ) ), esc_url( $data['url'] ),
esc_attr( $data['name'] ), esc_attr( $data['name'] ),
esc_attr( $data['icon']['url'] ), esc_attr( $data['icon']['url'] ),
esc_html( $data['name'] ), esc_html( $data['name'] ),

View file

@ -1,33 +0,0 @@
<?php
namespace Activitypub;
use Activitypub\Handler\Create;
use Activitypub\Handler\Delete;
use Activitypub\Handler\Follow;
use Activitypub\Handler\Undo;
use Activitypub\Handler\Update;
/**
* Handler class.
*/
class Handler {
/**
* Initialize the class, registering WordPress hooks
*/
public static function init() {
self::register_handlers();
}
/**
* Register handlers.
*/
public static function register_handlers() {
Create::init();
Delete::init();
Follow::init();
Undo::init();
Update::init();
do_action( 'activitypub_register_handlers' );
}
}

View file

@ -39,6 +39,11 @@ class Health_Check {
'test' => array( self::class, 'test_webfinger' ), 'test' => array( self::class, 'test_webfinger' ),
); );
$tests['direct']['activitypub_test_system_cron'] = array(
'label' => __( 'System Cron Test', 'activitypub' ),
'test' => array( self::class, 'test_system_cron' ),
);
return $tests; return $tests;
} }

View file

@ -20,7 +20,7 @@ class Http {
* @return array|WP_Error The POST Response or an WP_ERROR * @return array|WP_Error The POST Response or an WP_ERROR
*/ */
public static function post( $url, $body, $user_id ) { public static function post( $url, $body, $user_id ) {
\do_action( 'activitypub_pre_http_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' ); $date = \gmdate( 'D, d M Y H:i:s T' );
$digest = Signature::generate_digest( $body ); $digest = Signature::generate_digest( $body );
@ -70,7 +70,7 @@ class Http {
* @return array|WP_Error The GET Response or an WP_ERROR * @return array|WP_Error The GET Response or an WP_ERROR
*/ */
public static function get( $url ) { public static function get( $url ) {
\do_action( 'activitypub_pre_http_get', $url ); do_action( 'activitypub_pre_http_get', $url );
$date = \gmdate( 'D, d M Y H:i:s T' ); $date = \gmdate( 'D, d M Y H:i:s T' );
$signature = Signature::generate_signature( Users::APPLICATION_USER_ID, 'get', $url, $date ); $signature = Signature::generate_signature( Users::APPLICATION_USER_ID, 'get', $url, $date );
@ -108,24 +108,4 @@ class Http {
return $response; return $response;
} }
/**
* Check for URL for Tombstone.
*
* @param string $url The URL to check.
*
* @return bool True if the URL is a tombstone.
*/
public static function is_tombstone( $url ) {
\do_action( 'activitypub_pre_http_is_tombstone', $url );
$response = \wp_safe_remote_get( $url );
$code = \wp_remote_retrieve_response_code( $response );
if ( in_array( (int) $code, array( 404, 410 ), true ) ) {
return true;
}
return false;
}
} }

View file

@ -115,11 +115,6 @@ class Migration {
if ( version_compare( $version_from_db, '1.0.0', '<' ) ) { if ( version_compare( $version_from_db, '1.0.0', '<' ) ) {
self::migrate_from_0_17(); self::migrate_from_0_17();
} }
if ( version_compare( $version_from_db, '1.3.0', '<' ) ) {
self::migrate_from_1_2_0();
}
if ( version_compare( $version_from_db, 'version_number_transformer_management_placeholder', '<' ) ) { if ( version_compare( $version_from_db, 'version_number_transformer_management_placeholder', '<' ) ) {
self::migrate_from_version_number_transformer_management_placeholder(); self::migrate_from_version_number_transformer_management_placeholder();
} }
@ -200,22 +195,4 @@ class Migration {
\update_option( 'activitypub_custom_post_content', $content ); \update_option( 'activitypub_custom_post_content', $content );
} }
} }
/**
* Clear the cache after updating to 1.3.0
*
* @return void
*/
private static function migrate_from_1_2_0() {
$user_ids = get_users(
array(
'fields' => 'ID',
'capability__in' => array( 'publish_posts' ),
)
);
foreach ( $user_ids as $user_id ) {
wp_cache_delete( sprintf( Followers::CACHE_KEY_INBOXES, $user_id ), 'activitypub' );
}
}
} }

View file

@ -113,7 +113,7 @@ class Scheduler {
$followers = Followers::get_outdated_followers( $number ); $followers = Followers::get_outdated_followers( $number );
foreach ( $followers as $follower ) { foreach ( $followers as $follower ) {
$meta = get_remote_metadata_by_actor( $follower->get_id(), false ); $meta = get_remote_metadata_by_actor( $follower->get_url(), false );
if ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) { if ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) {
Followers::add_error( $follower->get__id(), $meta ); Followers::add_error( $follower->get__id(), $meta );

View file

@ -127,7 +127,8 @@ class Shortcodes {
// Strip out any remaining tags. // Strip out any remaining tags.
$excerpt = \wp_strip_all_tags( $excerpt ); $excerpt = \wp_strip_all_tags( $excerpt );
$excerpt_more = \apply_filters( 'activitypub_excerpt_more', ' [&hellip;]' ); /** This filter is documented in wp-includes/formatting.php */
$excerpt_more = \apply_filters( 'excerpt_more', ' [...]' );
$excerpt_more_len = strlen( $excerpt_more ); $excerpt_more_len = strlen( $excerpt_more );
// We now have a excerpt, but we need to check it's length, it may be longer than we want for two reasons: // We now have a excerpt, but we need to check it's length, it may be longer than we want for two reasons:

View file

@ -292,8 +292,7 @@ class Signature {
if ( is_array( $headers['digest'] ) ) { if ( is_array( $headers['digest'] ) ) {
$headers['digest'] = $headers['digest'][0]; $headers['digest'] = $headers['digest'][0];
} }
$hashalg = 'sha256'; $digest = explode( '=', $headers['digest'], 2 );
$digest = explode( '=', $headers['digest'], 2 );
if ( 'SHA-256' === $digest[0] ) { if ( 'SHA-256' === $digest[0] ) {
$hashalg = 'sha256'; $hashalg = 'sha256';
} }

View file

@ -2,10 +2,14 @@
namespace Activitypub\Collection; namespace Activitypub\Collection;
use WP_Error; use WP_Error;
use Exception;
use WP_Query; use WP_Query;
use Activitypub\Http; use Activitypub\Http;
use Activitypub\Webfinger; use Activitypub\Webfinger;
use Activitypub\Model\Follower; use Activitypub\Model\Follower;
use Activitypub\Collection\Users;
use Activitypub\Activity\Activity;
use Activitypub\Activity\Base_Object;
use function Activitypub\is_tombstone; use function Activitypub\is_tombstone;
use function Activitypub\get_remote_metadata_by_actor; use function Activitypub\get_remote_metadata_by_actor;
@ -20,6 +24,136 @@ class Followers {
const POST_TYPE = 'ap_follower'; const POST_TYPE = 'ap_follower';
const CACHE_KEY_INBOXES = 'follower_inboxes_%s'; const CACHE_KEY_INBOXES = 'follower_inboxes_%s';
/**
* Register WordPress hooks/actions and register Taxonomy
*
* @return void
*/
public static function init() {
// register "followers" post_type
self::register_post_type();
\add_action( 'activitypub_inbox_follow', array( self::class, 'handle_follow_request' ), 10, 2 );
\add_action( 'activitypub_inbox_undo', array( self::class, 'handle_undo_request' ), 10, 2 );
\add_action( 'activitypub_followers_post_follow', array( self::class, 'send_follow_response' ), 10, 4 );
}
/**
* Register the "Followers" Taxonomy
*
* @return void
*/
private static function register_post_type() {
register_post_type(
self::POST_TYPE,
array(
'labels' => array(
'name' => _x( 'Followers', 'post_type plural name', 'activitypub' ),
'singular_name' => _x( 'Follower', 'post_type single name', 'activitypub' ),
),
'public' => false,
'hierarchical' => false,
'rewrite' => false,
'query_var' => false,
'delete_with_user' => false,
'can_export' => true,
'supports' => array(),
)
);
register_post_meta(
self::POST_TYPE,
'activitypub_inbox',
array(
'type' => 'string',
'single' => true,
'sanitize_callback' => array( self::class, 'sanitize_url' ),
)
);
register_post_meta(
self::POST_TYPE,
'activitypub_errors',
array(
'type' => 'string',
'single' => false,
'sanitize_callback' => function( $value ) {
if ( ! is_string( $value ) ) {
throw new Exception( 'Error message is no valid string' );
}
return esc_sql( $value );
},
)
);
register_post_meta(
self::POST_TYPE,
'activitypub_user_id',
array(
'type' => 'string',
'single' => false,
'sanitize_callback' => function( $value ) {
return esc_sql( $value );
},
)
);
register_post_meta(
self::POST_TYPE,
'activitypub_actor_json',
array(
'type' => 'string',
'single' => true,
'sanitize_callback' => function( $value ) {
return sanitize_text_field( $value );
},
)
);
do_action( 'activitypub_after_register_post_type' );
}
public static function sanitize_url( $value ) {
if ( filter_var( $value, FILTER_VALIDATE_URL ) === false ) {
return null;
}
return esc_url_raw( $value );
}
/**
* Handle the "Follow" Request
*
* @param array $object The JSON "Follow" Activity
* @param int $user_id The ID of the ID of the WordPress User
*
* @return void
*/
public static function handle_follow_request( $object, $user_id ) {
// save follower
$follower = self::add_follower( $user_id, $object['actor'] );
do_action( 'activitypub_followers_post_follow', $object['actor'], $object, $user_id, $follower );
}
/**
* Handle "Unfollow" requests
*
* @param array $object The JSON "Undo" Activity
* @param int $user_id The ID of the ID of the WordPress User
*/
public static function handle_undo_request( $object, $user_id ) {
if (
isset( $object['object'] ) &&
isset( $object['object']['type'] ) &&
'Follow' === $object['object']['type']
) {
self::remove_follower( $user_id, $object['actor'] );
}
}
/** /**
* Add new Follower * Add new Follower
* *
@ -80,17 +214,16 @@ class Followers {
} }
/** /**
* Get a Follower. * Get a Follower
* *
* @param int $user_id The ID of the WordPress User * @param int $user_id The ID of the WordPress User
* @param string $actor The Actor URL * @param string $actor The Actor URL
* *
* @return \Activitypub\Model\Follower|null The Follower object or null * @return \Activitypub\Model\Follower The Follower object
*/ */
public static function get_follower( $user_id, $actor ) { public static function get_follower( $user_id, $actor ) {
global $wpdb; global $wpdb;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$post_id = $wpdb->get_var( $post_id = $wpdb->get_var(
$wpdb->prepare( $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 = 'activitypub_user_id' AND pm.meta_value = %d AND p.guid = %s",
@ -111,29 +244,51 @@ class Followers {
} }
/** /**
* Get a Follower by Actor indepenent from the User. * Send Accept response
* *
* @param string $actor The Actor URL. * @param string $actor The Actor URL
* @param array $object The Activity object
* @param int $user_id The ID of the WordPress User
* @param Activitypub\Model\Follower $follower The Follower object
* *
* @return \Activitypub\Model\Follower|null The Follower object or null * @return void
*/ */
public static function get_follower_by_actor( $actor ) { public static function send_follow_response( $actor, $object, $user_id, $follower ) {
global $wpdb; if ( is_wp_error( $follower ) ) {
// it is not even possible to send a "Reject" because
// we can not get the Remote-Inbox
return;
}
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching // only send minimal data
$post_id = $wpdb->get_var( $object = array_intersect_key(
$wpdb->prepare( $object,
"SELECT ID FROM $wpdb->posts WHERE guid=%s", array_flip(
esc_sql( $actor ) array(
'id',
'type',
'actor',
'object',
)
) )
); );
if ( $post_id ) { $user = Users::get_by_id( $user_id );
$post = get_post( $post_id );
return Follower::init_from_cpt( $post );
}
return null; // get inbox
$inbox = $follower->get_shared_inbox();
// send "Accept" activity
$activity = new Activity();
$activity->set_type( 'Accept' );
$activity->set_object( $object );
$activity->set_actor( $user->get_id() );
$activity->set_to( $actor );
$activity->set_id( $user->get_id() . '#follow-' . \preg_replace( '~^https?://~', '', $actor ) . '-' . \time() );
$activity = $activity->to_json();
Http::post( $inbox, $activity, $user_id );
} }
/** /**
@ -182,7 +337,7 @@ class Followers {
$query = new WP_Query( $args ); $query = new WP_Query( $args );
$total = $query->found_posts; $total = $query->found_posts;
$followers = array_map( $followers = array_map(
function ( $post ) { function( $post ) {
return Follower::init_from_cpt( $post ); return Follower::init_from_cpt( $post );
}, },
$query->get_posts() $query->get_posts()
@ -199,7 +354,6 @@ class Followers {
*/ */
public static function get_all_followers() { public static function get_all_followers() {
$args = array( $args = array(
'nopaging' => true,
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
'meta_query' => array( 'meta_query' => array(
'relation' => 'AND', 'relation' => 'AND',
@ -268,7 +422,6 @@ class Followers {
// get all Followers of a ID of the WordPress User // get all Followers of a ID of the WordPress User
$posts = new WP_Query( $posts = new WP_Query(
array( array(
'nopaging' => true,
'post_type' => self::POST_TYPE, 'post_type' => self::POST_TYPE,
'fields' => 'ids', 'fields' => 'ids',
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query

View file

@ -1,235 +0,0 @@
<?php
namespace Activitypub\Collection;
use WP_Error;
use WP_Comment_Query;
use function Activitypub\url_to_commentid;
use function Activitypub\object_id_to_comment;
use function Activitypub\get_remote_metadata_by_actor;
/**
* ActivityPub Interactions Collection
*/
class Interactions {
/**
* Add a comment to a post
*
* @param array $activity The activity-object
*
* @return array|false The commentdata or false on failure
*/
public static function add_comment( $activity ) {
if (
! isset( $activity['object'] ) ||
! isset( $activity['object']['id'] )
) {
return false;
}
if ( ! isset( $activity['object']['inReplyTo'] ) ) {
return false;
}
$in_reply_to = \esc_url_raw( $activity['object']['inReplyTo'] );
$comment_post_id = \url_to_postid( $in_reply_to );
$parent_comment = object_id_to_comment( $in_reply_to );
// save only replys and reactions
if ( ! $comment_post_id && $parent_comment ) {
$comment_post_id = $parent_comment->comment_post_ID;
}
// not a reply to a post or comment
if ( ! $comment_post_id ) {
return false;
}
$meta = get_remote_metadata_by_actor( $activity['actor'] );
if ( ! $meta || \is_wp_error( $meta ) ) {
return false;
}
$commentdata = array(
'comment_post_ID' => $comment_post_id,
'comment_author' => \esc_attr( $meta['name'] ),
'comment_author_url' => \esc_url_raw( $meta['url'] ),
'comment_content' => \addslashes( $activity['object']['content'] ),
'comment_type' => 'comment',
'comment_author_email' => '',
'comment_parent' => $parent_comment ? $parent_comment->comment_ID : 0,
'comment_meta' => array(
'source_id' => \esc_url_raw( $activity['object']['id'] ),
'source_url' => \esc_url_raw( $activity['object']['url'] ),
'protocol' => 'activitypub',
),
);
if ( isset( $meta['icon']['url'] ) ) {
$commentdata['comment_meta']['avatar_url'] = \esc_url_raw( $meta['icon']['url'] );
}
// disable flood control
\remove_action( 'check_comment_flood', 'check_comment_flood_db', 10 );
// do not require email for AP entries
\add_filter( 'pre_option_require_name_email', '__return_false' );
// No nonce possible for this submission route
\add_filter(
'akismet_comment_nonce',
function () {
return 'inactive';
}
);
\add_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ), 10, 2 );
$comment = \wp_new_comment( $commentdata, true );
\remove_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ), 10 );
\remove_filter( 'pre_option_require_name_email', '__return_false' );
// re-add flood control
\add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 );
return $comment;
}
/**
* Update a comment
*
* @param array $activity The activity-object
*
* @return array|false The commentdata or false on failure
*/
public static function update_comment( $activity ) {
$meta = get_remote_metadata_by_actor( $activity['actor'] );
//Determine comment_ID
$object_comment_id = url_to_commentid( \esc_url_raw( $activity['object']['id'] ) );
if ( ! $object_comment_id ) {
return false;
}
//found a local comment id
$commentdata = \get_comment( $object_comment_id, ARRAY_A );
$commentdata['comment_author'] = \esc_attr( $meta['name'] ? $meta['name'] : $meta['preferredUsername'] );
$commentdata['comment_content'] = \addslashes( $activity['object']['content'] );
if ( isset( $meta['icon']['url'] ) ) {
$commentdata['comment_meta']['avatar_url'] = \esc_url_raw( $meta['icon']['url'] );
}
// disable flood control
\remove_action( 'check_comment_flood', 'check_comment_flood_db', 10 );
// do not require email for AP entries
\add_filter( 'pre_option_require_name_email', '__return_false' );
// No nonce possible for this submission route
\add_filter(
'akismet_comment_nonce',
function () {
return 'inactive';
}
);
\add_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ), 10, 2 );
$comment = \wp_update_comment( $commentdata, true );
\remove_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ), 10 );
\remove_filter( 'pre_option_require_name_email', '__return_false' );
// re-add flood control
\add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 );
return $comment;
}
/**
* Get interaction(s) for a given URL/ID.
*
* @param strin $url The URL/ID to get interactions for.
*
* @return array The interactions as WP_Comment objects.
*/
public static function get_interaction_by_id( $url ) {
$args = array(
'nopaging' => true,
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
'meta_query' => array(
'relation' => 'AND',
array(
'key' => 'protocol',
'value' => 'activitypub',
),
array(
'relation' => 'OR',
array(
'key' => 'source_url',
'value' => $url,
),
array(
'key' => 'source_id',
'value' => $url,
),
),
),
);
$query = new WP_Comment_Query( $args );
return $query->comments;
}
/**
* Get interaction(s) for a given actor.
*
* @param string $actor The Actor-URL.
*
* @return array The interactions as WP_Comment objects.
*/
public static function get_interactions_by_actor( $actor ) {
$meta = get_remote_metadata_by_actor( $actor );
// get URL, because $actor seems to be the ID
if ( $meta && ! is_wp_error( $meta ) && isset( $meta['url'] ) ) {
$actor = $meta['url'];
}
$args = array(
'nopaging' => true,
'author_url' => $actor,
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
'meta_query' => array(
array(
'key' => 'protocol',
'value' => 'activitypub',
'compare' => '=',
),
),
);
$comment_query = new WP_Comment_Query( $args );
return $comment_query->comments;
}
/**
* Adds line breaks to the list of allowed comment tags.
*
* @param array $allowed_tags Allowed HTML tags.
* @param string $context Context.
*
* @return array Filtered tag list.
*/
public static function allowed_comment_html( $allowed_tags, $context = '' ) {
if ( 'pre_comment_content' !== $context ) {
// Do nothing.
return $allowed_tags;
}
// Add `p` and `br` to the list of allowed tags.
if ( ! array_key_exists( 'br', $allowed_tags ) ) {
$allowed_tags['br'] = array();
}
if ( ! array_key_exists( 'p', $allowed_tags ) ) {
$allowed_tags['p'] = array();
}
return $allowed_tags;
}
}

View file

@ -7,7 +7,6 @@ use Activitypub\Model\User;
use Activitypub\Model\Blog_User; use Activitypub\Model\Blog_User;
use Activitypub\Model\Application_User; use Activitypub\Model\Application_User;
use function Activitypub\url_to_authorid;
use function Activitypub\is_user_disabled; use function Activitypub\is_user_disabled;
class Users { class Users {
@ -104,8 +103,6 @@ class Users {
return self::get_by_id( $user->results[0] ); return self::get_by_id( $user->results[0] );
} }
$username = str_replace( array( '*', '%' ), '', $username );
// check for login or nicename. // check for login or nicename.
$user = new WP_User_Query( $user = new WP_User_Query(
array( array(
@ -136,79 +133,29 @@ class Users {
* @return \Acitvitypub\Model\User The User. * @return \Acitvitypub\Model\User The User.
*/ */
public static function get_by_resource( $resource ) { public static function get_by_resource( $resource ) {
$scheme = 'acct'; if ( \strpos( $resource, '@' ) === false ) {
$match = array(); return new WP_Error(
// try to extract the scheme and the host 'activitypub_unsupported_resource',
if ( preg_match( '/^([a-zA-Z^:]+):(.*)$/i', $resource, $match ) ) { \__( 'Resource is invalid', 'activitypub' ),
// extract the scheme array( 'status' => 400 )
$scheme = esc_attr( $match[1] ); );
} }
switch ( $scheme ) { $resource = \str_replace( 'acct:', '', $resource );
// check for http(s) URIs
case 'http':
case 'https':
$url_parts = wp_parse_url( $resource );
// check for http(s)://blog.example.com/@username $resource_identifier = \substr( $resource, 0, \strrpos( $resource, '@' ) );
if ( $resource_host = self::normalize_host( \substr( \strrchr( $resource, '@' ), 1 ) );
isset( $url_parts['path'] ) && $blog_host = self::normalize_host( \wp_parse_url( \home_url( '/' ), \PHP_URL_HOST ) );
str_starts_with( $url_parts['path'], '/@' )
) {
$identifier = str_replace( '/@', '', $url_parts['path'] );
$identifier = untrailingslashit( $identifier );
return self::get_by_username( $identifier ); if ( $blog_host !== $resource_host ) {
} return new WP_Error(
'activitypub_wrong_host',
// check for http(s)://blog.example.com/author/username \__( 'Resource host does not match blog host', 'activitypub' ),
$user_id = url_to_authorid( $resource ); array( 'status' => 404 )
);
if ( $user_id ) {
return self::get_by_id( $user_id );
}
// check for http(s)://blog.example.com/
if (
self::normalize_url( site_url() ) === self::normalize_url( $resource ) ||
self::normalize_url( home_url() ) === self::normalize_url( $resource )
) {
return self::get_by_id( self::BLOG_USER_ID );
}
return new WP_Error(
'activitypub_no_user_found',
\__( 'User not found', 'activitypub' ),
array( 'status' => 404 )
);
// check for acct URIs
case 'acct':
$resource = \str_replace( 'acct:', '', $resource );
$identifier = \substr( $resource, 0, \strrpos( $resource, '@' ) );
$host = self::normalize_host( \substr( \strrchr( $resource, '@' ), 1 ) );
$blog_host = self::normalize_host( \wp_parse_url( \home_url( '/' ), \PHP_URL_HOST ) );
if ( $blog_host !== $host ) {
return new WP_Error(
'activitypub_wrong_host',
\__( 'Resource host does not match blog host', 'activitypub' ),
array( 'status' => 404 )
);
}
// prepare wildcards https://github.com/mastodon/mastodon/issues/22213
if ( in_array( $identifier, array( '_', '*', '' ), true ) ) {
return self::get_by_id( self::BLOG_USER_ID );
}
return self::get_by_username( $identifier );
default:
return new WP_Error(
'activitypub_wrong_scheme',
\__( 'Wrong scheme', 'activitypub' ),
array( 'status' => 404 )
);
} }
return self::get_by_username( $resource_identifier );
} }
/** /**
@ -221,12 +168,7 @@ class Users {
public static function get_by_various( $id ) { public static function get_by_various( $id ) {
if ( is_numeric( $id ) ) { if ( is_numeric( $id ) ) {
return self::get_by_id( $id ); return self::get_by_id( $id );
} elseif ( } elseif ( filter_var( $id, FILTER_VALIDATE_URL ) ) {
// is URL
filter_var( $id, FILTER_VALIDATE_URL ) ||
// is acct
str_starts_with( $id, 'acct:' )
) {
return self::get_by_resource( $id ); return self::get_by_resource( $id );
} else { } else {
return self::get_by_username( $id ); return self::get_by_username( $id );
@ -234,7 +176,7 @@ class Users {
} }
/** /**
* Normalize a host. * Normalize the host.
* *
* @param string $host The host. * @param string $host The host.
* *
@ -244,22 +186,6 @@ class Users {
return \str_replace( 'www.', '', $host ); return \str_replace( 'www.', '', $host );
} }
/**
* Normalize a URL.
*
* @param string $url The URL.
*
* @return string The normalized URL.
*/
public static function normalize_url( $url ) {
$url = \untrailingslashit( $url );
$url = \str_replace( 'https://', '', $url );
$url = \str_replace( 'http://', '', $url );
$url = \str_replace( 'www.', '', $url );
return $url;
}
/** /**
* Get the User collection. * Get the User collection.
* *

View file

@ -44,35 +44,6 @@ if ( ! function_exists( 'is_countable' ) ) {
* @return bool True if `$value` is countable, otherwise false. * @return bool True if `$value` is countable, otherwise false.
*/ */
function is_countable( $value ) { function is_countable( $value ) {
return is_array( $value ) || $value instanceof \Countable; return is_array( $value ) || $value instanceof \Countable;
}
}
/**
* Polyfill for `array_is_list()` function added in PHP 7.3.
*
* @param array $array The array to check.
*
* @return bool True if `$array` is a list, otherwise false.
*/
if ( ! function_exists( 'array_is_list' ) ) {
function array_is_list( $array ) {
if ( ! is_array( $array ) ) {
return false;
}
if ( array_values( $array ) === $array ) {
return true;
}
$next_key = -1;
foreach ( $array as $k => $v ) {
if ( ++$next_key !== $k ) {
return false;
}
}
return true;
} }
} }

View file

@ -2,7 +2,6 @@
namespace Activitypub; namespace Activitypub;
use WP_Error; use WP_Error;
use WP_Comment_Query;
use Activitypub\Http; use Activitypub\Http;
use Activitypub\Activity\Activity; use Activitypub\Activity\Activity;
use Activitypub\Collection\Followers; use Activitypub\Collection\Followers;
@ -487,81 +486,6 @@ function is_blog_public() {
return (bool) apply_filters( 'activitypub_is_blog_public', \get_option( 'blog_public', 1 ) ); return (bool) apply_filters( 'activitypub_is_blog_public', \get_option( 'blog_public', 1 ) );
} }
/**
* Sanitize a URL
*
* @param string $value The URL to sanitize
*
* @return string|null The sanitized URL or null if invalid
*/
function sanitize_url( $value ) {
if ( filter_var( $value, FILTER_VALIDATE_URL ) === false ) {
return null;
}
return esc_url_raw( $value );
}
/**
* Extract recipient URLs from Activity object
*
* @param array $data
*
* @return array The list of user URLs
*/
function extract_recipients_from_activity( $data ) {
$recipient_items = array();
foreach ( array( 'to', 'bto', 'cc', 'bcc', 'audience' ) as $i ) {
if ( array_key_exists( $i, $data ) ) {
if ( is_array( $data[ $i ] ) ) {
$recipient = $data[ $i ];
} else {
$recipient = array( $data[ $i ] );
}
$recipient_items = array_merge( $recipient_items, $recipient );
}
if ( is_array( $data['object'] ) && array_key_exists( $i, $data['object'] ) ) {
if ( is_array( $data['object'][ $i ] ) ) {
$recipient = $data['object'][ $i ];
} else {
$recipient = array( $data['object'][ $i ] );
}
$recipient_items = array_merge( $recipient_items, $recipient );
}
}
$recipients = array();
// flatten array
foreach ( $recipient_items as $recipient ) {
if ( is_array( $recipient ) ) {
// check if recipient is an object
if ( array_key_exists( 'id', $recipient ) ) {
$recipients[] = $recipient['id'];
}
} else {
$recipients[] = $recipient;
}
}
return array_unique( $recipients );
}
/**
* Check if passed Activity is Public
*
* @param array $data The Activity object as array
*
* @return boolean True if public, false if not
*/
function is_activity_public( $data ) {
$recipients = extract_recipients_from_activity( $data );
return in_array( 'https://www.w3.org/ns/activitystreams#Public', $recipients, true );
}
/** /**
* Get active users based on a given duration * Get active users based on a given duration
* *
@ -633,105 +557,3 @@ function get_total_users() {
return $users + 1; return $users + 1;
} }
/**
* Examine a comment ID and look up an existing comment it represents.
*
* @param string $id ActivityPub object ID (usually a URL) to check.
*
* @return int|boolean Comment ID, or false on failure.
*/
function object_id_to_comment( $id ) {
$comment_query = new WP_Comment_Query(
array(
'meta_key' => 'source_id', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
'meta_value' => $id, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
)
);
if ( ! $comment_query->comments ) {
return false;
}
if ( count( $comment_query->comments ) > 1 ) {
return false;
}
return $comment_query->comments[0];
}
/**
* Verify if URL is a local comment,
* Or if it is a previously received remote comment
* (For threading comments locally)
*
* @param string $url The URL to check.
*
* @return int comment_ID or null if not found
*/
function url_to_commentid( $url ) {
if ( ! $url || ! filter_var( $url, FILTER_VALIDATE_URL ) ) {
return null;
}
$args = array(
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
'meta_query' => array(
'relation' => 'OR',
array(
'key' => 'source_url',
'value' => $url,
),
array(
'key' => 'source_id',
'value' => $url,
),
),
);
$query = new \WP_Comment_Query();
$comments = $query->query( $args );
if ( $comments && is_array( $comments ) ) {
return $comments[0]->comment_ID;
}
return null;
}
/**
* Get the URI of an ActivityPub object
*
* @param array $object The ActivityPub object
*
* @return string The URI of the ActivityPub object
*/
function object_to_uri( $object ) {
// check if it is already simple
if ( ! $object || is_string( $object ) ) {
return $object;
}
// check if it is a list, then take first item
// this plugin does not support collections
if ( array_is_list( $object ) ) {
$object = $object[0];
}
// check if it is simplified now
if ( is_string( $object ) ) {
return $object;
}
// return part of Object that makes most sense
switch ( $object['type'] ) {
case 'Link':
$object = $object['href'];
break;
default:
$object = $object['id'];
break;
}
return $object;
}

View file

@ -1,61 +0,0 @@
<?php
namespace Activitypub\Handler;
use WP_Error;
use Activitypub\Collection\Interactions;
use function Activitypub\is_activity_public;
use function Activitypub\object_id_to_comment;
/**
* Handle Create requests
*/
class Create {
/**
* Initialize the class, registering WordPress hooks
*/
public static function init() {
\add_action( 'activitypub_inbox_create', array( self::class, 'handle_create' ), 10, 3 );
}
/**
* Handles "Create" requests
*
* @param array $array The activity-object
* @param int $user_id The id of the local blog-user
* @param Activitypub\Activity $object The activity object
*
* @return void
*/
public static function handle_create( $array, $user_id, $object = null ) {
if (
! isset( $array['object'] ) ||
! isset( $array['object']['id'] )
) {
return;
}
// check if Activity is public or not
if ( ! is_activity_public( $array ) ) {
// @todo maybe send email
return;
}
$check_dupe = object_id_to_comment( $array['object']['id'] );
// if comment exists, call update action
if ( $check_dupe ) {
\do_action( 'activitypub_inbox_update', $array, $user_id, $object );
return;
}
$state = Interactions::add_comment( $array );
$reaction = null;
if ( $state && ! \is_wp_error( $reaction ) ) {
$reaction = \get_comment( $state );
}
\do_action( 'activitypub_handled_create', $array, $user_id, $state, $reaction );
}
}

View file

@ -1,165 +0,0 @@
<?php
namespace Activitypub\Handler;
use WP_Error;
use WP_REST_Request;
use Activitypub\Http;
use Activitypub\Collection\Followers;
use Activitypub\Collection\Interactions;
/**
* Handles Delete requests.
*/
class Delete {
/**
* Initialize the class, registering WordPress hooks
*/
public static function init() {
\add_action( 'activitypub_inbox_delete', array( self::class, 'handle_delete' ), 10, 2 );
// defer signature verification for `Delete` requests.
\add_filter( 'activitypub_defer_signature_verification', array( self::class, 'defer_signature_verification' ), 10, 2 );
// side effect
\add_action( 'activitypub_delete_actor_interactions', array( self::class, 'delete_interactions' ), 10, 1 );
}
/**
* Handles "Delete" requests.
*
* @param array $activity The delete activity.
* @param int $user_id The ID of the user performing the delete activity.
*/
public static function handle_delete( $activity, $user_id ) {
$object_type = isset( $activity['object']['type'] ) ? $activity['object']['type'] : '';
switch ( $object_type ) {
// Actor Types
// @see https://www.w3.org/TR/activitystreams-vocabulary/#actor-types
case 'Person':
case 'Group':
case 'Organization':
case 'Service':
case 'Application':
self::maybe_delete_follower( $user_id, $activity );
break;
// Object and Link Types
// @see https://www.w3.org/TR/activitystreams-vocabulary/#object-types
case 'Note':
case 'Article':
case 'Image':
case 'Audio':
case 'Video':
case 'Event':
case 'Document':
self::maybe_delete_interaction( $activity );
break;
// Tombstone Type
// @see: https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tombstone
case 'Tombstone':
self::maybe_delete_interaction( $activity );
break;
// Minimal Activity
// @see https://www.w3.org/TR/activitystreams-core/#example-1
default:
// ignore non Minimal Activities.
if ( ! is_string( $activity['object'] ) ) {
return;
}
// check if Object is an Actor.
if ( $activity['actor'] === $activity['object'] ) {
self::maybe_delete_follower( $activity );
self::maybe_delete_interactions( $activity );
} else { // assume a interaction otherwise.
self::maybe_delete_interaction( $activity );
}
// maybe handle Delete Activity for other Object Types.
break;
}
}
/**
* Delete a Follower if Actor-URL is a Tombstone.
*
* @param array $activity The delete activity.
*/
public static function maybe_delete_follower( $activity ) {
$follower = Followers::get_follower_by_actor( $activity['actor'] );
// verify if Actor is deleted.
if ( $follower && Http::is_tombstone( $activity['actor'] ) ) {
$follower->delete();
}
}
/**
* Delete Reactions if Actor-URL is a Tombstone.
*
* @param array $activity The delete activity.
*/
public static function maybe_delete_interactions( $activity ) {
// verify if Actor is deleted.
if ( Http::is_tombstone( $activity['actor'] ) ) {
\wp_schedule_single_event(
\time(),
'activitypub_delete_actor_interactions',
array( $activity['actor'] )
);
}
}
/**
* Delete comments from an Actor.
*
* @param array $comments The comments to delete.
*/
public static function delete_interactions( $actor ) {
$comments = Interactions::get_interactions_by_actor( $actor );
if ( is_array( $comments ) ) {
foreach ( $comments as $comment ) {
wp_delete_comment( $comment->comment_ID );
}
}
}
/**
* Delete a Reaction if URL is a Tombstone.
*
* @param array $activity The delete activity.
*
* @return void
*/
public static function maybe_delete_interaction( $activity ) {
if ( is_array( $activity['object'] ) ) {
$id = $activity['object']['id'];
} else {
$id = $activity['object'];
}
$comments = Interactions::get_interaction_by_id( $id );
if ( $comments && Http::is_tombstone( $id ) ) {
foreach ( $comments as $comment ) {
wp_delete_comment( $comment->comment_ID, true );
}
}
}
/**
* Defer signature verification for `Delete` requests.
*
* @param bool $defer Whether to defer signature verification.
* @param WP_REST_Request $request The request object.
*
* @return bool Whether to defer signature verification.
*/
public static function defer_signature_verification( $defer, $request ) {
$json = $request->get_json_params();
if ( isset( $json['type'] ) && 'Delete' === $json['type'] ) {
return true;
}
return false;
}
}

View file

@ -1,81 +0,0 @@
<?php
namespace Activitypub\Handler;
use Activitypub\Http;
use Activitypub\Activity\Activity;
use Activitypub\Collection\Users;
use Activitypub\Collection\Followers;
/**
* Handle Follow requests
*/
class Follow {
/**
* Initialize the class, registering WordPress hooks
*/
public static function init() {
\add_action( 'activitypub_inbox_follow', array( self::class, 'handle_follow' ), 10, 2 );
\add_action( 'activitypub_followers_post_follow', array( self::class, 'send_follow_response' ), 10, 4 );
}
/**
* Handle "Follow" requests
*
* @param array $activity The activity object
* @param int $user_id The user ID
*/
public static function handle_follow( $activity, $user_id ) {
// save follower
$follower = Followers::add_follower( $user_id, $activity['actor'] );
do_action( 'activitypub_followers_post_follow', $activity['actor'], $activity, $user_id, $follower );
}
/**
* Send Accept response
*
* @param string $actor The Actor URL
* @param array $object The Activity object
* @param int $user_id The ID of the WordPress User
* @param Activitypub\Model\Follower $follower The Follower object
*
* @return void
*/
public static function send_follow_response( $actor, $object, $user_id, $follower ) {
if ( \is_wp_error( $follower ) ) {
// it is not even possible to send a "Reject" because
// we can not get the Remote-Inbox
return;
}
// only send minimal data
$object = array_intersect_key(
$object,
array_flip(
array(
'id',
'type',
'actor',
'object',
)
)
);
$user = Users::get_by_id( $user_id );
// get inbox
$inbox = $follower->get_shared_inbox();
// send "Accept" activity
$activity = new Activity();
$activity->set_type( 'Accept' );
$activity->set_object( $object );
$activity->set_actor( $user->get_id() );
$activity->set_to( $actor );
$activity->set_id( $user->get_id() . '#follow-' . \preg_replace( '~^https?://~', '', $actor ) . '-' . \time() );
$activity = $activity->to_json();
Http::post( $inbox, $activity, $user_id );
}
}

View file

@ -1,31 +0,0 @@
<?php
namespace Activitypub\Handler;
use Activitypub\Collection\Followers;
/**
* Handle Undo requests
*/
class Undo {
/**
* Initialize the class, registering WordPress hooks
*/
public static function init() {
\add_action( 'activitypub_inbox_undo', array( self::class, 'handle_undo' ), 10, 2 );
}
/**
* Handle "Unfollow" requests
*
* @param array $activity The JSON "Undo" Activity
* @param int $user_id The ID of the ID of the WordPress User
*/
public static function handle_undo( $activity, $user_id ) {
if (
isset( $activity['object']['type'] ) &&
'Follow' === $activity['object']['type']
) {
Followers::remove_follower( $user_id, $activity['actor'] );
}
}
}

View file

@ -1,89 +0,0 @@
<?php
namespace Activitypub\Handler;
use WP_Error;
use Activitypub\Collection\Interactions;
use function Activitypub\get_remote_metadata_by_actor;
/**
* Handle Update requests.
*/
class Update {
/**
* Initialize the class, registering WordPress hooks
*/
public static function init() {
\add_action( 'activitypub_inbox_update', array( self::class, 'handle_update' ), 10, 2 );
}
/**
* Handle "Update" requests
*
* @param array $array The activity-object
* @param int $user_id The id of the local blog-user
*/
public static function handle_update( $array, $user_id ) {
$object_type = isset( $array['object']['type'] ) ? $array['object']['type'] : '';
switch ( $object_type ) {
// Actor Types
// @see https://www.w3.org/TR/activitystreams-vocabulary/#actor-types
case 'Person':
case 'Group':
case 'Organization':
case 'Service':
case 'Application':
self::update_actor( $array );
break;
// Object and Link Types
// @see https://www.w3.org/TR/activitystreams-vocabulary/#object-types
case 'Note':
case 'Article':
case 'Image':
case 'Audio':
case 'Video':
case 'Event':
case 'Document':
self::update_interaction( $array, $user_id );
break;
// Minimal Activity
// @see https://www.w3.org/TR/activitystreams-core/#example-1
default:
break;
}
}
/**
* Update an Interaction
*
* @param array $activity The activity-object
* @param int $user_id The id of the local blog-user
*
* @return void
*/
public static function update_interaction( $activity, $user_id ) {
$state = Interactions::update_comment( $activity );
$reaction = null;
if ( $state && ! \is_wp_error( $reaction ) ) {
$reaction = \get_comment( $state );
}
\do_action( 'activitypub_handled_update', $activity, $user_id, $state, $reaction );
}
/**
* Update an Actor
*
* @param array $activity The activity-object
*
* @return void
*/
public static function update_actor( $activity ) {
// update cache
get_remote_metadata_by_actor( $activity['actor'], false );
// @todo maybe also update all interactions
}
}

View file

@ -12,7 +12,7 @@
'<dt><code>[ap_content apply_filters="yes"]</code></dt>' . '<dt><code>[ap_content apply_filters="yes"]</code></dt>' .
'<dd>' . \wp_kses( __( 'The post\'s content. With <code>apply_filters</code> you can decide if filters (<code>apply_filters( \'the_content\', $content )</code>) should be applied or not (default is <code>yes</code>). The values can be <code>yes</code> or <code>no</code>. <code>apply_filters</code> attribute is optional.', 'activitypub' ), array( 'code' => array() ) ) . '</dd>' . '<dd>' . \wp_kses( __( 'The post\'s content. With <code>apply_filters</code> you can decide if filters (<code>apply_filters( \'the_content\', $content )</code>) should be applied or not (default is <code>yes</code>). The values can be <code>yes</code> or <code>no</code>. <code>apply_filters</code> attribute is optional.', 'activitypub' ), array( 'code' => array() ) ) . '</dd>' .
'<dt><code>[ap_excerpt length="400"]</code></dt>' . '<dt><code>[ap_excerpt length="400"]</code></dt>' .
'<dd>' . \wp_kses( __( 'The post\'s excerpt (uses <code>the_excerpt</code> if that is set). If no excerpt is provided, will truncate at <code>length</code> (optional, default = 400).', 'activitypub' ), array( 'code' => array() ) ) . '</dd>' . '<dd>' . \wp_kses( __( 'The post\'s excerpt (default 400 chars). <code>length</code> attribute is optional.', 'activitypub' ), array( 'code' => array() ) ) . '</dd>' .
'<dt><code>[ap_permalink type="url"]</code></dt>' . '<dt><code>[ap_permalink type="url"]</code></dt>' .
'<dd>' . \wp_kses( __( 'The post\'s permalink. <code>type</code> can be either: <code>url</code> or <code>html</code> (an &lt;a /&gt; tag). <code>type</code> attribute is optional.', 'activitypub' ), array( 'code' => array() ) ) . '</dd>' . '<dd>' . \wp_kses( __( 'The post\'s permalink. <code>type</code> can be either: <code>url</code> or <code>html</code> (an &lt;a /&gt; tag). <code>type</code> attribute is optional.', 'activitypub' ), array( 'code' => array() ) ) . '</dd>' .
'<dt><code>[ap_shortlink type="url"]</code></dt>' . '<dt><code>[ap_shortlink type="url"]</code></dt>' .

View file

@ -38,15 +38,6 @@ class Application_User extends Blog_User {
return get_rest_url_by_path( 'application' ); return get_rest_url_by_path( 'application' );
} }
/**
* Returns the User-URL with @-Prefix for the username.
*
* @return string The User-URL with @-Prefix for the username.
*/
public function get_alternate_url() {
return \esc_url( \trailingslashit( get_home_url() ) . '@' . $this->get_preferred_username() );
}
public function get_name() { public function get_name() {
return 'application'; return 'application';
} }
@ -78,8 +69,4 @@ class Application_User extends Blog_User {
public function get_indexable() { public function get_indexable() {
return false; return false;
} }
public function get_type() {
return $this->type;
}
} }

View file

@ -55,9 +55,9 @@ class Blog_User extends User {
*/ */
public function get_type() { public function get_type() {
if ( is_single_user() ) { if ( is_single_user() ) {
return 'Application'; return 'Person';
} else { } else {
return 'Application'; return 'Group';
} }
} }
@ -100,12 +100,12 @@ class Blog_User extends User {
} }
/** /**
* Get blog's homepage URL. * Returns the User-URL with @-Prefix for the username.
* *
* @return string The User-Url. * @return string The User-URL with @-Prefix for the username.
*/ */
public function get_alternate_url() { public function get_at_url() {
return \esc_url( \trailingslashit( get_home_url() ) ); return \esc_url( \trailingslashit( get_home_url() ) . '@' . $this->get_preferred_username() );
} }
/** /**

View file

@ -0,0 +1,132 @@
<?php
namespace Activitypub\Model;
use Activitypub\Transformer\Post as Transformer_Post;
/**
* ActivityPub Post Class
*
* @author Matthias Pfefferle
*/
class Post {
/**
* The \Activitypub\Activity\Base_Object object.
*
* @var \Activitypub\Activity\Base_Object
*/
protected $object;
/**
* The WordPress Post Object.
*
* @var WP_Post
*/
private $post;
/**
* Constructor
*
* @param WP_Post $post
* @param int $post_author
*/
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed, VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
public function __construct( $post, $post_author = null ) {
_deprecated_function( __CLASS__, '1.0.0', '\Activitypub\Transformer\Post' );
$this->post = $post;
$transformer = new Transformer_Post();
$this->object = $transformer->set_wp_post( $post )->to_object();
}
/**
* Returns the User ID.
*
* @return int the User ID.
*/
public function get_user_id() {
return apply_filters( 'activitypub_post_user_id', $this->post->post_author, $this->post );
}
/**
* Converts this Object into an Array.
*
* @return array the array representation of a Post.
*/
public function to_array() {
return \apply_filters( 'activitypub_post', $this->object->to_array(), $this->post );
}
/**
* Returns the Actor of this Object.
*
* @return string The URL of the Actor.
*/
public function get_actor() {
$user = User_Factory::get_by_id( $this->get_user_id() );
return $user->get_url();
}
/**
* Converts this Object into a JSON String
*
* @return string
*/
public function to_json() {
return \wp_json_encode( $this->to_array(), \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT );
}
/**
* Returns the URL of an Activity Object
*
* @return string
*/
public function get_url() {
return $this->object->get_url();
}
/**
* Returns the ID of an Activity Object
*
* @return string
*/
public function get_id() {
return $this->object->get_id();
}
/**
* Returns a list of Image Attachments
*
* @return array
*/
public function get_attachments() {
return $this->object->get_attachment();
}
/**
* Returns a list of Tags, used in the Post
*
* @return array
*/
public function get_tags() {
return $this->object->get_tag();
}
/**
* Returns the as2 object-type for a given post
*
* @return string the object-type
*/
public function get_object_type() {
return $this->object->get_type();
}
/**
* Returns the content for the ActivityPub Item.
*
* @return string the content
*/
public function get_content() {
return $this->object->get_content();
}
}

View file

@ -41,7 +41,7 @@ class User extends Actor {
* *
* @var string * @var string
*/ */
protected $type = 'Group'; protected $type = 'Person';
/** /**
* If the User is discoverable. * If the User is discoverable.
@ -135,8 +135,8 @@ class User extends Actor {
* *
* @return string The User-URL with @-Prefix for the username. * @return string The User-URL with @-Prefix for the username.
*/ */
public function get_alternate_url() { public function get_at_url() {
return \esc_url( \trailingslashit( get_home_url() ) . '@' . $this->get_preferred_username() ); return \esc_url( \trailingslashit( get_home_url() ) . '@' . $this->get_username() );
} }
public function get_preferred_username() { public function get_preferred_username() {

View file

@ -0,0 +1,34 @@
<?php
namespace Activitypub\Peer;
/**
* ActivityPub Followers DB-Class
*
* @author Matthias Pfefferle
*/
class Followers {
public static function get_followers( $author_id ) {
_deprecated_function( __METHOD__, '1.0.0', '\Activitypub\Collection\Followers::get_followers' );
return \Activitypub\Collection\Followers::get_followers( $author_id );
}
public static function count_followers( $author_id ) {
_deprecated_function( __METHOD__, '1.0.0', '\Activitypub\Collection\Followers::count_followers' );
return \Activitypub\Collection\Followers::count_followers( $author_id );
}
public static function add_follower( $actor, $author_id ) {
_deprecated_function( __METHOD__, '1.0.0', '\Activitypub\Collection\Followers::add_follower' );
return \Activitypub\Collection\Followers::add_follower( $author_id, $actor );
}
public static function remove_follower( $actor, $author_id ) {
_deprecated_function( __METHOD__, '1.0.0', '\Activitypub\Collection\Followers::remove_follower' );
return \Activitypub\Collection\Followers::remove_follower( $author_id, $actor );
}
}

View file

@ -4,7 +4,7 @@ namespace Activitypub\Rest;
use WP_Error; use WP_Error;
use WP_REST_Server; use WP_REST_Server;
use WP_REST_Response; use WP_REST_Response;
use Activitypub\Transformer\Transformer_Factory; use Activitypub\Transformer\Transformers_Manager;
use Activitypub\Activity\Activity; use Activitypub\Activity\Activity;
use Activitypub\Collection\Users as User_Collection; use Activitypub\Collection\Users as User_Collection;
@ -168,7 +168,7 @@ class Collection {
); );
foreach ( $posts as $post ) { foreach ( $posts as $post ) {
$response['orderedItems'][] = Transformer_Factory::instance()->transform( $post ); $response['orderedItems'][] = Transformers_Manager::instance()->get_transformer( $post )->to_object()->to_array();
} }
$rest_response = new WP_REST_Response( $response, 200 ); $rest_response = new WP_REST_Response( $response, 200 );

View file

@ -94,7 +94,7 @@ class Followers {
// phpcs:ignore // phpcs:ignore
$json->orderedItems = array_map( $json->orderedItems = array_map(
function ( $item ) use ( $context ) { function( $item ) use ( $context ) {
if ( 'full' === $context ) { if ( 'full' === $context ) {
return $item->to_array(); return $item->to_array();
} }

View file

@ -8,11 +8,9 @@ use Activitypub\Activity\Activity;
use Activitypub\Collection\Users as User_Collection; use Activitypub\Collection\Users as User_Collection;
use function Activitypub\get_context; use function Activitypub\get_context;
use function Activitypub\object_to_uri;
use function Activitypub\url_to_authorid; use function Activitypub\url_to_authorid;
use function Activitypub\get_rest_url_by_path; use function Activitypub\get_rest_url_by_path;
use function Activitypub\get_remote_metadata_by_actor; use function Activitypub\get_remote_metadata_by_actor;
use function Activitypub\extract_recipients_from_activity;
/** /**
* ActivityPub Inbox REST-Class * ActivityPub Inbox REST-Class
@ -27,6 +25,8 @@ class Inbox {
*/ */
public static function init() { public static function init() {
self::register_routes(); self::register_routes();
\add_action( 'activitypub_inbox_create', array( self::class, 'handle_create' ), 10, 2 );
} }
/** /**
@ -130,13 +130,12 @@ class Inbox {
return $user; return $user;
} }
$data = $request->get_json_params(); $data = $request->get_json_params();
$activity = Activity::init_from_array( $data ); $type = $request->get_param( 'type' );
$type = $request->get_param( 'type' ); $type = \strtolower( $type );
$type = \strtolower( $type );
\do_action( 'activitypub_inbox', $data, $user->get__id(), $type, $activity ); \do_action( 'activitypub_inbox', $data, $user->get__id(), $type );
\do_action( "activitypub_inbox_{$type}", $data, $user->get__id(), $activity ); \do_action( "activitypub_inbox_{$type}", $data, $user->get__id() );
$rest_response = new WP_REST_Response( array(), 202 ); $rest_response = new WP_REST_Response( array(), 202 );
$rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) ); $rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
@ -152,10 +151,9 @@ class Inbox {
* @return WP_REST_Response * @return WP_REST_Response
*/ */
public static function shared_inbox_post( $request ) { public static function shared_inbox_post( $request ) {
$data = $request->get_json_params(); $data = $request->get_json_params();
$activity = Activity::init_from_array( $data ); $type = $request->get_param( 'type' );
$type = $request->get_param( 'type' ); $users = self::extract_recipients( $data );
$users = self::get_recipients( $data );
if ( ! $users ) { if ( ! $users ) {
return new WP_Error( return new WP_Error(
@ -183,8 +181,8 @@ class Inbox {
$type = \strtolower( $type ); $type = \strtolower( $type );
\do_action( 'activitypub_inbox', $data, $user->ID, $type, $activity ); \do_action( 'activitypub_inbox', $data, $user->ID, $type );
\do_action( "activitypub_inbox_{$type}", $data, $user->ID, $activity ); \do_action( "activitypub_inbox_{$type}", $data, $user->ID );
} }
$rest_response = new WP_REST_Response( array(), 202 ); $rest_response = new WP_REST_Response( array(), 202 );
@ -237,8 +235,15 @@ class Inbox {
$params['actor'] = array( $params['actor'] = array(
'required' => true, 'required' => true,
'sanitize_callback' => function ( $param, $request, $key ) { 'sanitize_callback' => function( $param, $request, $key ) {
return object_to_uri( $param ); if ( \is_array( $param ) ) {
if ( isset( $param['id'] ) ) {
$param = $param['id'];
} else {
$param = $param['url'];
}
}
return \esc_url_raw( $param );
}, },
); );
@ -246,7 +251,7 @@ class Inbox {
'required' => true, 'required' => true,
//'type' => 'enum', //'type' => 'enum',
//'enum' => array( 'Create' ), //'enum' => array( 'Create' ),
//'sanitize_callback' => function ( $param, $request, $key ) { //'sanitize_callback' => function( $param, $request, $key ) {
// return \strtolower( $param ); // return \strtolower( $param );
//}, //},
); );
@ -279,8 +284,11 @@ class Inbox {
$params['actor'] = array( $params['actor'] = array(
'required' => true, 'required' => true,
//'type' => array( 'object', 'string' ), //'type' => array( 'object', 'string' ),
'sanitize_callback' => function ( $param, $request, $key ) { 'sanitize_callback' => function( $param, $request, $key ) {
return object_to_uri( $param ); if ( ! \is_string( $param ) ) {
$param = $param['id'];
}
return \esc_url_raw( $param );
}, },
); );
@ -288,7 +296,7 @@ class Inbox {
'required' => true, 'required' => true,
//'type' => 'enum', //'type' => 'enum',
//'enum' => array( 'Create' ), //'enum' => array( 'Create' ),
//'sanitize_callback' => function ( $param, $request, $key ) { //'sanitize_callback' => function( $param, $request, $key ) {
// return \strtolower( $param ); // return \strtolower( $param );
//}, //},
); );
@ -300,7 +308,7 @@ class Inbox {
$params['to'] = array( $params['to'] = array(
'required' => false, 'required' => false,
'sanitize_callback' => function ( $param, $request, $key ) { 'sanitize_callback' => function( $param, $request, $key ) {
if ( \is_string( $param ) ) { if ( \is_string( $param ) ) {
$param = array( $param ); $param = array( $param );
} }
@ -310,7 +318,7 @@ class Inbox {
); );
$params['cc'] = array( $params['cc'] = array(
'sanitize_callback' => function ( $param, $request, $key ) { 'sanitize_callback' => function( $param, $request, $key ) {
if ( \is_string( $param ) ) { if ( \is_string( $param ) ) {
$param = array( $param ); $param = array( $param );
} }
@ -320,7 +328,7 @@ class Inbox {
); );
$params['bcc'] = array( $params['bcc'] = array(
'sanitize_callback' => function ( $param, $request, $key ) { 'sanitize_callback' => function( $param, $request, $key ) {
if ( \is_string( $param ) ) { if ( \is_string( $param ) ) {
$param = array( $param ); $param = array( $param );
} }
@ -332,6 +340,121 @@ class Inbox {
return $params; return $params;
} }
/**
* Handles "Create" requests
*
* @param array $object The activity-object
* @param int $user_id The id of the local blog-user
*/
public static function handle_create( $object, $user_id ) {
$meta = get_remote_metadata_by_actor( $object['actor'] );
if ( ! isset( $object['object']['inReplyTo'] ) ) {
return;
}
// check if Activity is public or not
if ( ! self::is_activity_public( $object ) ) {
// @todo maybe send email
return;
}
$comment_post_id = \url_to_postid( $object['object']['inReplyTo'] );
// save only replys and reactions
if ( ! $comment_post_id ) {
return false;
}
$commentdata = array(
'comment_post_ID' => $comment_post_id,
'comment_author' => \esc_attr( $meta['name'] ),
'comment_author_url' => \esc_url_raw( $object['actor'] ),
'comment_content' => addslashes( \wp_kses( $object['object']['content'], 'pre_comment_content' ) ),
'comment_type' => 'comment',
'comment_author_email' => '',
'comment_parent' => 0,
'comment_meta' => array(
'source_url' => \esc_url_raw( $object['object']['url'] ),
'avatar_url' => \esc_url_raw( $meta['icon']['url'] ),
'protocol' => 'activitypub',
),
);
// disable flood control
\remove_action( 'check_comment_flood', 'check_comment_flood_db', 10 );
// do not require email for AP entries
\add_filter( 'pre_option_require_name_email', '__return_false' );
// No nonce possible for this submission route
\add_filter(
'akismet_comment_nonce',
function() {
return 'inactive';
}
);
\add_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ), 10, 2 );
$state = \wp_new_comment( $commentdata, true );
\remove_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ) );
\remove_filter( 'pre_option_require_name_email', '__return_false' );
// re-add flood control
\add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 );
do_action( 'activitypub_handled_create', $object, $user_id, $state, $commentdata );
}
/**
* Extract recipient URLs from Activity object
*
* @param array $data
*
* @return array The list of user URLs
*/
public static function extract_recipients( $data ) {
$recipient_items = array();
foreach ( array( 'to', 'bto', 'cc', 'bcc', 'audience' ) as $i ) {
if ( array_key_exists( $i, $data ) ) {
if ( is_array( $data[ $i ] ) ) {
$recipient = $data[ $i ];
} else {
$recipient = array( $data[ $i ] );
}
$recipient_items = array_merge( $recipient_items, $recipient );
}
if ( is_array( $data['object'] ) && array_key_exists( $i, $data['object'] ) ) {
if ( is_array( $data['object'][ $i ] ) ) {
$recipient = $data['object'][ $i ];
} else {
$recipient = array( $data['object'][ $i ] );
}
$recipient_items = array_merge( $recipient_items, $recipient );
}
}
$recipients = array();
// flatten array
foreach ( $recipient_items as $recipient ) {
if ( is_array( $recipient ) ) {
// check if recipient is an object
if ( array_key_exists( 'id', $recipient ) ) {
$recipients[] = $recipient['id'];
}
} else {
$recipients[] = $recipient;
}
}
return array_unique( $recipients );
}
/** /**
* Get local user recipients * Get local user recipients
* *
@ -340,7 +463,7 @@ class Inbox {
* @return array The list of local users * @return array The list of local users
*/ */
public static function get_recipients( $data ) { public static function get_recipients( $data ) {
$recipients = extract_recipients_from_activity( $data ); $recipients = self::extract_recipients( $data );
$users = array(); $users = array();
foreach ( $recipients as $recipient ) { foreach ( $recipients as $recipient ) {
@ -355,4 +478,41 @@ class Inbox {
return $users; return $users;
} }
/**
* Check if passed Activity is Public
*
* @param array $data
* @return boolean
*/
public static function is_activity_public( $data ) {
$recipients = self::extract_recipients( $data );
return in_array( 'https://www.w3.org/ns/activitystreams#Public', $recipients, true );
}
/**
* Adds line breaks to the list of allowed comment tags.
*
* @param array $allowedtags Allowed HTML tags.
* @param string $context Context.
* @return array Filtered tag list.
*/
public static function allowed_comment_html( $allowedtags, $context = '' ) {
if ( 'pre_comment_content' !== $context ) {
// Do nothing.
return $allowedtags;
}
// Add `p` and `br` to the list of allowed tags.
if ( ! array_key_exists( 'br', $allowedtags ) ) {
$allowedtags['br'] = array();
}
if ( ! array_key_exists( 'p', $allowedtags ) ) {
$allowedtags['p'] = array();
}
return $allowedtags;
}
} }

View file

@ -3,9 +3,6 @@ namespace Activitypub\Rest;
use WP_REST_Response; use WP_REST_Response;
use Activitypub\Collection\Users;
use Activitypub\Model\Application_User;
use function Activitypub\get_total_users; use function Activitypub\get_total_users;
use function Activitypub\get_active_users; use function Activitypub\get_active_users;
use function Activitypub\get_rest_url_by_path; use function Activitypub\get_rest_url_by_path;
@ -108,12 +105,6 @@ class Nodeinfo {
'outbound' => array(), 'outbound' => array(),
); );
$nodeinfo['metadata'] = array(
'nodeName' => \get_bloginfo( 'name' ),
'nodeDescription' => \get_bloginfo( 'description' ),
'nodeIcon' => \get_site_icon_url(),
);
return new WP_REST_Response( $nodeinfo, 200 ); return new WP_REST_Response( $nodeinfo, 200 );
} }
@ -178,12 +169,8 @@ class Nodeinfo {
'rel' => 'http://nodeinfo.diaspora.software/ns/schema/2.0', 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/2.0',
'href' => get_rest_url_by_path( 'nodeinfo' ), 'href' => get_rest_url_by_path( 'nodeinfo' ),
), ),
array(
'rel' => 'https://www.w3.org/ns/activitystreams#Application',
'href' => get_rest_url_by_path( 'application' ),
),
); );
return new \WP_REST_Response( $discovery, 200 ); return new \WP_REST_Response( $discovery, 200 );
} }
} }

View file

@ -5,7 +5,7 @@ use stdClass;
use WP_Error; use WP_Error;
use WP_REST_Server; use WP_REST_Server;
use WP_REST_Response; use WP_REST_Response;
use Activitypub\Transformer\Transformer_Factory; use Activitypub\Transformer\Transformers_Manager;
use Activitypub\Activity\Activity; use Activitypub\Activity\Activity;
use Activitypub\Collection\Users as User_Collection; use Activitypub\Collection\Users as User_Collection;
@ -105,11 +105,12 @@ class Outbox {
); );
foreach ( $posts as $post ) { foreach ( $posts as $post ) {
$object = \Activitypub\Transformer\Transformer_Factory::instance()->transform( $post ); $transformer = \Activitypub\Transformer\Transformers_Manager::instance()->get_transformer( $post );
$post = $transformer->to_object();
$activity = new Activity(); $activity = new Activity();
$activity->set_type( 'Create' ); $activity->set_type( 'Create' );
$activity->set_context( null ); $activity->set_context( null );
$activity->set_object( $object ); $activity->set_object( $post );
$json->orderedItems[] = $activity->to_array(); // phpcs:ignore $json->orderedItems[] = $activity->to_array(); // phpcs:ignore
} }

View file

@ -110,20 +110,14 @@ class Server {
if ( 'GET' !== $request->get_method() ) { if ( 'GET' !== $request->get_method() ) {
$verified_request = Signature::verify_http_signature( $request ); $verified_request = Signature::verify_http_signature( $request );
if ( \is_wp_error( $verified_request ) ) { if ( \is_wp_error( $verified_request ) ) {
return new WP_Error( return new WP_Error( 'activitypub_signature_verification', $verified_request->get_error_message(), array( 'status' => 401 ) );
'activitypub_signature_verification',
$verified_request->get_error_message(),
array( 'status' => 401 )
);
} }
} elseif ( 'GET' === $request->get_method() && ACTIVITYPUB_AUTHORIZED_FETCH ) { // GET-Requests are only signed in secure mode } elseif ( 'GET' === $request->get_method() ) { // GET-Requests are only signed in secure mode
$verified_request = Signature::verify_http_signature( $request ); if ( ACTIVITYPUB_AUTHORIZED_FETCH ) {
if ( \is_wp_error( $verified_request ) ) { $verified_request = Signature::verify_http_signature( $request );
return new WP_Error( if ( \is_wp_error( $verified_request ) ) {
'activitypub_signature_verification', return new WP_Error( 'activitypub_signature_verification', $verified_request->get_error_message(), array( 'status' => 401 ) );
$verified_request->get_error_message(), }
array( 'status' => 401 )
);
} }
} }

View file

@ -72,7 +72,7 @@ class Webfinger {
$params['resource'] = array( $params['resource'] = array(
'required' => true, 'required' => true,
'type' => 'string', 'type' => 'string',
'pattern' => '^(acct:)|^(https?://)(.+)$', 'pattern' => '^acct:(.+)@(.+)$',
); );
return $params; return $params;
@ -94,13 +94,10 @@ class Webfinger {
$aliases = array( $aliases = array(
$user->get_url(), $user->get_url(),
$user->get_alternate_url(),
); );
$aliases = array_unique( $aliases );
$profile = array( $profile = array(
'subject' => sprintf( 'acct:%s', $user->get_resource() ), 'subject' => $resource,
'aliases' => array_values( array_unique( $aliases ) ), 'aliases' => array_values( array_unique( $aliases ) ),
'links' => array( 'links' => array(
array( array(
@ -116,12 +113,6 @@ class Webfinger {
), ),
); );
if ( 'Person' !== $user->get_type() ) {
$profile['links'][0]['properties'] = array(
'https://www.w3.org/ns/activitystreams#type' => $user->get_type(),
);
}
return $profile; return $profile;
} }
} }

View file

@ -5,8 +5,6 @@ use WP_List_Table;
use Activitypub\Collection\Users; use Activitypub\Collection\Users;
use Activitypub\Collection\Followers as FollowerCollection; use Activitypub\Collection\Followers as FollowerCollection;
use function Activitypub\object_to_uri;
if ( ! \class_exists( '\WP_List_Table' ) ) { if ( ! \class_exists( '\WP_List_Table' ) ) {
require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php'; require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
} }
@ -103,7 +101,7 @@ class Followers extends WP_List_Table {
'icon' => esc_attr( $follower->get_icon_url() ), 'icon' => esc_attr( $follower->get_icon_url() ),
'post_title' => esc_attr( $follower->get_name() ), 'post_title' => esc_attr( $follower->get_name() ),
'username' => esc_attr( $follower->get_preferred_username() ), 'username' => esc_attr( $follower->get_preferred_username() ),
'url' => esc_attr( object_to_uri( $follower->get_url() ) ), 'url' => esc_attr( $follower->get_url() ),
'identifier' => esc_attr( $follower->get_id() ), 'identifier' => esc_attr( $follower->get_id() ),
'published' => esc_attr( $follower->get_published() ), 'published' => esc_attr( $follower->get_published() ),
'modified' => esc_attr( $follower->get_updated() ), 'modified' => esc_attr( $follower->get_updated() ),

View file

@ -19,13 +19,8 @@ use function Activitypub\site_supports_blocks;
/** /**
* Base class to implement WordPress to ActivityPub transformers. * Base class to implement WordPress to ActivityPub transformers.
*
* All have to be extended from this one. It provides some WordPress typical
* getter functions, that you might want to use:
*
*
*/ */
abstract class Base { // todo renmae to Base_tranformer abstract class Base {
/** /**
* The WP_Post object. * The WP_Post object.
* *
@ -33,121 +28,6 @@ abstract class Base { // todo renmae to Base_tranformer
*/ */
protected $wp_post; protected $wp_post;
/**
* Time format string for date or gmdate
*
* @var string
*/
protected $timeformat;
/**
* Get the name used for registering the transformer with the ActivityPub plugin.
*
* @since version_number_transformer_management_placeholder
* @return string name
*/
abstract public function get_name();
/**
* Get the display name for the ActivityPub transformer.
*
* @since version_number_transformer_management_placeholder
* @return string display name
*/
abstract public function get_label();
/**
* Returns the ActivityStreams 2.0 Object-Type for a Post.
*
* @see https://www.w3.org/TR/activitystreams-vocabulary/#activity-types
*
* @return string The Object-Type.
*/
abstract protected function get_object_type();
/**
* Transforms the WP_Post object to an ActivityPub Object
*
* @see \Activitypub\Activity\Base_Object
* @param WP_Post $wp_post The WordPress Post
*
* @return \Activitypub\Activity\Base_Object The ActivityPub Object
*/
abstract public function transform();
/**
* Retrieves a post meta field for the configured post.
*
* @param string $key The meta key to retrieve. By default,
* returns data for all keys.
* @param bool $single Optional. Whether to return a single value.
* This parameter has no effect if `$key` is not specified.
* Default false.
* @param mixed $default_value Optional. if the key doesn't exists, it returns the default value.
* Default null.
* @return mixed An array of values if `$single` is false.
* The value of the meta field if `$single` is true.
* False for an invalid `$post_id` (non-numeric, zero, or negative value).
* An empty string if a valid but non-existing post ID is passed.
*/
public function get_post_meta( $key, $single = false, $default_value = null ) {
if ( \metadata_exists( 'post', $this->wp_post->ID, $key ) ) {
return \get_post_meta( $this->wp_post->ID, $key, $single );
}
return $default_value;
}
/**
* Retrieves a post property field for the configured post.
*
* @param string $key The property key to retrieve. By default,
* returns data for all keys.
* @param mixed $default_value Optional. if the key doesn't exists, it returns the default value.
* Default null.
* @return mixed The value of the property.
*/
public function get_post_property( $key, $default_value = null ) {
if ( isset( $this->wp_post->$key ) ) {
return $this->wp_post->$key;
}
return $default_value;
}
/**
* Retrieves a post meta field for the configured post.
*
* @param string $key The meta key of the time value to retrieve.
* @return string formatted time
*/
public function get_post_meta_time( string $key ) {
$time = $this->get_post_meta( $key, true ); // todo what if not exists
if ( null === $time ) {
return null;
}
$time = \strtotime( $time );
return \gmdate( $this->timeformat, $time );
}
/**
* Retrieves if comments are enabled of the configured post.
*
* @return bool True if the comments are open.
*/
public function get_comments_open() {
return comments_open( $this->wp_post->ID );
}
/**
* Getter function for this wp_post.
*
* @return WP_post This WordPress Post object.
*/
protected function get_wp_post() {
return $this->wp_post;
}
/** /**
* Assign WP_Post Object to a specific transformer instance. * Assign WP_Post Object to a specific transformer instance.
* *
@ -169,56 +49,6 @@ abstract class Base { // todo renmae to Base_tranformer
$this->wp_post = $wp_post; $this->wp_post = $wp_post;
} }
/**
* Setter for $timeformat.
*
* This configures a timeformat string for converting timestaps into dates
*
* @param string $timeformat The time format string.
* @return void
*/
public function set_timeformat( string $timeformat ) {
$this->timeformat = $timeformat;
}
/**
* Get published time in configured format
*
* This queries the published time from the wp_object and returns it in the configured format
*
* @return string
*/
public function get_published() {
$published = \strtotime( $this->wp_post->post_date_gmt );
return \gmdate( 'Y-m-d\TH:i:s\Z', $published );
}
/**
* Get updated time in configured format
*
* This queries the update time from the wp_object and returns it in the configured format
*
* @return string
*/
public function get_updated() {
$updated = \strtotime( $this->wp_post->post_modified_gmt );
return \gmdate( 'Y-m-d\TH:i:s\Z', $updated );
}
/**
* TODO review and write description
*
* @return array
*/
public function get_followers_stream() {
$path = sprintf( 'users/%d/followers', intval( $this->wp_post->post_author ) );
return array(
'https://www.w3.org/ns/activitystreams#Public',
get_rest_url_by_path( $path ),
);
}
/** /**
* Get the supported WP post types that the transformer can use as an input. * Get the supported WP post types that the transformer can use as an input.
* *
@ -228,7 +58,7 @@ abstract class Base { // todo renmae to Base_tranformer
* @since version_number_transformer_management_placeholder * @since version_number_transformer_management_placeholder
* @return string[] An array containing all the supported post types. * @return string[] An array containing all the supported post types.
*/ */
public static function get_supported_post_types() { public function get_supported_post_types() {
return \get_post_types( array( 'public' => true ), 'names' ); return \get_post_types( array( 'public' => true ), 'names' );
} }
@ -239,7 +69,7 @@ abstract class Base { // todo renmae to Base_tranformer
* @since version_number_transformer_management_placeholder * @since version_number_transformer_management_placeholder
* @return string Plugin name * @return string Plugin name
*/ */
final private function get_plugin_name_from_transformer_instance( $transformer ) { private function get_plugin_name_from_transformer_instance( $transformer ) {
$class_reflection = new \ReflectionClass( $transformer ); $class_reflection = new \ReflectionClass( $transformer );
$plugin_basename = plugin_basename( $class_reflection->getFileName() ); $plugin_basename = plugin_basename( $class_reflection->getFileName() );
@ -266,6 +96,31 @@ abstract class Base { // todo renmae to Base_tranformer
return in_array( $post_type, $this->get_supported_post_types(), true ); return in_array( $post_type, $this->get_supported_post_types(), true );
} }
/**
* Get the name used for registering the transformer with the ActivityPub plugin.
*
* @since version_number_transformer_management_placeholder
* @return string name
*/
abstract public function get_name();
/**
* Get the display name for the ActivityPub transformer.
*
* @since version_number_transformer_management_placeholder
* @return string display name
*/
abstract public function get_label();
/**
* Returns the ActivityStreams 2.0 Object-Type for a Post.
*
* @see https://www.w3.org/TR/activitystreams-vocabulary/#activity-types
*
* @return string The Object-Type.
*/
abstract protected function get_object_type();
/** /**
* Returns the content for the ActivityPub Item. * Returns the content for the ActivityPub Item.
* *
@ -314,24 +169,19 @@ abstract class Base { // todo renmae to Base_tranformer
* @return string The Template. * @return string The Template.
*/ */
protected function get_post_content_template() { protected function get_post_content_template() {
$type = \get_option( 'activitypub_post_content_type', 'content' ); if ( 'excerpt' === \get_option( 'activitypub_post_content_type', 'content' ) ) {
return "[ap_excerpt]\n\n[ap_permalink type=\"html\"]";
switch ( $type ) {
case 'excerpt':
$template = "[ap_excerpt]\n\n[ap_permalink type=\"html\"]";
break;
case 'title':
$template = "[ap_title]\n\n[ap_permalink type=\"html\"]";
break;
case 'content':
$template = "[ap_content]\n\n[ap_permalink type=\"html\"]\n\n[ap_hashtags]";
break;
default:
$template = \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT );
break;
} }
return apply_filters( 'activitypub_object_content_template', $template, $this->wp_post ); if ( 'title' === \get_option( 'activitypub_post_content_type', 'content' ) ) {
return "[ap_title]\n\n[ap_permalink type=\"html\"]";
}
if ( 'content' === \get_option( 'activitypub_post_content_type', 'content' ) ) {
return "[ap_content]\n\n[ap_permalink type=\"html\"]\n\n[ap_hashtags]";
}
return \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT );
} }
/** /**
@ -368,9 +218,7 @@ abstract class Base { // todo renmae to Base_tranformer
* @return string The User-URL. * @return string The User-URL.
*/ */
protected function get_attributed_to() { protected function get_attributed_to() {
// todo is this descriptive enough? what if for some people the event should be attributed to the venue? if ( is_single_user() ) {
// i would maybe prefer something like "get post author??"
if ( is_single_user() ) { // todo what is if this setting gets changed
$user = new Blog_User(); $user = new Blog_User();
return $user->get_url(); return $user->get_url();
} }
@ -624,6 +472,7 @@ abstract class Base { // todo renmae to Base_tranformer
return $image; return $image;
} }
/** /**
* Helper function to get the @-Mentions from the post content. * Helper function to get the @-Mentions from the post content.
* *
@ -633,13 +482,13 @@ abstract class Base { // todo renmae to Base_tranformer
return apply_filters( 'activitypub_extract_mentions', array(), $this->wp_post->post_content, $this->wp_post ); return apply_filters( 'activitypub_extract_mentions', array(), $this->wp_post->post_content, $this->wp_post );
} }
/** /**
* Returns a list of Mentions, used in the Post. * Returns a list of Mentions, used in the Post.
* *
* @see https://docs.joinmastodon.org/spec/activitypub/#Mention * @see https://docs.joinmastodon.org/spec/activitypub/#Mention
* *
* @return array The list of Mentions. * @return array The list of Mentions.
*/ */
protected function get_cc() { protected function get_cc() {
$cc = array(); $cc = array();
@ -690,6 +539,7 @@ abstract class Base { // todo renmae to Base_tranformer
return $tags; return $tags;
} }
/** /**
* Returns the locale of the post. * Returns the locale of the post.
* *
@ -718,9 +568,52 @@ abstract class Base { // todo renmae to Base_tranformer
* *
* @return array the contenmap * @return array the contenmap
*/ */
protected function get_basic_content_map() { protected function get_content_map() {
return array( return array(
$this->get_locale() => $this->get_content(), // todo is this a valid assumption? $this->get_locale() => $this->get_content(),
); );
} }
/**
* Transforms the WP_Post object to an ActivityPub Object
*
* @see \Activitypub\Activity\Base_Object
*
* @return \Activitypub\Activity\Base_Object The ActivityPub Object
*/
public function to_object() {
$wp_post = $this->wp_post;
$object = new Base_Object();
$object->set_id( $this->get_id() );
$object->set_url( $this->get_url() );
$object->set_type( $this->get_object_type() );
$published = \strtotime( $wp_post->post_date_gmt );
$object->set_published( \gmdate( 'Y-m-d\TH:i:s\Z', $published ) );
$updated = \strtotime( $wp_post->post_modified_gmt );
if ( $updated > $published ) {
$object->set_updated( \gmdate( 'Y-m-d\TH:i:s\Z', $updated ) );
}
$object->set_attributed_to( $this->get_attributed_to() );
$object->set_content( $this->get_content() );
$object->set_content_map( $this->get_content_map );
$path = sprintf( 'users/%d/followers', intval( $wp_post->post_author ) );
$object->set_to(
array(
'https://www.w3.org/ns/activitystreams#Public',
get_rest_url_by_path( $path ),
)
);
$object->set_cc( $this->get_cc() );
$object->set_attachment( $this->get_attachments() );
$object->set_tag( $this->get_tags() );
return $object;
}
} }

View file

@ -106,74 +106,4 @@ class Post extends Base {
return $object_type; return $object_type;
} }
/**
* Gets the template to use to generate the content of the activitypub item.
*
* @return string The Template.
*/
protected function get_post_content_template() {
$type = \get_option( 'activitypub_post_content_type', 'content' );
switch ( $type ) {
case 'excerpt':
$template = "[ap_excerpt]\n\n[ap_permalink type=\"html\"]";
break;
case 'title':
$template = "[ap_title]\n\n[ap_permalink type=\"html\"]";
break;
case 'content':
$template = "[ap_content]\n\n[ap_permalink type=\"html\"]\n\n[ap_hashtags]";
break;
default:
$template = \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT );
break;
}
return apply_filters( 'activitypub_object_content_template', $template, $this->wp_post );
}
/**
* Transforms the WP_Post object to an ActivityPub Object
*
* @see \Activitypub\Activity\Base_Object
* @param WP_Post $wp_post The WordPress Post
*
* @return \Activitypub\Activity\Base_Object The ActivityPub Object
*/
public function transform() {
$object = new Base_Object();
$wp_post = $this->get_wp_post();
$object->set_id( $this->get_id() );
$object->set_url( $this->get_url() );
$object->set_type( $this->get_object_type() );
$published = \strtotime( $wp_post->post_date_gmt );
$object->set_published( \gmdate( 'Y-m-d\TH:i:s\Z', $published ) );
$updated = \strtotime( $wp_post->post_modified_gmt );
if ( $updated > $published ) {
$object->set_updated( \gmdate( 'Y-m-d\TH:i:s\Z', $updated ) );
}
$object->set_attributed_to( $this->get_attributed_to() );
$object->set_content( $this->get_content() );
$object->set_content_map( $this->get_content_map );
$path = sprintf( 'users/%d/followers', intval( $wp_post->post_author ) );
$object->set_to(
array(
'https://www.w3.org/ns/activitystreams#Public',
get_rest_url_by_path( $path ),
)
);
$object->set_cc( $this->get_cc() );
$object->set_attachment( $this->get_attachments() );
$object->set_tag( $this->get_tags() );
return $object;
}
} }

View file

@ -26,7 +26,7 @@ if ( ! defined( 'ABSPATH' ) ) {
* *
* @since version_number_transformer_management_placeholder * @since version_number_transformer_management_placeholder
*/ */
class Transformer_Factory { class Transformers_Manager {
const DEFAULT_TRANSFORMER_MAPPING = array( const DEFAULT_TRANSFORMER_MAPPING = array(
'post' => ACTIVITYPUB_DEFAULT_TRANSFORMER, 'post' => ACTIVITYPUB_DEFAULT_TRANSFORMER,
'page' => ACTIVITYPUB_DEFAULT_TRANSFORMER, 'page' => ACTIVITYPUB_DEFAULT_TRANSFORMER,
@ -215,6 +215,8 @@ class Transformer_Factory {
$this->transformers = []; $this->transformers = [];
foreach ( $builtin_transformers as $transformer_name ) { foreach ( $builtin_transformers as $transformer_name ) {
include ACTIVITYPUB_PLUGIN_DIR . 'includes/transformer/class-' . $transformer_name . '.php';
$class_name = ucfirst( $transformer_name ); $class_name = ucfirst( $transformer_name );
$class_name = '\Activitypub\Transformer\\' . $class_name; $class_name = '\Activitypub\Transformer\\' . $class_name;
@ -229,7 +231,7 @@ class Transformer_Factory {
* *
* @since version_number_transformer_management_placeholder * @since version_number_transformer_management_placeholder
* *
* @param Transformer_Factory $this The widgets manager. * @param Transformers_Manager $this The widgets manager.
*/ */
do_action( 'activitypub_transformers_register', $this ); do_action( 'activitypub_transformers_register', $this );
} }
@ -267,29 +269,24 @@ class Transformer_Factory {
* @since version_number_transformer_management_placeholder * @since version_number_transformer_management_placeholder
* @access public * @access public
* *
* @param WP_Post|WP_Comment $wp_object The WordPress Post/Comment. * @param WP_Post|WP_Comment $object The WordPress Post/Comment.
* *
* @return \ActivityPub\Transformer\Base|null Registered transformers. * @return \ActivityPub\Transformer\Base|null Registered transformers.
*/ */
public function transform( $wp_object ) { public function get_transformer( $object ) {
switch ( get_class( $wp_object ) ) { switch ( get_class( $object ) ) {
case 'WP_Post': case 'WP_Post':
$post_type = get_post_type( $wp_object ); $post_type = get_post_type( $object );
$transformer_mapping = \get_option( 'activitypub_transformer_mapping', self::DEFAULT_TRANSFORMER_MAPPING ); $transformer_mapping = \get_option( 'activitypub_transformer_mapping', self::DEFAULT_TRANSFORMER_MAPPING );
$transformer_name = $transformer_mapping[ $post_type ]; $transformer_name = $transformer_mapping[ $post_type ];
$transformer_class = $this->get_transformers( $transformer_name ); $transformer_class = $this->get_transformers( $transformer_name );
if ( null === $transformer_class ) {
wp_die("transformer '". $transformer_name . "' not found", '', ["response" => 404]);
}
$transformer_instance = new $transformer_class(); $transformer_instance = new $transformer_class();
$transformer_instance->set_wp_post( $wp_object ); $transformer_instance->set_wp_post( $object );
return $transformer_instance->transform(); return $transformer_instance;
case 'WP_Comment': case 'WP_Comment':
return new Comment( $wp_object ); return new Comment( $object );
default: default:
return apply_filters( 'activitypub_transformer', null, $wp_object, get_class( $wp_object ) ); return apply_filters( 'activitypub_transformer', null, $object, get_class( $object ) );
} }
} }
} }

View file

@ -3,7 +3,6 @@ namespace Activitypub\Integration;
use function Activitypub\get_total_users; use function Activitypub\get_total_users;
use function Activitypub\get_active_users; use function Activitypub\get_active_users;
use function Activitypub\get_rest_url_by_path;
/** /**
* Compatibility with the NodeInfo plugin * Compatibility with the NodeInfo plugin
@ -15,10 +14,8 @@ class Nodeinfo {
* Initialize the class, registering WordPress hooks * Initialize the class, registering WordPress hooks
*/ */
public static function init() { public static function init() {
\add_filter( 'nodeinfo_data', array( self::class, 'add_nodeinfo_data' ), 10, 2 ); \add_filter( 'nodeinfo_data', array( self::class, 'add_nodeinfo_discovery' ), 10, 2 );
\add_filter( 'nodeinfo2_data', array( self::class, 'add_nodeinfo2_data' ), 10 ); \add_filter( 'nodeinfo2_data', array( self::class, 'add_nodeinfo2_discovery' ), 10 );
\add_filter( 'wellknown_nodeinfo_data', array( self::class, 'add_wellknown_nodeinfo_data' ), 10, 2 );
} }
/** /**
@ -29,7 +26,7 @@ class Nodeinfo {
* *
* @return array The extended array * @return array The extended array
*/ */
public static function add_nodeinfo_data( $nodeinfo, $version ) { public static function add_nodeinfo_discovery( $nodeinfo, $version ) {
if ( $version >= '2.0' ) { if ( $version >= '2.0' ) {
$nodeinfo['protocols'][] = 'activitypub'; $nodeinfo['protocols'][] = 'activitypub';
} else { } else {
@ -53,7 +50,7 @@ class Nodeinfo {
* *
* @return array The extended array * @return array The extended array
*/ */
public static function add_nodeinfo2_data( $nodeinfo ) { public static function add_nodeinfo2_discovery( $nodeinfo ) {
$nodeinfo['protocols'][] = 'activitypub'; $nodeinfo['protocols'][] = 'activitypub';
$nodeinfo['usage']['users'] = array( $nodeinfo['usage']['users'] = array(
@ -64,20 +61,4 @@ class Nodeinfo {
return $nodeinfo; return $nodeinfo;
} }
/**
* Extend the well-known nodeinfo data
*
* @param array $data The well-known nodeinfo data
*
* @return array The extended array
*/
public static function add_wellknown_nodeinfo_data( $data ) {
$data['links'][] = array(
'rel' => 'https://www.w3.org/ns/activitystreams#Application',
'href' => get_rest_url_by_path( 'application' ),
);
return $data;
}
} }

View file

@ -14,8 +14,8 @@ class Webfinger {
* Initialize the class, registering WordPress hooks * Initialize the class, registering WordPress hooks
*/ */
public static function init() { public static function init() {
\add_filter( 'webfinger_user_data', array( self::class, 'add_user_discovery' ), 1, 3 ); \add_filter( 'webfinger_user_data', array( self::class, 'add_user_discovery' ), 10, 3 );
\add_filter( 'webfinger_data', array( self::class, 'add_pseudo_user_discovery' ), 1, 2 ); \add_filter( 'webfinger_data', array( self::class, 'add_pseudo_user_discovery' ), 99, 2 );
} }
/** /**
@ -34,11 +34,6 @@ class Webfinger {
return $array; return $array;
} }
$array['subject'] = sprintf( 'acct:%s', $user->get_resource() );
$array['aliases'][] = $user->get_url();
$array['aliases'][] = $user->get_alternate_url();
$array['links'][] = array( $array['links'][] = array(
'rel' => 'self', 'rel' => 'self',
'type' => 'application/activity+json', 'type' => 'application/activity+json',
@ -58,12 +53,10 @@ class Webfinger {
* @return array the jrd array * @return array the jrd array
*/ */
public static function add_pseudo_user_discovery( $array, $resource ) { public static function add_pseudo_user_discovery( $array, $resource ) {
$user = Webfinger_Rest::get_profile( $resource ); if ( $array ) {
if ( ! $user || is_wp_error( $user ) ) {
return $array; return $array;
} }
return $user; return Webfinger_Rest::get_profile( $resource );
} }
} }

View file

@ -3,7 +3,7 @@ Contributors: automattic, pfefferle, mediaformat, mattwiebe, akirk, jeherve, nur
Tags: OStatus, fediverse, activitypub, activitystream Tags: OStatus, fediverse, activitypub, activitystream
Requires at least: 4.7 Requires at least: 4.7
Tested up to: 6.4 Tested up to: 6.4
Stable tag: 1.3.0 Stable tag: 1.2.0
Requires PHP: 5.6 Requires PHP: 5.6
License: MIT License: MIT
License URI: http://opensource.org/licenses/MIT License URI: http://opensource.org/licenses/MIT
@ -68,10 +68,10 @@ Implemented:
* share posts * share posts
* receive comments/reactions * receive comments/reactions
* signature verification * signature verification
* threaded comments support
To implement: To implement:
* threaded comments support
* replace shortcodes with blocks for layout * replace shortcodes with blocks for layout
= What is "ActivityPub for WordPress" = = What is "ActivityPub for WordPress" =
@ -105,25 +105,6 @@ Where 'blog' is the path to the subdirectory at which your blog resides.
Project maintained on GitHub at [automattic/wordpress-activitypub](https://github.com/automattic/wordpress-activitypub). Project maintained on GitHub at [automattic/wordpress-activitypub](https://github.com/automattic/wordpress-activitypub).
= 2.0.0 =
* Removed: Deprecated Classes
* Fixed: Normalize attributes that can have mixed value types
* Added: URL support for WebFinger
* Added: Make Post-Template filterable
* Added: CSS class for ActivityPub comments to allow custom designs
* Added: FEP-2677: Identifying the Application Actor
* Improved: WebFinger endpoints
= 1.3.0 =
* Added: Threaded-Comments support
* Improved: alt text for avatars in Follow Me/Followers blocks
* Improved: `Delete`, `Update` and `Follow` Activities
* Improved: better/more effective handling of `Delete` Activities
* Improved: allow `<p />` and `<br />` for Comments
* Fixed: removed default limit of WP_Query to send updates to all Inboxes and not only to the first 10
= 1.2.0 = = 1.2.0 =
* Add: Search and order followerer lists * Add: Search and order followerer lists

View file

@ -37,7 +37,7 @@ function Profile( { profile, popupStyles, userId } ) {
const { avatar, name, resource } = profile; const { avatar, name, resource } = profile;
return ( return (
<div className="activitypub-profile"> <div className="activitypub-profile">
<img className="activitypub-profile__avatar" src={ avatar } alt={ name } /> <img className="activitypub-profile__avatar" src={ avatar } />
<div className="activitypub-profile__content"> <div className="activitypub-profile__content">
<div className="activitypub-profile__name">{ name }</div> <div className="activitypub-profile__name">{ name }</div>
<div className="activitypub-profile__handle" title={ resource }>{ resource }</div> <div className="activitypub-profile__handle" title={ resource }>{ resource }</div>

View file

@ -106,13 +106,7 @@ function Follower( { name, icon, url, preferredUsername, followLinks = true } )
} }
return ( return (
<ExternalLink className="activitypub-link" href={ url } title={ handle } { ...extraProps }> <ExternalLink className="activitypub-link" href={ url } title={ handle } { ...extraProps }>
<img <img width="40" height="40" src={ icon.url } class="avatar activitypub-avatar" />
width="40"
height="40"
src={ icon.url }
class="avatar activitypub-avatar"
alt={ name }
/>
<span class="activitypub-actor"> <span class="activitypub-actor">
<strong className="activitypub-name">{ name }</strong> <strong className="activitypub-name">{ name }</strong>
<span class="sep">/</span> <span class="sep">/</span>

View file

@ -2,9 +2,9 @@
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$post = \get_post(); $post = \get_post();
$object = \Activitypub\Transformer\Transformer_Factory::instance()->transform( $post ); $transformer = \Activitypub\Transformer\Transformers_Manager::instance()->get_transformer( $post );
$json = \array_merge( array( '@context' => \Activitypub\get_context() ), $object->to_array() ); $json = \array_merge( array( '@context' => \Activitypub\get_context() ), $transformer->to_object()->to_array() );
// filter output // filter output
$json = \apply_filters( 'activitypub_json_post_array', $json ); $json = \apply_filters( 'activitypub_json_post_array', $json );
@ -20,7 +20,7 @@ if ( \get_query_var( 'pretty' ) ) {
$options |= \JSON_PRETTY_PRINT; // phpcs:ignore $options |= \JSON_PRETTY_PRINT; // phpcs:ignore
} }
$options |= ACTIVITYPUB_JSON_FORMAT_OPTIONS; $options |= \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT;
/* /*
* Options to be passed to json_encode() * Options to be passed to json_encode()
@ -31,6 +31,7 @@ $options = \apply_filters( 'activitypub_json_post_options', $options );
\header( 'Content-Type: application/activity+json' ); \header( 'Content-Type: application/activity+json' );
echo \wp_json_encode( $json, $options ); echo \wp_json_encode( $json, $options );
/* /*
* Action triggerd after the ActivityPub profile has been created and sent to the client * Action triggerd after the ActivityPub profile has been created and sent to the client
*/ */

View file

@ -93,7 +93,7 @@
<?php \esc_html_e( 'Excerpt', 'activitypub' ); ?> <?php \esc_html_e( 'Excerpt', 'activitypub' ); ?>
- -
<span class="description"> <span class="description">
<?php \esc_html_e( 'A content summary without markup (truncated if no excerpt is provided).', 'activitypub' ); ?> <?php \esc_html_e( 'A content summary, shortened to 400 characters and without markup.', 'activitypub' ); ?>
</span> </span>
</label> </label>
</p> </p>
@ -125,7 +125,7 @@
<ul> <ul>
<li><code>[ap_title]</code> - <?php \esc_html_e( 'The post\'s title.', 'activitypub' ); ?></li> <li><code>[ap_title]</code> - <?php \esc_html_e( 'The post\'s title.', 'activitypub' ); ?></li>
<li><code>[ap_content]</code> - <?php \esc_html_e( 'The post\'s content.', 'activitypub' ); ?></li> <li><code>[ap_content]</code> - <?php \esc_html_e( 'The post\'s content.', 'activitypub' ); ?></li>
<li><code>[ap_excerpt]</code> - <?php \esc_html_e( 'The post\'s excerpt (may be truncated).', 'activitypub' ); ?></li> <li><code>[ap_excerpt]</code> - <?php \esc_html_e( 'The post\'s excerpt (default 400 chars).', 'activitypub' ); ?></li>
<li><code>[ap_permalink]</code> - <?php \esc_html_e( 'The post\'s permalink.', 'activitypub' ); ?></li> <li><code>[ap_permalink]</code> - <?php \esc_html_e( 'The post\'s permalink.', 'activitypub' ); ?></li>
<li><code>[ap_shortlink]</code> - <?php echo \wp_kses( \__( 'The post\'s shortlink. I can recommend <a href="https://wordpress.org/plugins/hum/" target="_blank">Hum</a>.', 'activitypub' ), 'default' ); ?></li> <li><code>[ap_shortlink]</code> - <?php echo \wp_kses( \__( 'The post\'s shortlink. I can recommend <a href="https://wordpress.org/plugins/hum/" target="_blank">Hum</a>.', 'activitypub' ), 'default' ); ?></li>
<li><code>[ap_hashtags]</code> - <?php \esc_html_e( 'The post\'s tags as hashtags.', 'activitypub' ); ?></li> <li><code>[ap_hashtags]</code> - <?php \esc_html_e( 'The post\'s tags as hashtags.', 'activitypub' ); ?></li>
@ -237,7 +237,7 @@
return $object->name; return $object->name;
}, $all_public_post_types); }, $all_public_post_types);
$transformer_manager = \Activitypub\Transformer\Transformer_Factory::instance(); $transformer_manager = \Activitypub\Transformer\Transformers_Manager::instance();
$transformers = $transformer_manager->get_transformers(); $transformers = $transformer_manager->get_transformers();
?> ?>

View file

@ -12,7 +12,7 @@ class ActivityPub_TestCase_Cache_HTTP extends \WP_UnitTestCase {
add_filter( add_filter(
'rest_url', 'rest_url',
function () { function() {
return get_option( 'home' ) . '/wp-json/'; return get_option( 'home' ) . '/wp-json/';
} }
); );

View file

@ -70,7 +70,7 @@ class Test_Activitypub_Activity_Dispatcher extends ActivityPub_TestCase_Cache_HT
add_filter( add_filter(
'activitypub_extract_mentions', 'activitypub_extract_mentions',
function ( $mentions ) { function( $mentions ) {
$mentions[] = 'https://example.com/alex'; $mentions[] = 'https://example.com/alex';
return $mentions; return $mentions;
}, },
@ -134,7 +134,7 @@ class Test_Activitypub_Activity_Dispatcher extends ActivityPub_TestCase_Cache_HT
add_filter( add_filter(
'activitypub_is_user_type_disabled', 'activitypub_is_user_type_disabled',
function ( $value, $type ) { function( $value, $type ) {
if ( 'blog' === $type ) { if ( 'blog' === $type ) {
return false; return false;
} else { } else {

View file

@ -1,6 +1,4 @@
<?php <?php
use DMS\PHPUnitExtensions\ArraySubset\Assert;
class Test_Activitypub_Activity extends WP_UnitTestCase { class Test_Activitypub_Activity extends WP_UnitTestCase {
public function test_activity_mentions() { public function test_activity_mentions() {
$post = \wp_insert_post( $post = \wp_insert_post(
@ -12,7 +10,7 @@ class Test_Activitypub_Activity extends WP_UnitTestCase {
add_filter( add_filter(
'activitypub_extract_mentions', 'activitypub_extract_mentions',
function ( $mentions ) { function( $mentions ) {
$mentions['@alex'] = 'https://example.com/alex'; $mentions['@alex'] = 'https://example.com/alex';
return $mentions; return $mentions;
}, },
@ -20,7 +18,7 @@ class Test_Activitypub_Activity extends WP_UnitTestCase {
); );
$wp_post = get_post( $post ); $wp_post = get_post( $post );
$activitypub_post = \Activitypub\Transformer\Transformer_Factory::instance()->transform(); $activitypub_post = \Activitypub\Transformer\Transformers_Manager::instance()->get_transformer( $post )->to_object();
$activitypub_activity = new \Activitypub\Activity\Activity(); $activitypub_activity = new \Activitypub\Activity\Activity();
$activitypub_activity->set_type( 'Create' ); $activitypub_activity->set_type( 'Create' );
@ -45,21 +43,4 @@ class Test_Activitypub_Activity extends WP_UnitTestCase {
$this->assertEquals( 'Hello world!', $object->get_content() ); $this->assertEquals( 'Hello world!', $object->get_content() );
$this->assertEquals( $test_array, $object->to_array() ); $this->assertEquals( $test_array, $object->to_array() );
} }
public function test_activity_object() {
$test_array = array(
'id' => 'https://example.com/post/123',
'type' => 'Create',
'object' => array(
'id' => 'https://example.com/post/123/activity',
'type' => 'Note',
'content' => 'Hello world!',
),
);
$activity = \Activitypub\Activity\Activity::init_from_array( $test_array );
$this->assertEquals( 'Hello world!', $activity->get_object()->get_content() );
Assert::assertArraySubset( $test_array, $activity->to_array() );
}
} }

View file

@ -1,70 +0,0 @@
<?php
class Test_Activitypub_Create_Handler extends WP_UnitTestCase {
public $user_id;
public $user_url;
public $post_id;
public $post_permalink;
public function set_up() {
$this->user_id = 1;
$authordata = \get_userdata( $this->user_id );
$this->user_url = $authordata->user_url;
$this->post_id = \wp_insert_post(
array(
'post_author' => $this->user_id,
'post_content' => 'test',
)
);
$this->post_permalink = \get_permalink( $this->post_id );
\add_filter( 'pre_get_remote_metadata_by_actor', array( '\Test_Activitypub_Create_Handler', 'get_remote_metadata_by_actor' ), 0, 2 );
}
public static function get_remote_metadata_by_actor( $value, $actor ) {
return array(
'name' => 'Example User',
'icon' => array(
'url' => 'https://example.com/icon',
),
'url' => $actor,
'id' => 'http://example.org/users/example',
);
}
public function create_test_object( $id = 'https://example.com/123' ) {
return array(
'actor' => $this->user_url,
'id' => 'https://example.com/id/' . microtime( true ),
'to' => [ $this->user_url ],
'cc' => [ 'https://www.w3.org/ns/activitystreams#Public' ],
'object' => array(
'id' => $id,
'url' => 'https://example.com/example',
'inReplyTo' => $this->post_permalink,
'content' => 'example',
),
);
}
public function test_handle_create_object_unset_rejected() {
$object = $this->create_test_object();
unset( $object['object'] );
$converted = Activitypub\Handler\Create::handle_create( $object, $this->user_id );
$this->assertNull( $converted );
}
public function test_handle_create_non_public_rejected() {
$object = $this->create_test_object();
$object['cc'] = [];
$converted = Activitypub\Handler\Create::handle_create( $object, $this->user_id );
$this->assertNull( $converted );
}
public function test_handle_create_no_id_rejected() {
$object = $this->create_test_object();
unset( $object['object']['id'] );
$converted = Activitypub\Handler\Create::handle_create( $object, $this->user_id );
$this->assertNull( $converted );
}
}

View file

@ -1,174 +0,0 @@
<?php
class Test_Activitypub_Interactions extends WP_UnitTestCase {
public $user_id;
public $user_url;
public $post_id;
public $post_permalink;
public function set_up() {
$this->user_id = 1;
$authordata = \get_userdata( $this->user_id );
$this->user_url = $authordata->user_url;
$this->post_id = \wp_insert_post(
array(
'post_author' => $this->user_id,
'post_content' => 'test',
)
);
$this->post_permalink = \get_permalink( $this->post_id );
\add_filter( 'pre_get_remote_metadata_by_actor', array( '\Test_Activitypub_Interactions', 'get_remote_metadata_by_actor' ), 0, 2 );
}
public static function get_remote_metadata_by_actor( $value, $actor ) {
return array(
'name' => 'Example User',
'icon' => array(
'url' => 'https://example.com/icon',
),
'url' => $actor,
'id' => 'http://example.org/users/example',
);
}
public function create_test_object( $id = 'https://example.com/123' ) {
return array(
'actor' => $this->user_url,
'id' => 'https://example.com/id/' . microtime( true ),
'to' => [ $this->user_url ],
'cc' => [ 'https://www.w3.org/ns/activitystreams#Public' ],
'object' => array(
'id' => $id,
'url' => 'https://example.com/example',
'inReplyTo' => $this->post_permalink,
'content' => 'example',
),
);
}
public function create_test_rich_object( $id = 'https://example.com/123' ) {
return array(
'actor' => $this->user_url,
'id' => 'https://example.com/id/' . microtime( true ),
'to' => [ $this->user_url ],
'cc' => [ 'https://www.w3.org/ns/activitystreams#Public' ],
'object' => array(
'id' => $id,
'url' => 'https://example.com/example',
'inReplyTo' => $this->post_permalink,
'content' => 'Hello<br />example<p>example</p><img src="https://example.com/image.jpg" />',
),
);
}
public function test_handle_create_basic() {
$comment_id = Activitypub\Collection\Interactions::add_comment( $this->create_test_object() );
$comment = get_comment( $comment_id, ARRAY_A );
$this->assertIsArray( $comment );
$this->assertEquals( $this->post_id, $comment['comment_post_ID'] );
$this->assertEquals( 'Example User', $comment['comment_author'] );
$this->assertEquals( $this->user_url, $comment['comment_author_url'] );
$this->assertEquals( 'example', $comment['comment_content'] );
$this->assertEquals( 'comment', $comment['comment_type'] );
$this->assertEquals( '', $comment['comment_author_email'] );
$this->assertEquals( 0, $comment['comment_parent'] );
$this->assertEquals( 'https://example.com/123', get_comment_meta( $comment_id, 'source_id', true ) );
$this->assertEquals( 'https://example.com/example', get_comment_meta( $comment_id, 'source_url', true ) );
$this->assertEquals( 'https://example.com/icon', get_comment_meta( $comment_id, 'avatar_url', true ) );
$this->assertEquals( 'activitypub', get_comment_meta( $comment_id, 'protocol', true ) );
}
public function test_handle_create_rich() {
$comment_id = Activitypub\Collection\Interactions::add_comment( $this->create_test_rich_object() );
$comment = get_comment( $comment_id, ARRAY_A );
$this->assertEquals( 'Hello<br />example<p>example</p>', $comment['comment_content'] );
$commentarray = array(
'comment_post_ID' => $this->post_id,
'comment_author' => 'Example User',
'comment_author_url' => $this->user_url,
'comment_content' => 'Hello<br />example<p>example</p>',
'comment_type' => 'comment',
'comment_author_email' => '',
'comment_parent' => 0,
'comment_meta' => array(
'source_id' => 'https://example.com/123',
'source_url' => 'https://example.com/example',
'protocol' => 'activitypub',
),
);
\remove_action( 'check_comment_flood', 'check_comment_flood_db', 10 );
$comment_id = wp_new_comment( $commentarray );
\add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 );
$comment = get_comment( $comment_id, ARRAY_A );
$this->assertEquals( 'Helloexampleexample', $comment['comment_content'] );
}
public function test_convert_object_to_comment_not_reply_rejected() {
$object = $this->create_test_object();
unset( $object['object']['inReplyTo'] );
$converted = Activitypub\Collection\Interactions::add_comment( $object );
$this->assertFalse( $converted );
}
public function test_convert_object_to_comment_already_exists_rejected() {
$object = $this->create_test_object( 'https://example.com/test_convert_object_to_comment_already_exists_rejected' );
Activitypub\Collection\Interactions::add_comment( $object );
$converted = Activitypub\Collection\Interactions::add_comment( $object );
$this->assertEquals( $converted->get_error_code(), 'comment_duplicate' );
}
public function test_convert_object_to_comment_reply_to_comment() {
$id = 'https://example.com/test_convert_object_to_comment_reply_to_comment';
$object = $this->create_test_object( $id );
Activitypub\Collection\Interactions::add_comment( $object );
$comment = \Activitypub\object_id_to_comment( $id );
$object['object']['inReplyTo'] = $id;
$object['object']['id'] = 'https://example.com/234';
$id = Activitypub\Collection\Interactions::add_comment( $object );
$converted = get_comment( $id, ARRAY_A );
$this->assertIsArray( $converted );
$this->assertEquals( $this->post_id, $converted['comment_post_ID'] );
$this->assertEquals( $comment->comment_ID, $converted['comment_parent'] );
}
public function test_convert_object_to_comment_reply_to_non_existent_comment_rejected() {
$object = $this->create_test_object();
$object['object']['inReplyTo'] = 'https://example.com/not_found';
$converted = Activitypub\Collection\Interactions::add_comment( $object );
$this->assertFalse( $converted );
}
public function test_handle_create_basic2() {
$id = 'https://example.com/test_handle_create_basic';
$object = $this->create_test_object( $id );
Activitypub\Collection\Interactions::add_comment( $object );
$comment = \Activitypub\object_id_to_comment( $id );
$this->assertInstanceOf( WP_Comment::class, $comment );
}
public function test_get_interaction_by_id() {
$id = 'https://example.com/test_get_interaction_by_id';
$url = 'https://example.com/test_get_interaction_by_url';
$object = $this->create_test_object( $id );
$object['object']['url'] = $url;
Activitypub\Collection\Interactions::add_comment( $object );
$comment = \Activitypub\object_id_to_comment( $id );
$interactions = Activitypub\Collection\Interactions::get_interaction_by_id( $id );
$this->assertIsArray( $interactions );
$this->assertEquals( $comment->comment_ID, $interactions[0]->comment_ID );
$comment = \Activitypub\object_id_to_comment( $id );
$interactions = Activitypub\Collection\Interactions::get_interaction_by_id( $url );
$this->assertIsArray( $interactions );
$this->assertEquals( $comment->comment_ID, $interactions[0]->comment_ID );
}
}

View file

@ -10,13 +10,13 @@ class Test_Activitypub_Post extends WP_UnitTestCase {
$permalink = \get_permalink( $post ); $permalink = \get_permalink( $post );
$activitypub_post = \Activitypub\Transformer\Transformer_Factory::instance()->transform( get_post( $post ) ); $activitypub_post = \Activitypub\Transformer\Transformers_Manager::instance()->get_transformer( get_post( $post ) )->to_object();
$this->assertEquals( $permalink, $activitypub_post->get_id() ); $this->assertEquals( $permalink, $activitypub_post->get_id() );
\wp_trash_post( $post ); \wp_trash_post( $post );
$activitypub_post = \Activitypub\Transformer\Transformer_Factory::instance()->transform( get_post( $post ) ); $activitypub_post = \Activitypub\Transformer\Transformers_Manager::instance()->get_transformer( get_post( $post ) )->to_object();
$this->assertEquals( $permalink, $activitypub_post->get_id() ); $this->assertEquals( $permalink, $activitypub_post->get_id() );

View file

@ -5,7 +5,7 @@ class Test_Activitypub_Rest_Inbox extends WP_UnitTestCase {
*/ */
public function test_is_activity_public( $data, $check ) { public function test_is_activity_public( $data, $check ) {
$this->assertEquals( $check, Activitypub\is_activity_public( $data ) ); $this->assertEquals( $check, Activitypub\Rest\Inbox::is_activity_public( $data ) );
} }
public function the_data_provider() { public function the_data_provider() {

View file

@ -10,7 +10,7 @@ class Test_Activitypub_Signature_Verification extends WP_UnitTestCase {
) )
); );
$remote_actor = \get_author_posts_url( 2 ); $remote_actor = \get_author_posts_url( 2 );
$activitypub_post = \Activitypub\Transformer\Transformer_Factory::instance()->transform( get_post( $post ) ); $activitypub_post = \Activitypub\Transformer\Transformers_Manager::instance()->get_transformer( get_post( $post ) )->to_object();
$activitypub_activity = new Activitypub\Activity\Activity( 'Create' ); $activitypub_activity = new Activitypub\Activity\Activity( 'Create' );
$activitypub_activity->set_type( 'Create' ); $activitypub_activity->set_type( 'Create' );
$activitypub_activity->set_object( $activitypub_post ); $activitypub_activity->set_object( $activitypub_post );
@ -55,7 +55,7 @@ class Test_Activitypub_Signature_Verification extends WP_UnitTestCase {
public function test_rest_activity_signature() { public function test_rest_activity_signature() {
add_filter( add_filter(
'pre_get_remote_metadata_by_actor', 'pre_get_remote_metadata_by_actor',
function ( $json, $actor ) { function( $json, $actor ) {
$user = Activitypub\Collection\Users::get_by_id( 1 ); $user = Activitypub\Collection\Users::get_by_id( 1 );
$public_key = Activitypub\Signature::get_public_key_for( $user->get__id() ); $public_key = Activitypub\Signature::get_public_key_for( $user->get__id() );
// return ActivityPub Profile with signature // return ActivityPub Profile with signature
@ -82,7 +82,7 @@ class Test_Activitypub_Signature_Verification extends WP_UnitTestCase {
); );
$remote_actor = \get_author_posts_url( 2 ); $remote_actor = \get_author_posts_url( 2 );
$remote_actor_inbox = Activitypub\get_rest_url_by_path( '/inbox' ); $remote_actor_inbox = Activitypub\get_rest_url_by_path( '/inbox' );
$activitypub_post = \Activitypub\Transformer\Transformer_Factory::instance()->transform( get_post( $post ) ); $activitypub_post = \Activitypub\Transformer\Transformers_Manager::instance()->get_transformer( get_post( $post ) )->to_object();
$activitypub_activity = new Activitypub\Activity\Activity(); $activitypub_activity = new Activitypub\Activity\Activity();
$activitypub_activity->set_type( 'Create' ); $activitypub_activity->set_type( 'Create' );
$activitypub_activity->set_object( $activitypub_post ); $activitypub_activity->set_object( $activitypub_post );

View file

@ -62,34 +62,4 @@ class Test_Activitypub_Shortcodes extends WP_UnitTestCase {
$this->assertEquals( '', $content ); $this->assertEquals( '', $content );
Shortcodes::unregister(); Shortcodes::unregister();
} }
public function test_excerpt() {
Shortcodes::register();
global $post;
$post_id = -97; // negative ID, to avoid clash with a valid post
$post = new stdClass();
$post->ID = $post_id;
$post->post_author = 1;
$post->post_date = current_time( 'mysql' );
$post->post_date_gmt = current_time( 'mysql', 1 );
$post->post_title = 'Some title or other';
$post->post_content = '<script>test</script>Lorem ipsum dolor sit amet, consectetur.<script type="javascript">{"asdf": "qwerty"}</script><style></style>';
$post->post_status = 'publish';
$post->comment_status = 'closed';
$post->ping_status = 'closed';
$post->post_name = 'fake-page-' . rand( 1, 99999 ); // append random number to avoid clash
$post->post_type = 'page';
$post->filter = 'raw'; // important!
$content = '[ap_excerpt length="25"]';
// Fill in the shortcodes.
setup_postdata( $post );
$content = do_shortcode( $content );
wp_reset_postdata();
$this->assertEquals( "<p>Lorem ipsum [&hellip;]</p>\n", $content );
Shortcodes::unregister();
}
} }

View file

@ -1,43 +0,0 @@
<?php
class Test_Activitypub_Users_Collection extends WP_UnitTestCase {
public function set_up() {
parent::set_up();
add_option( 'activitypub_blog_user_identifier', 'blog' );
add_user_meta( 1, 'activitypub_user_identifier', 'admin' );
}
/**
* @dataProvider the_resource_provider
*/
public function test_get_by_various( $resource, $expected ) {
$user = Activitypub\Collection\Users::get_by_resource( $resource );
$this->assertInstanceOf( $expected, $user );
}
public function the_resource_provider() {
return array(
array( 'http://example.org/?author=1', 'Activitypub\Model\User' ),
array( 'https://example.org/?author=1', 'Activitypub\Model\User' ),
array( 'http://example.org/?author=7', 'WP_Error' ),
array( 'acct:admin@example.org', 'Activitypub\Model\User' ),
array( 'acct:blog@example.org', 'Activitypub\Model\Blog_User' ),
array( 'acct:*@example.org', 'Activitypub\Model\Blog_User' ),
array( 'acct:_@example.org', 'Activitypub\Model\Blog_User' ),
array( 'acct:aksd@example.org', 'WP_Error' ),
array( 'admin@example.org', 'Activitypub\Model\User' ),
array( 'acct:application@example.org', 'Activitypub\Model\Application_User' ),
array( 'http://example.org/@admin', 'Activitypub\Model\User' ),
array( 'http://example.org/@blog', 'Activitypub\Model\Blog_User' ),
array( 'https://example.org/@blog', 'Activitypub\Model\Blog_User' ),
array( 'http://example.org/@blog/', 'Activitypub\Model\Blog_User' ),
array( 'http://example.org/', 'Activitypub\Model\Blog_User' ),
array( 'http://example.org', 'Activitypub\Model\Blog_User' ),
array( 'https://example.org/', 'Activitypub\Model\Blog_User' ),
array( 'https://example.org', 'Activitypub\Model\Blog_User' ),
array( 'http://example.org/@blog/s', 'WP_Error' ),
array( 'http://example.org/@blogs/', 'WP_Error' ),
);
}
}

View file

@ -1,5 +1,5 @@
<?php <?php
class Test_Activitypub_Followers extends WP_UnitTestCase { class Test_Db_Activitypub_Followers extends WP_UnitTestCase {
public static $users = array( public static $users = array(
'username@example.org' => array( 'username@example.org' => array(
'id' => 'https://example.org/users/username', 'id' => 'https://example.org/users/username',
@ -76,7 +76,7 @@ class Test_Activitypub_Followers extends WP_UnitTestCase {
$this->assertEquals( 3, \count( $db_followers ) ); $this->assertEquals( 3, \count( $db_followers ) );
$db_followers = array_map( $db_followers = array_map(
function ( $item ) { function( $item ) {
return $item->get_url(); return $item->get_url();
}, },
$db_followers $db_followers
@ -329,82 +329,6 @@ class Test_Activitypub_Followers extends WP_UnitTestCase {
$this->assertEquals( $name, $follower->get_name() ); $this->assertEquals( $name, $follower->get_name() );
} }
public function test_get_inboxes() {
for ( $i = 0; $i < 30; $i++ ) {
$meta = array(
'id' => 'https://example.org/users/' . $i,
'url' => 'https://example.org/users/' . $i,
'inbox' => 'https://example.org/users/' . $i . '/inbox',
'name' => 'user' . $i,
'preferredUsername' => 'user' . $i,
'publicKey' => 'https://example.org/users/' . $i . '#main-key',
'publicKeyPem' => $i,
);
$follower = new \Activitypub\Model\Follower();
$follower->from_array( $meta );
$id = $follower->upsert();
add_post_meta( $id, 'activitypub_user_id', 1 );
}
$inboxes = \Activitypub\Collection\Followers::get_inboxes( 1 );
$this->assertCount( 30, $inboxes );
wp_cache_delete( sprintf( \Activitypub\Collection\Followers::CACHE_KEY_INBOXES, 1 ), 'activitypub' );
for ( $j = 0; $j < 5; $j++ ) {
$k = $j + 100;
$meta = array(
'id' => 'https://example.org/users/' . $k,
'url' => 'https://example.org/users/' . $k,
'inbox' => 'https://example.org/users/' . $j . '/inbox',
'name' => 'user' . $k,
'preferredUsername' => 'user' . $k,
'publicKey' => 'https://example.org/users/' . $k . '#main-key',
'publicKeyPem' => $k,
);
$follower = new \Activitypub\Model\Follower();
$follower->from_array( $meta );
$id = $follower->upsert();
add_post_meta( $id, 'activitypub_user_id', 1 );
}
$inboxes2 = \Activitypub\Collection\Followers::get_inboxes( 1 );
$this->assertCount( 30, $inboxes2 );
}
public function test_get_all_followers() {
for ( $i = 0; $i < 30; $i++ ) {
$meta = array(
'id' => 'https://example.org/users/' . $i,
'url' => 'https://example.org/users/' . $i,
'inbox' => 'https://example.org/users/' . $i . '/inbox',
'name' => 'user' . $i,
'preferredUsername' => 'user' . $i,
'publicKey' => 'https://example.org/users/' . $i . '#main-key',
'publicKeyPem' => $i,
);
$follower = new \Activitypub\Model\Follower();
$follower->from_array( $meta );
$id = $follower->upsert();
add_post_meta( $id, 'activitypub_user_id', 1 );
}
$followers = \Activitypub\Collection\Followers::get_all_followers();
$this->assertCount( 30, $followers );
}
public static function http_request_host_is_external( $in, $host ) { public static function http_request_host_is_external( $in, $host ) {
if ( in_array( $host, array( 'example.com', 'example.org' ), true ) ) { if ( in_array( $host, array( 'example.com', 'example.org' ), true ) ) {
return true; return true;

View file

@ -1,149 +1,9 @@
<?php <?php
class Test_Functions extends ActivityPub_TestCase_Cache_HTTP { class Test_Functions extends ActivityPub_TestCase_Cache_HTTP {
public $user_id;
public $post_id;
public function test_get_remote_metadata_by_actor() { public function test_get_remote_metadata_by_actor() {
$metadata = \ActivityPub\get_remote_metadata_by_actor( 'pfefferle@notiz.blog' ); $metadata = \ActivityPub\get_remote_metadata_by_actor( 'pfefferle@notiz.blog' );
$this->assertEquals( 'https://notiz.blog/author/matthias-pfefferle/', $metadata['url'] ); $this->assertEquals( 'https://notiz.blog/author/matthias-pfefferle/', $metadata['url'] );
$this->assertEquals( 'pfefferle', $metadata['preferredUsername'] ); $this->assertEquals( 'pfefferle', $metadata['preferredUsername'] );
$this->assertEquals( 'Matthias Pfefferle', $metadata['name'] ); $this->assertEquals( 'Matthias Pfefferle', $metadata['name'] );
} }
public function set_up() {
$this->post_id = \wp_insert_post(
array(
'post_author' => $this->user_id,
'post_content' => 'test',
)
);
}
public function test_object_id_to_comment_basic() {
$single_comment_source_id = 'https://example.com/single';
$content = 'example';
$comment_id = \wp_new_comment(
array(
'comment_post_ID' => $this->post_id,
'comment_author' => 'Example User',
'comment_author_url' => 'https://example.com/user',
'comment_content' => $content,
'comment_type' => '',
'comment_author_email' => '',
'comment_parent' => 0,
'comment_meta' => array(
'source_id' => $single_comment_source_id,
'source_url' => 'https://example.com/123',
'avatar_url' => 'https://example.com/icon',
'protocol' => 'activitypub',
),
),
true
);
$query_result = \Activitypub\object_id_to_comment( $single_comment_source_id );
$this->assertInstanceOf( WP_Comment::class, $query_result );
$this->assertEquals( $comment_id, $query_result->comment_ID );
$this->assertEquals( $content, $query_result->comment_content );
}
public function test_object_id_to_comment_none() {
$single_comment_source_id = 'https://example.com/none';
$query_result = \Activitypub\object_id_to_comment( $single_comment_source_id );
$this->assertFalse( $query_result );
}
public function test_object_id_to_comment_duplicate() {
$duplicate_comment_source_id = 'https://example.com/duplicate';
for ( $i = 0; $i < 2; ++$i ) {
\wp_new_comment(
array(
'comment_post_ID' => $this->post_id,
'comment_author' => 'Example User',
'comment_author_url' => 'https://example.com/user',
'comment_content' => 'example',
'comment_type' => '',
'comment_author_email' => '',
'comment_parent' => 0,
'comment_meta' => array(
'source_id' => $duplicate_comment_source_id,
'source_url' => 'https://example.com/123',
'avatar_url' => 'https://example.com/icon',
'protocol' => 'activitypub',
),
),
true
);
}
$query_result = \Activitypub\object_id_to_comment( $duplicate_comment_source_id );
$this->assertFalse( $query_result );
}
/**
* @dataProvider object_to_uri_provider
*/
public function test_object_to_uri( $input, $output ) {
$this->assertEquals( $output, \Activitypub\object_to_uri( $input ) );
}
public function object_to_uri_provider() {
return array(
array( null, null ),
array( 'https://example.com', 'https://example.com' ),
array( array( 'https://example.com' ), 'https://example.com' ),
array(
array(
'https://example.com',
'https://example.org',
),
'https://example.com',
),
array(
array(
'type' => 'Link',
'href' => 'https://example.com',
),
'https://example.com',
),
array(
array(
array(
'type' => 'Link',
'href' => 'https://example.com',
),
array(
'type' => 'Link',
'href' => 'https://example.org',
),
),
'https://example.com',
),
array(
array(
'type' => 'Actor',
'id' => 'https://example.com',
),
'https://example.com',
),
array(
array(
array(
'type' => 'Actor',
'id' => 'https://example.com',
),
array(
'type' => 'Actor',
'id' => 'https://example.org',
),
),
'https://example.com',
),
array(
array(
'type' => 'Activity',
'id' => 'https://example.com',
),
'https://example.com',
),
);
}
} }