updated plugin ActivityPub version 2.4.0

This commit is contained in:
KawaiiPunk 2024-06-27 12:10:38 +00:00 committed by Gitium
parent eeef5ad6e0
commit 4e493c268e
49 changed files with 1368 additions and 491 deletions

View File

@ -1,42 +0,0 @@
.DS_Store
.editorconfig
.git
.gitignore
.github
.travis.yml
.codeclimate.yml
.data
.svnignore
.wordpress-org
.php_cs
Gruntfile.js
LINGUAS
Makefile
README.md
readme.md
CHANGELOG.md
CODE_OF_CONDUCT.md
FEDERATION.md
SECURITY.md
LICENSE.md
_site
_config.yml
bin
composer.json
composer.lock
docker-compose.yml
docker-compose-test.yml
Dockerfile
gulpfile.js
package.json
node_modules
npm-debug.log
phpcs.xml
package.json
package-lock.json
phpunit.xml
phpunit.xml.dist
tests
node_modules
vendor
src

View File

@ -3,7 +3,7 @@
* Plugin Name: 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.
* Version: 2.3.1
* Version: 2.4.0
* Author: Matthias Pfefferle & Automattic
* Author URI: https://automattic.com/
* License: MIT
@ -21,7 +21,7 @@ use function Activitypub\site_supports_blocks;
require_once __DIR__ . '/includes/compat.php';
require_once __DIR__ . '/includes/functions.php';
\define( 'ACTIVITYPUB_PLUGIN_VERSION', '2.3.1' );
\define( 'ACTIVITYPUB_PLUGIN_VERSION', '2.4.0' );
/**
* Initialize the plugin constants.
@ -50,7 +50,7 @@ require_once __DIR__ . '/includes/functions.php';
* Initialize REST routes.
*/
function rest_init() {
Rest\Users::init();
Rest\Actors::init();
Rest\Outbox::init();
Rest\Inbox::init();
Rest\Followers::init();
@ -100,6 +100,11 @@ function plugin_init() {
require_once __DIR__ . '/integration/class-enable-mastodon-apps.php';
Integration\Enable_Mastodon_Apps::init();
if ( \defined( 'JETPACK__VERSION' ) && ! \defined( 'IS_WPCOM' ) ) {
require_once __DIR__ . '/integration/class-jetpack.php';
Integration\Jetpack::init();
}
}
\add_action( 'plugins_loaded', __NAMESPACE__ . '\plugin_init' );

View File

@ -197,3 +197,8 @@ input.blog-user-identifier {
border-bottom: none;
margin-bottom: 0;
}
#dashboard_right_now li a.activitypub-followers::before {
content: "\f307";
font-family: dashicons;
}

File diff suppressed because one or more lines are too long

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' => '536d43b3eaab93a5c9ef');
<?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' => 'a351235e5feab398a954');

View File

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

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' => '23bc54443801976420cd');
<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-components', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-url'), 'version' => '8c01e26171636c3b698f');

View File

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

File diff suppressed because one or more lines are too long

View File

@ -162,7 +162,13 @@ class Activity extends Base_Object {
}
if ( $object->get_id() && ! $this->get_id() ) {
$this->set( 'id', $object->get_id() . '#activity' );
$id = strtok( $object->get_id(), '#' );
if ( $object->get_updated() ) {
$updated = $object->get_updated();
} else {
$updated = $object->get_published();
}
$this->set( 'id', $id . '#activity-' . strtolower( $this->get_type() ) . '-' . $updated );
}
}

View File

@ -64,7 +64,11 @@ class Activity_Dispatcher {
* @return void
*/
public static function send_activity( $wp_object, $type, $user_id = null ) {
$transformer = Factory::get_transformer( $wp_object );
$transformer = Factory::get_transformer( $wp_object ); // Could potentially return a `\WP_Error` instance.
if ( \is_wp_error( $transformer ) ) {
return;
}
if ( null !== $user_id ) {
$transformer->change_wp_user_id( $user_id );
@ -98,18 +102,22 @@ class Activity_Dispatcher {
return;
}
// do not announce replies
if ( $wp_object instanceof WP_Comment ) {
$transformer = Factory::get_transformer( $wp_object );
if ( \is_wp_error( $transformer ) ) {
return;
}
$transformer = Factory::get_transformer( $wp_object );
$transformer->change_wp_user_id( Users::BLOG_USER_ID );
$user_id = Users::BLOG_USER_ID;
$activity = $transformer->to_activity( $type );
$user = Users::get_by_id( Users::BLOG_USER_ID );
$user_id = $transformer->get_wp_user_id();
$activity = $transformer->to_activity( 'Announce' );
$announce = new Activity();
$announce->set_type( 'Announce' );
$announce->set_object( $activity );
$announce->set_actor( $user->get_id() );
self::send_activity_to_followers( $activity, $user_id, $wp_object );
self::send_activity_to_followers( $announce, $user_id, $wp_object );
}
/**

View File

@ -9,6 +9,7 @@ use Activitypub\Collection\Followers;
use function Activitypub\is_comment;
use function Activitypub\sanitize_url;
use function Activitypub\is_local_comment;
use function Activitypub\is_user_type_disabled;
use function Activitypub\is_activitypub_request;
use function Activitypub\should_comment_be_federated;
@ -95,37 +96,39 @@ class Activitypub {
$json_template = false;
// check if user can publish posts
if ( \is_author() && is_wp_error( Users::get_by_id( \get_the_author_meta( 'ID' ) ) ) ) {
return $template;
}
// check if blog-user is enabled
if ( \is_home() && is_wp_error( Users::get_by_id( Users::BLOG_USER_ID ) ) ) {
return $template;
}
if ( \is_author() ) {
if ( \is_author() && ! is_user_disabled( \get_the_author_meta( 'ID' ) ) ) {
$json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/author-json.php';
} elseif ( is_comment() ) {
$json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/comment-json.php';
} elseif ( \is_singular() ) {
$json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/post-json.php';
} elseif ( \is_home() ) {
} elseif ( \is_home() && ! is_user_type_disabled( 'blog' ) ) {
$json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/blog-json.php';
}
if ( ACTIVITYPUB_AUTHORIZED_FETCH ) {
/*
* Check if the request is authorized.
*
* @see https://www.w3.org/wiki/SocialCG/ActivityPub/Primer/Authentication_Authorization#Authorized_fetch
* @see https://swicg.github.io/activitypub-http-signature/#authorized-fetch
*/
if ( $json_template && ACTIVITYPUB_AUTHORIZED_FETCH ) {
$verification = Signature::verify_http_signature( $_SERVER );
if ( \is_wp_error( $verification ) ) {
header( 'HTTP/1.1 401 Unauthorized' );
// fallback as template_loader can't return http headers
return $template;
}
}
if ( $json_template ) {
return $json_template;
}
return $template;
}
/**
* Custom redirects for ActivityPub requests.
*
@ -292,7 +295,7 @@ class Activitypub {
\add_rewrite_rule(
'^@([\w\-\.]+)',
'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/users/$matches[1]',
'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/actors/$matches[1]',
'top'
);

View File

@ -2,8 +2,10 @@
namespace Activitypub;
use WP_User_Query;
use Activitypub\Model\Blog_User;
use Activitypub\Model\Blog;
use Activitypub\Collection\Users;
use function Activitypub\count_followers;
use function Activitypub\is_user_disabled;
use function Activitypub\was_comment_received;
use function Activitypub\is_comment_federatable;
@ -37,6 +39,8 @@ class Admin {
if ( ! is_user_disabled( get_current_user_id() ) ) {
\add_action( 'show_user_profile', array( self::class, 'add_profile' ) );
}
\add_filter( 'dashboard_glance_items', array( self::class, 'dashboard_glance_items' ) );
}
/**
@ -216,7 +220,7 @@ class Admin {
'type' => 'string',
'description' => \esc_html__( 'The Identifier of the Blog-User', 'activitypub' ),
'show_in_rest' => true,
'default' => Blog_User::get_default_username(),
'default' => Blog::get_default_username(),
'sanitize_callback' => function ( $value ) {
// hack to allow dots in the username
$parts = explode( '.', $value );
@ -247,7 +251,7 @@ class Admin {
'error'
);
return Blog_User::get_default_username();
return Blog::get_default_username();
}
return $sanitized;
@ -313,8 +317,12 @@ class Admin {
public static function enqueue_scripts( $hook_suffix ) {
if ( false !== strpos( $hook_suffix, 'activitypub' ) ) {
wp_enqueue_style( 'activitypub-admin-styles', plugins_url( 'assets/css/activitypub-admin.css', ACTIVITYPUB_PLUGIN_FILE ), array(), '1.0.0' );
wp_enqueue_script( 'activitypub-admin-styles', plugins_url( 'assets/js/activitypub-admin.js', ACTIVITYPUB_PLUGIN_FILE ), array( 'jquery' ), '1.0.0', false );
wp_enqueue_style( 'activitypub-admin-styles', plugins_url( 'assets/css/activitypub-admin.css', ACTIVITYPUB_PLUGIN_FILE ), array(), get_plugin_version() );
wp_enqueue_script( 'activitypub-admin-script', plugins_url( 'assets/js/activitypub-admin.js', ACTIVITYPUB_PLUGIN_FILE ), array( 'jquery' ), get_plugin_version(), false );
}
if ( 'index.php' === $hook_suffix ) {
wp_enqueue_style( 'activitypub-admin-styles', plugins_url( 'assets/css/activitypub-admin.css', ACTIVITYPUB_PLUGIN_FILE ), array(), get_plugin_version() );
}
}
@ -469,4 +477,57 @@ class Admin {
return $sendback;
}
/**
* Add ActivityPub infos to the dashboard glance items
*
* @param array $items The existing glance items.
*
* @return array The extended glance items.
*/
public static function dashboard_glance_items( $items ) {
\add_filter( 'number_format_i18n', '\Activitypub\custom_large_numbers', 10, 3 );
if ( ! is_user_disabled( get_current_user_id() ) ) {
$follower_count = sprintf(
// translators: %s: number of followers
_n(
'%s Follower',
'%s Followers',
count_followers( \get_current_user_id() ),
'activitypub'
),
\number_format_i18n( count_followers( \get_current_user_id() ) )
);
$items['activitypub-followers-user'] = sprintf(
'<a class="activitypub-followers" href="%1$s" title="%2$s">%3$s</a>',
\esc_url( \admin_url( 'users.php?page=activitypub-followers-list' ) ),
\esc_attr__( 'Your followers', 'activitypub' ),
\esc_html( $follower_count )
);
}
if ( ! is_user_type_disabled( 'blog' ) && current_user_can( 'manage_options' ) ) {
$follower_count = sprintf(
// translators: %s: number of followers
_n(
'%s Follower (Blog)',
'%s Followers (Blog)',
count_followers( Users::BLOG_USER_ID ),
'activitypub'
),
\number_format_i18n( count_followers( Users::BLOG_USER_ID ) )
);
$items['activitypub-followers-blog'] = sprintf(
'<a class="activitypub-followers" href="%1$s" title="%2$s">%3$s</a>',
\esc_url( \admin_url( 'options-general.php?page=activitypub&tab=followers' ) ),
\esc_attr__( 'The Blog\'s followers', 'activitypub' ),
\esc_html( $follower_count )
);
}
\remove_filter( 'number_format_i18n', '\Activitypub\custom_large_numbers', 10, 3 );
return $items;
}
}

View File

@ -379,7 +379,7 @@ class Comment {
}
// generate URI based on comment ID
return \add_query_arg( 'c', $comment->comment_ID, \home_url() );
return \add_query_arg( 'c', $comment->comment_ID, \trailingslashit( \home_url() ) );
}
/**

View File

@ -1,6 +1,7 @@
<?php
namespace Activitypub;
use Activitypub\Handler\Announce;
use Activitypub\Handler\Create;
use Activitypub\Handler\Delete;
use Activitypub\Handler\Follow;
@ -22,6 +23,7 @@ class Handler {
* Register handlers.
*/
public static function register_handlers() {
Announce::init();
Create::init();
Delete::init();
Follow::init();

View File

@ -154,4 +154,98 @@ class Http {
public static function generate_cache_key( $url ) {
return 'activitypub_http_' . \md5( $url );
}
/**
* Requests the Data from the Object-URL or Object-Array
*
* @param array|string $url_or_object The Object or the Object URL.
* @param bool $cached If the result should be cached.
*
* @return array|WP_Error The Object data as array or WP_Error on failure.
*/
public static function get_remote_object( $url_or_object, $cached = true ) {
if ( is_array( $url_or_object ) ) {
if ( array_key_exists( 'id', $url_or_object ) ) {
$url = $url_or_object['id'];
} elseif ( array_key_exists( 'url', $url_or_object ) ) {
$url = $url_or_object['url'];
} else {
return new WP_Error(
'activitypub_no_valid_actor_identifier',
\__( 'The "actor" identifier is not valid', 'activitypub' ),
array(
'status' => 404,
'object' => $url_or_object,
)
);
}
} else {
$url = $url_or_object;
}
if ( preg_match( '/^@?' . ACTIVITYPUB_USERNAME_REGEXP . '$/i', $url ) ) {
$url = Webfinger::resolve( $url );
}
if ( ! $url ) {
return new WP_Error(
'activitypub_no_valid_actor_identifier',
\__( 'The "actor" identifier is not valid', 'activitypub' ),
array(
'status' => 404,
'object' => $url,
)
);
}
if ( is_wp_error( $url ) ) {
return $url;
}
$transient_key = self::generate_cache_key( $url );
// only check the cache if needed.
if ( $cached ) {
$data = \get_transient( $transient_key );
if ( $data ) {
return $data;
}
}
if ( ! \wp_http_validate_url( $url ) ) {
return new WP_Error(
'activitypub_no_valid_object_url',
\__( 'The "object" is/has no valid URL', 'activitypub' ),
array(
'status' => 400,
'object' => $url,
)
);
}
$response = self::get( $url );
if ( \is_wp_error( $response ) ) {
return $response;
}
$data = \wp_remote_retrieve_body( $response );
$data = \json_decode( $data, true );
if ( ! $data ) {
return new WP_Error(
'activitypub_invalid_json',
\__( 'No valid JSON data', 'activitypub' ),
array(
'status' => 400,
'object' => $url,
)
);
}
\set_transient( $transient_key, $data, WEEK_IN_SECONDS );
return $data;
}
}

View File

@ -4,6 +4,8 @@ namespace Activitypub;
use WP_Error;
use Activitypub\Webfinger;
use function Activitypub\object_to_uri;
/**
* ActivityPub Mention Class
*
@ -93,7 +95,11 @@ class Mention {
public static function replace_with_links( $result ) {
$metadata = get_remote_metadata_by_actor( $result[0] );
if ( ! empty( $metadata ) && ! is_wp_error( $metadata ) && ! empty( $metadata['url'] ) ) {
if (
! empty( $metadata ) &&
! is_wp_error( $metadata ) &&
( ! empty( $metadata['id'] ) || ! empty( $metadata['url'] ) )
) {
$username = ltrim( $result[0], '@' );
if ( ! empty( $metadata['name'] ) ) {
$username = $metadata['name'];
@ -102,11 +108,7 @@ class Mention {
$username = $metadata['preferredUsername'];
}
$url = isset( $metadata['url'] ) ? $metadata['url'] : $metadata['id'];
if ( \is_array( $url ) ) {
$url = $url[0];
}
$url = isset( $metadata['url'] ) ? object_to_uri( $metadata['url'] ) : object_to_uri( $metadata['id'] );
return \sprintf( '<a rel="mention" class="u-url mention" href="%s">@<span>%s</span></a>', esc_url( $url ), esc_html( $username ) );
}

View File

@ -2,7 +2,7 @@
namespace Activitypub;
use Activitypub\Activitypub;
use Activitypub\Model\Blog_User;
use Activitypub\Model\Blog;
use Activitypub\Collection\Followers;
/**

View File

@ -0,0 +1,58 @@
<?php
namespace Activitypub;
/**
* Notification class.
*/
class Notification {
/**
* The type of the notification.
*
* @var string
*/
public $type;
/**
* The actor URL.
*
* @var string
*/
public $actor;
/**
* The Activity object.
*
* @var array
*/
public $object;
/**
* The WordPress User-Id.
*
* @var int
*/
public $target;
/**
* Notification constructor.
*
* @param string $type The type of the notification.
* @param string $actor The actor URL.
* @param array $object The Activity object.
* @param int $target The WordPress User-Id.
*/
public function __construct( $type, $actor, $object, $target ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.objectFound
$this->type = $type;
$this->actor = $actor;
$this->object = $object;
$this->target = $target;
}
/**
* Send the notification.
*/
public function send() {
do_action( 'activitypub_notification', $this );
}
}

View File

@ -4,8 +4,8 @@ namespace Activitypub\Collection;
use WP_Error;
use WP_User_Query;
use Activitypub\Model\User;
use Activitypub\Model\Blog_User;
use Activitypub\Model\Application_User;
use Activitypub\Model\Blog;
use Activitypub\Model\Application;
use function Activitypub\object_to_uri;
use function Activitypub\url_to_authorid;
@ -47,9 +47,9 @@ class Users {
}
if ( self::BLOG_USER_ID === $user_id ) {
return Blog_User::from_wp_user( $user_id );
return new Blog();
} elseif ( self::APPLICATION_USER_ID === $user_id ) {
return Application_User::from_wp_user( $user_id );
return new Application();
} elseif ( $user_id > 0 ) {
return User::from_wp_user( $user_id );
}
@ -70,12 +70,12 @@ class Users {
*/
public static function get_by_username( $username ) {
// check for blog user.
if ( Blog_User::get_default_username() === $username ) {
return self::get_by_id( self::BLOG_USER_ID );
if ( Blog::get_default_username() === $username ) {
return new Blog();
}
if ( get_option( 'activitypub_blog_user_identifier' ) === $username ) {
return self::get_by_id( self::BLOG_USER_ID );
return new Blog();
}
// check for application user.
@ -144,25 +144,32 @@ class Users {
// try to extract the scheme and the host
if ( preg_match( '/^([a-zA-Z^:]+):(.*)$/i', $resource, $match ) ) {
// extract the scheme
$scheme = esc_attr( $match[1] );
$scheme = \esc_attr( $match[1] );
}
switch ( $scheme ) {
// check for http(s) URIs
case 'http':
case 'https':
$url_parts = wp_parse_url( $resource );
$resource_path = \wp_parse_url( $resource, PHP_URL_PATH );
if ( $resource_path ) {
$blog_path = \wp_parse_url( \home_url(), PHP_URL_PATH );
if ( $blog_path ) {
$resource_path = \str_replace( $blog_path, '', $resource_path );
}
$resource_path = \trim( $resource_path, '/' );
// check for http(s)://blog.example.com/@username
if (
isset( $url_parts['path'] ) &&
str_starts_with( $url_parts['path'], '/@' )
) {
$identifier = str_replace( '/@', '', $url_parts['path'] );
$identifier = untrailingslashit( $identifier );
if ( str_starts_with( $resource_path, '@' ) ) {
$identifier = \str_replace( '@', '', $resource_path );
$identifier = \trim( $identifier, '/' );
return self::get_by_username( $identifier );
}
}
// check for http(s)://blog.example.com/author/username
$user_id = url_to_authorid( $resource );
@ -222,18 +229,26 @@ class Users {
* @return \Acitvitypub\Model\User The User.
*/
public static function get_by_various( $id ) {
$user = null;
if ( is_numeric( $id ) ) {
return self::get_by_id( $id );
$user = self::get_by_id( $id );
} elseif (
// is URL
filter_var( $id, FILTER_VALIDATE_URL ) ||
// is acct
str_starts_with( $id, 'acct:' )
str_starts_with( $id, 'acct:' ) ||
// is email
filter_var( $id, FILTER_VALIDATE_EMAIL )
) {
return self::get_by_resource( $id );
} else {
return self::get_by_username( $id );
$user = self::get_by_resource( $id );
}
if ( $user && ! is_wp_error( $user ) ) {
return $user;
}
return self::get_by_username( $id );
}
/**

View File

@ -854,3 +854,118 @@ function get_masked_wp_version() {
return implode( '.', $version );
}
/**
* Get the enclosures of a post.
*
* @param int $post_id The post ID.
*
* @return array The enclosures.
*/
function get_enclosures( $post_id ) {
$enclosures = get_post_meta( $post_id, 'enclosure' );
if ( ! $enclosures ) {
return array();
}
$enclosures = array_map(
function ( $enclosure ) {
$attributes = explode( "\n", $enclosure );
if ( ! isset( $attributes[0] ) || ! \wp_http_validate_url( $attributes[0] ) ) {
return false;
}
return array(
'url' => $attributes[0],
'length' => isset( $attributes[1] ) ? trim( $attributes[1] ) : null,
'mediaType' => isset( $attributes[2] ) ? trim( $attributes[2] ) : null,
);
},
$enclosures
);
return array_filter( $enclosures );
}
/**
* Retrieves the IDs of the ancestors of a comment.
*
* Adaption of `get_post_ancestors` from WordPress core.
*
* @see https://developer.wordpress.org/reference/functions/get_post_ancestors/
*
* @param int|WP_Comment $comment Comment ID or comment object.
*
* @return WP_Comment[] Array of ancestor comments or empty array if there are none.
*/
function get_comment_ancestors( $comment ) {
$comment = \get_comment( $comment );
// phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
if ( ! $comment || empty( $comment->comment_parent ) || $comment->comment_parent == $comment->comment_ID ) {
return array();
}
$ancestors = array();
$id = (int) $comment->comment_parent;
$ancestors[] = $id;
// phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
while ( $id > 0 ) {
$ancestor = \get_comment( $id );
$parent_id = (int) $ancestor->comment_parent;
// Loop detection: If the ancestor has been seen before, break.
if ( empty( $parent_id ) || ( $parent_id === (int) $comment->comment_ID ) || in_array( $parent_id, $ancestors, true ) ) {
break;
}
$id = $parent_id;
$ancestors[] = $id;
}
return $ancestors;
}
/**
* Change the display of large numbers on the site.
*
* @author Jeremy Herve
*
* @see https://wordpress.org/support/topic/abbreviate-numbers-with-k/
*
* @param string $formatted Converted number in string format.
* @param float $number The number to convert based on locale.
* @param int $decimals Precision of the number of decimal places.
*
* @return string Converted number in string format.
*/
function custom_large_numbers( $formatted, $number, $decimals ) {
global $wp_locale;
$decimals = 0;
$decimal_point = '.';
$thousands_sep = ',';
if ( isset( $wp_locale ) ) {
$decimals = (int) $wp_locale->number_format['decimal_point'];
$decimal_point = $wp_locale->number_format['decimal_point'];
$thousands_sep = $wp_locale->number_format['thousands_sep'];
}
if ( $number < 1000 ) { // any number less than a Thousand.
return \number_format( $number, $decimals, $decimal_point, $thousands_sep );
} elseif ( $number < 1000000 ) { // any number less than a million
return \number_format( $number / 1000, $decimals, $decimal_point, $thousands_sep ) . 'K';
} elseif ( $number < 1000000000 ) { // any number less than a billion
return \number_format( $number / 1000000, $decimals, $decimal_point, $thousands_sep ) . 'M';
} else { // at least a billion
return \number_format( $number / 1000000000, $decimals, $decimal_point, $thousands_sep ) . 'B';
}
// Default fallback. We should not get here.
return $formatted;
}

View File

@ -0,0 +1,69 @@
<?php
namespace Activitypub\Handler;
use Activitypub\Http;
use function Activitypub\is_activity_public;
/**
* Handle Create requests
*/
class Announce {
/**
* Initialize the class, registering WordPress hooks
*/
public static function init() {
\add_action(
'activitypub_inbox_announce',
array( self::class, 'handle_announce' ),
10,
3
);
}
/**
* Handles "Announce" requests
*
* @param array $array The activity-object
* @param int $user_id The id of the local blog-user
* @param Activitypub\Activity $activity The activity object
*
* @return void
*/
public static function handle_announce( $array, $user_id, $activity = null ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.arrayFound
if ( ACTIVITYPUB_DISABLE_INCOMING_INTERACTIONS ) {
return;
}
if ( ! isset( $array['object'] ) ) {
return;
}
// check if Activity is public or not
if ( ! is_activity_public( $array ) ) {
// @todo maybe send email
return;
}
// @todo save the `Announce`-Activity itself
if ( is_string( $array['object'] ) ) {
$object = Http::get_remote_object( $array['object'] );
} else {
$object = $array['object'];
}
if ( ! $object || is_wp_error( $object ) ) {
return;
}
if ( ! isset( $object['type'] ) ) {
return;
}
$type = \strtolower( $object['type'] );
\do_action( 'activitypub_inbox', $object, $user_id, $type, $activity );
\do_action( "activitypub_inbox_{$type}", $object, $user_id, $activity );
}
}

View File

@ -2,6 +2,7 @@
namespace Activitypub\Handler;
use Activitypub\Http;
use Activitypub\Notification;
use Activitypub\Activity\Activity;
use Activitypub\Collection\Users;
use Activitypub\Collection\Followers;
@ -57,6 +58,15 @@ class Follow {
$user_id,
$follower
);
// send notification
$notification = new Notification(
'follow',
$activity['actor'],
$activity,
$user_id
);
$notification->send();
}
/**

View File

@ -1,79 +0,0 @@
<?php
namespace Activitypub\Model;
use WP_Query;
use Activitypub\Signature;
use Activitypub\Collection\Users;
use function Activitypub\get_rest_url_by_path;
class Application_User extends Blog_User {
/**
* The User-ID
*
* @var int
*/
protected $_id = Users::APPLICATION_USER_ID; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore
public function get_type() {
return 'Application';
}
public function get_discoverable() {
return false;
}
public function get_manually_approves_followers() {
return true;
}
/**
* Get the User-Url.
*
* @return string The User-Url.
*/
public function get_url() {
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() {
return 'application';
}
public function get_preferred_username() {
return $this->get_name();
}
public function get_followers() {
return null;
}
public function get_following() {
return null;
}
public function get_attachment() {
return null;
}
public function get_featured() {
return null;
}
public function get_moderators() {
return null;
}
public function get_indexable() {
return false;
}
}

View File

@ -0,0 +1,200 @@
<?php
namespace Activitypub\Model;
use WP_Query;
use Activitypub\Signature;
use Activitypub\Activity\Actor;
use Activitypub\Collection\Users;
use function Activitypub\get_rest_url_by_path;
class Application extends Actor {
/**
* The User-ID
*
* @var int
*/
protected $_id = Users::APPLICATION_USER_ID; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore
/**
* If the User is discoverable.
*
* @see https://docs.joinmastodon.org/spec/activitypub/#discoverable
*
* @context http://joinmastodon.org/ns#discoverable
*
* @var boolean
*/
protected $discoverable = false;
/**
* If the User is indexable.
*
* @context http://joinmastodon.org/ns#indexable
*
* @var boolean
*/
protected $indexable = false;
/**
* The WebFinger Resource.
*
* @var string<url>
*/
protected $webfinger;
public function get_type() {
return 'Application';
}
public function get_manually_approves_followers() {
return true;
}
public function get_id() {
return get_rest_url_by_path( 'application' );
}
/**
* Get the User-Url.
*
* @return string The User-Url.
*/
public function get_url() {
return $this->get_id();
}
/**
* 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 $this->get_url();
}
public function get_name() {
return 'application';
}
public function get_preferred_username() {
return $this->get_name();
}
/**
* Get the User-Icon.
*
* @return array The User-Icon.
*/
public function get_icon() {
// try site icon first
$icon_id = get_option( 'site_icon' );
// try custom logo second
if ( ! $icon_id ) {
$icon_id = get_theme_mod( 'custom_logo' );
}
$icon_url = false;
if ( $icon_id ) {
$icon = wp_get_attachment_image_src( $icon_id, 'full' );
if ( $icon ) {
$icon_url = $icon[0];
}
}
if ( ! $icon_url ) {
// fallback to default icon
$icon_url = plugins_url( '/assets/img/wp-logo.png', ACTIVITYPUB_PLUGIN_FILE );
}
return array(
'type' => 'Image',
'url' => esc_url( $icon_url ),
);
}
/**
* Get the User-Header-Image.
*
* @return array|null The User-Header-Image.
*/
public function get_header_image() {
if ( \has_header_image() ) {
return array(
'type' => 'Image',
'url' => esc_url( \get_header_image() ),
);
}
return null;
}
public function get_published() {
$first_post = new WP_Query(
array(
'orderby' => 'date',
'order' => 'ASC',
'number' => 1,
)
);
if ( ! empty( $first_post->posts[0] ) ) {
$time = \strtotime( $first_post->posts[0]->post_date_gmt );
} else {
$time = \time();
}
return \gmdate( 'Y-m-d\TH:i:s\Z', $time );
}
/**
* Returns the Inbox-API-Endpoint.
*
* @return string The Inbox-Endpoint.
*/
public function get_inbox() {
return get_rest_url_by_path( sprintf( 'actors/%d/inbox', $this->get__id() ) );
}
/**
* Returns the Outbox-API-Endpoint.
*
* @return string The Outbox-Endpoint.
*/
public function get_outbox() {
return get_rest_url_by_path( sprintf( 'actors/%d/outbox', $this->get__id() ) );
}
/**
* Returns a user@domain type of identifier for the user.
*
* @return string The Webfinger-Identifier.
*/
public function get_webfinger() {
return $this->get_preferred_username() . '@' . \wp_parse_url( \home_url(), \PHP_URL_HOST );
}
public function get_public_key() {
return array(
'id' => $this->get_id() . '#main-key',
'owner' => $this->get_id(),
'publicKeyPem' => Signature::get_public_key_for( Users::APPLICATION_USER_ID ),
);
}
/**
* Get the User-Description.
*
* @return string The User-Description.
*/
public function get_summary() {
return \wpautop(
\wp_kses(
\get_bloginfo( 'description' ),
'default'
)
);
}
}

View File

@ -4,13 +4,38 @@ namespace Activitypub\Model;
use WP_Query;
use WP_Error;
use Activitypub\Signature;
use Activitypub\Activity\Actor;
use Activitypub\Collection\Users;
use function Activitypub\is_single_user;
use function Activitypub\is_user_disabled;
use function Activitypub\get_rest_url_by_path;
class Blog_User extends User {
class Blog extends Actor {
/**
* The Featured-Posts.
*
* @see https://docs.joinmastodon.org/spec/activitypub/#featured
*
* @context {
* "@id": "http://joinmastodon.org/ns#featured",
* "@type": "@id"
* }
*
* @var string
*/
protected $featured;
/**
* Moderators endpoint.
*
* @see https://join-lemmy.org/docs/contributors/05-federation.html
*
* @var string
*/
protected $moderators;
/**
* The User-ID
*
@ -18,6 +43,42 @@ class Blog_User extends User {
*/
protected $_id = Users::BLOG_USER_ID; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore
/**
* If the User is indexable.
*
* @context http://joinmastodon.org/ns#indexable
*
* @var boolean
*/
protected $indexable;
/**
* The WebFinger Resource.
*
* @var string<url>
*/
protected $webfinger;
/**
* If the User is discoverable.
*
* @see https://docs.joinmastodon.org/spec/activitypub/#discoverable
*
* @context http://joinmastodon.org/ns#discoverable
*
* @var boolean
*/
protected $discoverable;
/**
* Restrict posting to mods
*
* @see https://join-lemmy.org/docs/contributors/05-federation.html
*
* @var boolean
*/
protected $posting_restricted_to_mods;
public function get_manually_approves_followers() {
return false;
}
@ -26,19 +87,13 @@ class Blog_User extends User {
return true;
}
public static function from_wp_user( $user_id ) {
if ( is_user_disabled( $user_id ) ) {
return new WP_Error(
'activitypub_user_not_found',
\__( 'User not found', 'activitypub' ),
array( 'status' => 404 )
);
}
$object = new static();
$object->_id = $user_id;
return $object;
/**
* Get the User-ID.
*
* @return string The User-ID.
*/
public function get_id() {
return $this->get_url();
}
/**
@ -204,10 +259,6 @@ class Blog_User extends User {
return \gmdate( 'Y-m-d\TH:i:s\Z', $time );
}
public function get_attachment() {
return array();
}
public function get_canonical_url() {
return \home_url();
}
@ -228,6 +279,14 @@ class Blog_User extends User {
return get_rest_url_by_path( 'collections/moderators' );
}
public function get_public_key() {
return array(
'id' => $this->get_id() . '#main-key',
'owner' => $this->get_id(),
'publicKeyPem' => Signature::get_public_key_for( $this->get__id() ),
);
}
public function get_posting_restricted_to_mods() {
if ( 'Group' === $this->get_type() ) {
return true;
@ -235,4 +294,78 @@ class Blog_User extends User {
return null;
}
/**
* Returns the Inbox-API-Endpoint.
*
* @return string The Inbox-Endpoint.
*/
public function get_inbox() {
return get_rest_url_by_path( sprintf( 'actors/%d/inbox', $this->get__id() ) );
}
/**
* Returns the Outbox-API-Endpoint.
*
* @return string The Outbox-Endpoint.
*/
public function get_outbox() {
return get_rest_url_by_path( sprintf( 'actors/%d/outbox', $this->get__id() ) );
}
/**
* Returns the Followers-API-Endpoint.
*
* @return string The Followers-Endpoint.
*/
public function get_followers() {
return get_rest_url_by_path( sprintf( 'actors/%d/followers', $this->get__id() ) );
}
/**
* Returns the Following-API-Endpoint.
*
* @return string The Following-Endpoint.
*/
public function get_following() {
return get_rest_url_by_path( sprintf( 'actors/%d/following', $this->get__id() ) );
}
public function get_endpoints() {
$endpoints = null;
if ( ACTIVITYPUB_SHARED_INBOX_FEATURE ) {
$endpoints = array(
'sharedInbox' => get_rest_url_by_path( 'inbox' ),
);
}
return $endpoints;
}
/**
* Returns a user@domain type of identifier for the user.
*
* @return string The Webfinger-Identifier.
*/
public function get_webfinger() {
return $this->get_preferred_username() . '@' . \wp_parse_url( \home_url(), \PHP_URL_HOST );
}
/**
* Returns the Featured-API-Endpoint.
*
* @return string The Featured-Endpoint.
*/
public function get_featured() {
return get_rest_url_by_path( sprintf( 'actors/%d/collections/featured', $this->get__id() ) );
}
public function get_indexable() {
if ( \get_option( 'blog_public', 1 ) ) {
return true;
} else {
return false;
}
}
}

View File

@ -2,7 +2,7 @@
namespace Activitypub\Model;
use Activitypub\Collection\Users;
use Activitypub\Transformer\Post as Post_Transformer;
use Activitypub\Transformer\Factory;
/**
* ActivityPub Post Class
@ -32,10 +32,14 @@ class Post {
*/
// 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' );
_deprecated_function( __METHOD__, '1.0.0', '\Activitypub\Transformer\Factory::get_transformer' );
$transformer = Factory::get_transformer( $post );
if ( ! \is_wp_error( $transformer ) ) {
$this->post = $post;
$this->object = Post_Transformer::transform( $post )->to_object();
$this->object = $transformer->to_object();
}
}
/**

View File

@ -4,8 +4,9 @@ namespace Activitypub\Model;
use WP_Query;
use WP_Error;
use Activitypub\Signature;
use Activitypub\Collection\Users;
use Activitypub\Model\Blog;
use Activitypub\Activity\Actor;
use Activitypub\Collection\Users;
use function Activitypub\is_user_disabled;
use function Activitypub\get_rest_url_by_path;
@ -32,19 +33,6 @@ class User extends Actor {
*/
protected $featured;
/**
* Moderators endpoint.
*
* @see https://join-lemmy.org/docs/contributors/05-federation.html
*
* @var string
*/
protected $moderators;
public function get_type() {
return 'Person';
}
/**
* If the User is discoverable.
*
@ -72,14 +60,9 @@ class User extends Actor {
*/
protected $webfinger;
/**
* Restrict posting to mods
*
* @see https://join-lemmy.org/docs/contributors/05-federation.html
*
* @var boolean
*/
protected $posting_restricted_to_mods = null;
public function get_type() {
return 'Person';
}
public static function from_wp_user( $user_id ) {
if ( is_user_disabled( $user_id ) ) {
@ -193,7 +176,7 @@ class User extends Actor {
* @return string The Inbox-Endpoint.
*/
public function get_inbox() {
return get_rest_url_by_path( sprintf( 'users/%d/inbox', $this->get__id() ) );
return get_rest_url_by_path( sprintf( 'actors/%d/inbox', $this->get__id() ) );
}
/**
@ -202,7 +185,7 @@ class User extends Actor {
* @return string The Outbox-Endpoint.
*/
public function get_outbox() {
return get_rest_url_by_path( sprintf( 'users/%d/outbox', $this->get__id() ) );
return get_rest_url_by_path( sprintf( 'actors/%d/outbox', $this->get__id() ) );
}
/**
@ -211,7 +194,7 @@ class User extends Actor {
* @return string The Followers-Endpoint.
*/
public function get_followers() {
return get_rest_url_by_path( sprintf( 'users/%d/followers', $this->get__id() ) );
return get_rest_url_by_path( sprintf( 'actors/%d/followers', $this->get__id() ) );
}
/**
@ -220,7 +203,7 @@ class User extends Actor {
* @return string The Following-Endpoint.
*/
public function get_following() {
return get_rest_url_by_path( sprintf( 'users/%d/following', $this->get__id() ) );
return get_rest_url_by_path( sprintf( 'actors/%d/following', $this->get__id() ) );
}
/**
@ -229,7 +212,7 @@ class User extends Actor {
* @return string The Featured-Endpoint.
*/
public function get_featured() {
return get_rest_url_by_path( sprintf( 'users/%d/collections/featured', $this->get__id() ) );
return get_rest_url_by_path( sprintf( 'actors/%d/collections/featured', $this->get__id() ) );
}
public function get_endpoints() {
@ -296,10 +279,6 @@ class User extends Actor {
return $this->get_preferred_username() . '@' . \wp_parse_url( \home_url(), \PHP_URL_HOST );
}
public function get_resource() {
return $this->get_webfinger();
}
public function get_canonical_url() {
return $this->get_url();
}

View File

@ -12,13 +12,13 @@ use Activitypub\Collection\Users as User_Collection;
use function Activitypub\is_activitypub_request;
/**
* ActivityPub Followers REST-Class
* ActivityPub Actors REST-Class
*
* @author Matthias Pfefferle
*
* @see https://www.w3.org/TR/activitypub/#followers
*/
class Users {
class Actors {
/**
* Initialize the class, registering WordPress hooks
*/
@ -32,7 +32,7 @@ class Users {
public static function register_routes() {
\register_rest_route(
ACTIVITYPUB_REST_NAMESPACE,
'/users/(?P<user_id>[\w\-\.]+)',
'/(users|actors)/(?P<user_id>[\w\-\.]+)',
array(
array(
'methods' => WP_REST_Server::READABLE,
@ -45,7 +45,7 @@ class Users {
\register_rest_route(
ACTIVITYPUB_REST_NAMESPACE,
'/users/(?P<user_id>[\w\-\.]+)/remote-follow',
'/(users|actors)/(?P<user_id>[\w\-\.]+)/remote-follow',
array(
array(
'methods' => WP_REST_Server::READABLE,

View File

@ -1,13 +1,12 @@
<?php
namespace Activitypub\Rest;
use WP_Error;
use WP_REST_Server;
use WP_REST_Response;
use Activitypub\Transformer\Post;
use Activitypub\Activity\Actor;
use Activitypub\Activity\Base_Object;
use Activitypub\Collection\Users as User_Collection;
use Activitypub\Transformer\Factory;
use function Activitypub\esc_hashtag;
use function Activitypub\is_single_user;
@ -35,7 +34,7 @@ class Collection {
public static function register_routes() {
\register_rest_route(
ACTIVITYPUB_REST_NAMESPACE,
'/users/(?P<user_id>[\w\-\.]+)/collections/tags',
'/(users|actors)/(?P<user_id>[\w\-\.]+)/collections/tags',
array(
array(
'methods' => WP_REST_Server::READABLE,
@ -48,7 +47,7 @@ class Collection {
\register_rest_route(
ACTIVITYPUB_REST_NAMESPACE,
'/users/(?P<user_id>[\w\-\.]+)/collections/featured',
'/(users|actors)/(?P<user_id>[\w\-\.]+)/collections/featured',
array(
array(
'methods' => WP_REST_Server::READABLE,
@ -104,7 +103,7 @@ class Collection {
$response = array(
'@context' => Base_Object::JSON_LD_CONTEXT,
'id' => get_rest_url_by_path( sprintf( 'users/%d/collections/tags', $user->get__id() ) ),
'id' => get_rest_url_by_path( sprintf( 'actors/%d/collections/tags', $user->get__id() ) ),
'type' => 'Collection',
'totalItems' => is_countable( $tags ) ? count( $tags ) : 0,
'items' => array(),
@ -162,14 +161,20 @@ class Collection {
$response = array(
'@context' => Base_Object::JSON_LD_CONTEXT,
'id' => get_rest_url_by_path( sprintf( 'users/%d/collections/featured', $user_id ) ),
'id' => get_rest_url_by_path( sprintf( 'actors/%d/collections/featured', $user_id ) ),
'type' => 'OrderedCollection',
'totalItems' => is_countable( $posts ) ? count( $posts ) : 0,
'orderedItems' => array(),
);
foreach ( $posts as $post ) {
$response['orderedItems'][] = Post::transform( $post )->to_object()->to_array( false );
$transformer = Factory::get_transformer( $post );
if ( \is_wp_error( $transformer ) ) {
continue;
}
$response['orderedItems'][] = $transformer->to_object()->to_array( false );
}
$rest_response = new WP_REST_Response( $response, 200 );

View File

@ -29,7 +29,7 @@ class Comment {
public static function register_routes() {
\register_rest_route(
ACTIVITYPUB_REST_NAMESPACE,
'/comments/(?P<comment_id>\d+)/remote-reply',
'/(users|actors)/(?P<comment_id>\d+)/remote-reply',
array(
array(
'methods' => WP_REST_Server::READABLE,

View File

@ -32,7 +32,7 @@ class Followers {
public static function register_routes() {
\register_rest_route(
ACTIVITYPUB_REST_NAMESPACE,
'/users/(?P<user_id>[\w\-\.]+)/followers',
'/(users|actors)/(?P<user_id>[\w\-\.]+)/followers',
array(
array(
'methods' => WP_REST_Server::READABLE,
@ -74,13 +74,13 @@ class Followers {
$json->{'@context'} = \Activitypub\get_context();
$json->id = get_rest_url_by_path( sprintf( 'users/%d/followers', $user->get__id() ) );
$json->id = get_rest_url_by_path( sprintf( 'actors/%d/followers', $user->get__id() ) );
$json->generator = 'http://wordpress.org/?v=' . get_masked_wp_version();
$json->actor = $user->get_id();
$json->type = 'OrderedCollectionPage';
$json->totalItems = $data['total']; // phpcs:ignore
$json->partOf = get_rest_url_by_path( sprintf( 'users/%d/followers', $user->get__id() ) ); // phpcs:ignore
$json->partOf = get_rest_url_by_path( sprintf( 'actors/%d/followers', $user->get__id() ) ); // phpcs:ignore
$json->first = \add_query_arg( 'page', 1, $json->partOf ); // phpcs:ignore
$json->last = \add_query_arg( 'page', \ceil ( $json->totalItems / $per_page ), $json->partOf ); // phpcs:ignore

View File

@ -31,7 +31,7 @@ class Following {
public static function register_routes() {
\register_rest_route(
ACTIVITYPUB_REST_NAMESPACE,
'/users/(?P<user_id>[\w\-\.]+)/following',
'/(users|actors)/(?P<user_id>[\w\-\.]+)/following',
array(
array(
'methods' => \WP_REST_Server::READABLE,
@ -67,12 +67,12 @@ class Following {
$json->{'@context'} = \Activitypub\get_context();
$json->id = get_rest_url_by_path( sprintf( 'users/%d/following', $user->get__id() ) );
$json->id = get_rest_url_by_path( sprintf( 'actors/%d/following', $user->get__id() ) );
$json->generator = 'http://wordpress.org/?v=' . get_masked_wp_version();
$json->actor = $user->get_id();
$json->type = 'OrderedCollectionPage';
$json->partOf = get_rest_url_by_path( sprintf( 'users/%d/following', $user->get__id() ) ); // phpcs:ignore
$json->partOf = get_rest_url_by_path( sprintf( 'actors/%d/following', $user->get__id() ) ); // phpcs:ignore
$items = apply_filters( 'activitypub_rest_following', array(), $user ); // phpcs:ignore

View File

@ -48,7 +48,7 @@ class Inbox {
\register_rest_route(
ACTIVITYPUB_REST_NAMESPACE,
'/users/(?P<user_id>[\w\-\.]+)/inbox',
'/(users|actors)/(?P<user_id>[\w\-\.]+)/inbox',
array(
array(
'methods' => WP_REST_Server::CREATABLE,
@ -90,10 +90,10 @@ class Inbox {
$json = new \stdClass();
$json->{'@context'} = get_context();
$json->id = get_rest_url_by_path( sprintf( 'users/%d/inbox', $user->get__id() ) );
$json->id = get_rest_url_by_path( sprintf( 'actors/%d/inbox', $user->get__id() ) );
$json->generator = 'http://wordpress.org/?v=' . get_masked_wp_version();
$json->type = 'OrderedCollectionPage';
$json->partOf = get_rest_url_by_path( sprintf( 'users/%d/inbox', $user->get__id() ) ); // phpcs:ignore
$json->partOf = get_rest_url_by_path( sprintf( 'actors/%d/inbox', $user->get__id() ) ); // phpcs:ignore
$json->totalItems = 0; // phpcs:ignore
$json->orderedItems = array(); // phpcs:ignore
$json->first = $json->partOf; // phpcs:ignore

View File

@ -5,9 +5,9 @@ use stdClass;
use WP_Error;
use WP_REST_Server;
use WP_REST_Response;
use Activitypub\Transformer\Post;
use Activitypub\Activity\Activity;
use Activitypub\Collection\Users as User_Collection;
use Activitypub\Transformer\Factory;
use function Activitypub\get_context;
use function Activitypub\get_rest_url_by_path;
@ -34,7 +34,7 @@ class Outbox {
public static function register_routes() {
\register_rest_route(
ACTIVITYPUB_REST_NAMESPACE,
'/users/(?P<user_id>[\w\-\.]+)/outbox',
'/(users|actors)/(?P<user_id>[\w\-\.]+)/outbox',
array(
array(
'methods' => WP_REST_Server::READABLE,
@ -72,11 +72,11 @@ class Outbox {
$json = new stdClass();
$json->{'@context'} = get_context();
$json->id = get_rest_url_by_path( sprintf( 'users/%d/outbox', $user_id ) );
$json->id = get_rest_url_by_path( sprintf( 'actors/%d/outbox', $user_id ) );
$json->generator = 'http://wordpress.org/?v=' . get_masked_wp_version();
$json->actor = $user->get_id();
$json->type = 'OrderedCollectionPage';
$json->partOf = get_rest_url_by_path( sprintf( 'users/%d/outbox', $user_id ) ); // phpcs:ignore
$json->partOf = get_rest_url_by_path( sprintf( 'actors/%d/outbox', $user_id ) ); // phpcs:ignore
$json->totalItems = 0; // phpcs:ignore
if ( $user_id > 0 ) {
@ -111,7 +111,13 @@ class Outbox {
);
foreach ( $posts as $post ) {
$post = Post::transform( $post )->to_object();
$transformer = Factory::get_transformer( $post );
if ( \is_wp_error( $transformer ) ) {
continue;
}
$post = $transformer->to_object();
$activity = new Activity();
$activity->set_type( 'Create' );
$activity->set_object( $post );

View File

@ -5,7 +5,7 @@ use stdClass;
use WP_Error;
use WP_REST_Response;
use Activitypub\Signature;
use Activitypub\Model\Application_User;
use Activitypub\Model\Application;
/**
* ActivityPub Server REST-Class
@ -47,7 +47,7 @@ class Server {
* @return WP_REST_Response The JSON profile of the Application Actor.
*/
public static function application_actor() {
$user = new Application_User();
$user = new Application();
$json = $user->to_array();
@ -62,6 +62,9 @@ class Server {
*
* @see WP_REST_Request
*
* @see https://www.w3.org/wiki/SocialCG/ActivityPub/Primer/Authentication_Authorization#Authorized_fetch
* @see https://swicg.github.io/activitypub-http-signature/#authorized-fetch
*
* @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client.
* Usually a WP_REST_Response or WP_Error.
* @param array $handler Route handler used for the request.
@ -80,7 +83,8 @@ class Server {
if (
! \str_starts_with( $route, '/' . ACTIVITYPUB_REST_NAMESPACE ) ||
\str_starts_with( $route, '/' . \trailingslashit( ACTIVITYPUB_REST_NAMESPACE ) . 'webfinger' ) ||
\str_starts_with( $route, '/' . \trailingslashit( ACTIVITYPUB_REST_NAMESPACE ) . 'nodeinfo' )
\str_starts_with( $route, '/' . \trailingslashit( ACTIVITYPUB_REST_NAMESPACE ) . 'nodeinfo' ) ||
\str_starts_with( $route, '/' . \trailingslashit( ACTIVITYPUB_REST_NAMESPACE ) . 'application' )
) {
return $response;
}
@ -102,17 +106,12 @@ class Server {
return $response;
}
// POST-Requets are always signed
if ( 'GET' !== $request->get_method() ) {
$verified_request = Signature::verify_http_signature( $request );
if ( \is_wp_error( $verified_request ) ) {
return new WP_Error(
'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
if (
// POST-Requests are always signed
'GET' !== $request->get_method() ||
// GET-Requests only require a signature in secure mode
( 'GET' === $request->get_method() && ACTIVITYPUB_AUTHORIZED_FETCH )
) {
$verified_request = Signature::verify_http_signature( $request );
if ( \is_wp_error( $verified_request ) ) {
return new WP_Error(

View File

@ -1,6 +1,7 @@
<?php
namespace Activitypub\Transformer;
use WP_Error;
use WP_Post;
use WP_Comment;
@ -30,9 +31,9 @@ abstract class Base {
*
* @param WP_Post|WP_Comment $wp_object The WordPress object
*
* @return Base_Object
* @return Base
*/
public static function transform( $object ) {
public static function transform( $object ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.objectFound
return new static( $object );
}

View File

@ -6,12 +6,13 @@ use WP_Comment_Query;
use Activitypub\Webfinger;
use Activitypub\Comment as Comment_Utils;
use Activitypub\Model\Blog_User;
use Activitypub\Model\Blog;
use Activitypub\Collection\Users;
use Activitypub\Transformer\Base;
use function Activitypub\is_single_user;
use function Activitypub\get_rest_url_by_path;
use function Activitypub\get_comment_ancestors;
/**
* WordPress Comment Transformer
@ -69,7 +70,7 @@ class Comment extends Base {
$this->get_locale() => $this->get_content(),
)
);
$path = sprintf( 'users/%d/followers', intval( $comment->comment_author ) );
$path = sprintf( 'actors/%d/followers', intval( $comment->comment_author ) );
$object->set_to(
array(
@ -90,7 +91,7 @@ class Comment extends Base {
*/
protected function get_attributed_to() {
if ( is_single_user() ) {
$user = new Blog_User();
$user = new Blog();
return $user->get_url();
}
@ -218,6 +219,23 @@ class Comment extends Base {
return apply_filters( 'activitypub_extract_mentions', array(), $this->wp_object->comment_content, $this->wp_object );
}
/**
* Gets the ancestors of the comment, but only the ones that are ActivityPub comments.
*
* @return array The list of ancestors.
*/
protected function get_comment_ancestors() {
$ancestors = get_comment_ancestors( $this->wp_object );
// Now that we have the full tree of ancestors, only return the ones received from the fediverse
return array_filter(
$ancestors,
function ( $comment_id ) {
return \get_comment_meta( $comment_id, 'protocol', true ) === 'activitypub';
}
);
}
/**
* Collect all other Users that participated in this comment-thread
* to send them a notification about the new reply.
@ -232,22 +250,14 @@ class Comment extends Base {
return $mentions;
}
$comment_query = new WP_Comment_Query(
array(
'post_id' => $this->wp_object->comment_post_ID,
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
'meta_query' => array(
array(
'key' => 'protocol',
'value' => 'activitypub',
),
),
)
);
$ancestors = $this->get_comment_ancestors();
if ( ! $ancestors ) {
return $mentions;
}
if ( $comment_query->comments ) {
foreach ( $comment_query->comments as $comment ) {
if ( ! empty( $comment->comment_author_url ) ) {
foreach ( $ancestors as $comment_id ) {
$comment = \get_comment( $comment_id );
if ( $comment && ! empty( $comment->comment_author_url ) ) {
$acct = Webfinger::uri_to_acct( $comment->comment_author_url );
if ( $acct && ! is_wp_error( $acct ) ) {
$acct = str_replace( 'acct:', '@', $acct );
@ -255,7 +265,6 @@ class Comment extends Base {
}
}
}
}
return $mentions;
}

View File

@ -11,7 +11,11 @@ use Activitypub\Transformer\Attachment;
* Transformer Factory
*/
class Factory {
public static function get_transformer( $object ) {
/**
* @param mixed $object The object to transform
* @return \Activitypub\Transformer|\WP_Error The transformer to use, or an error.
*/
public static function get_transformer( $object ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.objectFound
if ( ! \is_object( $object ) ) {
return new WP_Error( 'invalid_object', __( 'Invalid object', 'activitypub' ) );
}
@ -41,7 +45,7 @@ class Factory {
* return $transformer;
* }, 10, 3 );
*
* @param Activitypub\Transformer\Base $transformer The transformer to use.
* @param Base $transformer The transformer to use.
* @param mixed $object The object to transform.
* @param string $object_class The class of the object to transform.
*

View File

@ -3,13 +3,13 @@ namespace Activitypub\Transformer;
use WP_Post;
use Activitypub\Shortcodes;
use Activitypub\Model\Blog_User;
use Activitypub\Model\Blog;
use Activitypub\Transformer\Base;
use Activitypub\Collection\Users;
use Activitypub\Activity\Base_Object;
use function Activitypub\esc_hashtag;
use function Activitypub\is_single_user;
use function Activitypub\get_enclosures;
use function Activitypub\get_rest_url_by_path;
use function Activitypub\site_supports_blocks;
@ -70,7 +70,7 @@ class Post extends Base {
$this->get_locale() => $this->get_content(),
)
);
$path = sprintf( 'users/%d/followers', intval( $post->post_author ) );
$path = sprintf( 'actors/%d/followers', intval( $post->post_author ) );
$object->set_to(
array(
@ -116,7 +116,7 @@ class Post extends Base {
* @return string The User-URL.
*/
protected function get_attributed_to() {
$blog_user = new Blog_User();
$blog_user = new Blog();
if ( is_single_user() ) {
return $blog_user->get_url();
@ -139,14 +139,34 @@ class Post extends Base {
protected function get_attachment() {
// Once upon a time we only supported images, but we now support audio/video as well.
// We maintain the image-centric naming for backwards compatibility.
$max_media = \intval( \apply_filters( 'activitypub_max_image_attachments', \get_option( 'activitypub_max_image_attachments', ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS ) ) );
$max_media = \intval(
\apply_filters(
'activitypub_max_image_attachments',
\get_option( 'activitypub_max_image_attachments', ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS )
)
);
if ( site_supports_blocks() && \has_blocks( $this->wp_object->post_content ) ) {
$media = $this->get_block_attachments( $max_media );
} else {
$media = $this->get_classic_editor_images( $max_media );
$media = array(
'audio' => array(),
'video' => array(),
'image' => array(),
);
$id = $this->wp_object->ID;
// list post thumbnail first if this post has one
if ( \function_exists( 'has_post_thumbnail' ) && \has_post_thumbnail( $id ) ) {
$media['image'][] = array( 'id' => \get_post_thumbnail_id( $id ) );
}
$media = $this->get_enclosures( $media );
if ( site_supports_blocks() && \has_blocks( $this->wp_object->post_content ) ) {
$media = $this->get_block_attachments( $media, $max_media );
} else {
$media = $this->get_classic_editor_images( $media, $max_media );
}
$media = self::filter_media_by_object_type( $media, \get_post_format( $this->wp_object ), $this->wp_object );
$unique_ids = \array_unique( \array_column( $media, 'id' ) );
$media = \array_intersect_key( $media, $unique_ids );
$media = \array_slice( $media, 0, $max_media );
@ -157,35 +177,21 @@ class Post extends Base {
/**
* Get media attachments from blocks. They will be formatted as ActivityPub attachments, not as WP attachments.
*
* @param array $media The media array grouped by type.
* @param int $max_media The maximum number of attachments to return.
*
* @return array The attachments.
*/
protected function get_block_attachments( $max_media ) {
protected function get_block_attachments( $media, $max_media ) {
// max media can't be negative or zero
if ( $max_media <= 0 ) {
return array();
}
$id = $this->wp_object->ID;
$media = array(
'image' => array(),
'audio' => array(),
'video' => array(),
);
// list post thumbnail first if this post has one
if ( \function_exists( 'has_post_thumbnail' ) && \has_post_thumbnail( $id ) ) {
$media['image'][] = array( 'id' => \get_post_thumbnail_id( $id ) );
}
if ( $max_media > 0 ) {
$blocks = \parse_blocks( $this->wp_object->post_content );
$media = self::get_media_from_blocks( $blocks, $media );
}
return self::filter_media_by_object_type( $media, \get_post_format( $this->wp_object ) );
return $media;
}
/**
@ -202,6 +208,7 @@ class Post extends Base {
if ( $max_images <= 0 ) {
return array();
}
$images = array();
$query = new \WP_Query(
array(
@ -214,6 +221,7 @@ class Post extends Base {
'posts_per_page' => $max_images,
)
);
foreach ( $query->get_posts() as $attachment ) {
if ( ! \in_array( $attachment->ID, $images, true ) ) {
$images[] = array( 'id' => $attachment->ID );
@ -249,7 +257,7 @@ class Post extends Base {
// This linter warning is a false positive - we have to
// re-count each time here as we modify $images.
// phpcs:ignore Squiz.PHP.DisallowSizeFunctionsInLoops.Found
while ( $tags->next_tag( 'img' ) && ( \count( $images ) < $max_images ) ) {
while ( $tags->next_tag( 'img' ) && ( \count( $images ) <= $max_images ) ) {
$src = $tags->get_attribute( 'src' );
// If the img source is in our uploads dir, get the
@ -292,34 +300,68 @@ class Post extends Base {
* Get post images from the classic editor.
* Note that audio/video attachments are only supported in the block editor.
*
* @param array $media The media array grouped by type.
* @param int $max_images The maximum number of images to return.
*
* @return array The attachments.
*/
protected function get_classic_editor_images( $max_images ) {
protected function get_classic_editor_images( $media, $max_images ) {
// max images can't be negative or zero
if ( $max_images <= 0 ) {
return array();
}
$id = $this->wp_object->ID;
$images = array();
// list post thumbnail first if this post has one
if ( \function_exists( 'has_post_thumbnail' ) && \has_post_thumbnail( $id ) ) {
$images[] = \get_post_thumbnail_id( $id );
}
if ( \count( $images ) < $max_images ) {
if ( \count( $media['image'] ) <= $max_images ) {
if ( \class_exists( '\WP_HTML_Tag_Processor' ) ) {
$images = \array_merge( $images, $this->get_classic_editor_image_embeds( $max_images ) );
$media['image'] = \array_merge( $media['image'], $this->get_classic_editor_image_embeds( $max_images ) );
} else {
$images = \array_merge( $images, $this->get_classic_editor_image_attachments( $max_images ) );
$media['image'] = \array_merge( $media['image'], $this->get_classic_editor_image_attachments( $max_images ) );
}
}
return $images;
return $media;
}
/**
* Get enclosures for a post.
*
* @param array $media The media array grouped by type.
*
* @return array The media array extended with enclosures.
*/
public function get_enclosures( $media ) {
$enclosures = get_enclosures( $this->wp_object->ID );
if ( ! $enclosures ) {
return $media;
}
foreach ( $enclosures as $enclosure ) {
// check if URL is an attachment
$attachment_id = \attachment_url_to_postid( $enclosure['url'] );
if ( $attachment_id ) {
$enclosure['id'] = $attachment_id;
$enclosure['url'] = \wp_get_attachment_url( $attachment_id );
$enclosure['mediaType'] = \get_post_mime_type( $attachment_id );
}
$mime_type = $enclosure['mediaType'];
$mime_type_parts = \explode( '/', $mime_type );
switch ( $mime_type_parts[0] ) {
case 'image':
$media['image'][] = $enclosure;
break;
case 'audio':
$media['audio'][] = $enclosure;
break;
case 'video':
$media['video'][] = $enclosure;
break;
}
}
return $media;
}
/**
@ -331,7 +373,6 @@ class Post extends Base {
* @return array The image IDs.
*/
protected static function get_media_from_blocks( $blocks, $media ) {
foreach ( $blocks as $block ) {
// recurse into inner blocks
if ( ! empty( $block['innerBlocks'] ) ) {
@ -398,18 +439,18 @@ class Post extends Base {
* Filter media IDs by object type.
*
* @param array $media The media array grouped by type.
* @param array $type The object type.
* @param string $type The object type.
*
* @return array The filtered media IDs.
*/
protected static function filter_media_by_object_type( $media, $type ) {
$type = \apply_filters( 'filter_media_by_object_type', \strtolower( $type ) );
protected static function filter_media_by_object_type( $media, $type, $wp_object ) {
$type = \apply_filters( 'filter_media_by_object_type', \strtolower( $type ), $wp_object );
if ( ! empty( $media[ $type ] ) ) {
return $media[ $type ];
}
return array_merge( array(), ...array_values( $media ) );
return array_filter( array_merge( array(), ...array_values( $media ) ) );
}
/**
@ -420,6 +461,10 @@ class Post extends Base {
* @return array The ActivityPub Attachment.
*/
public static function wp_attachment_to_activity_attachment( $media ) {
if ( ! isset( $media['id'] ) ) {
return $media;
}
$id = $media['id'];
$attachment = array();
$mime_type = \get_post_mime_type( $id );
@ -427,14 +472,14 @@ class Post extends Base {
// switching on image/audio/video
switch ( $mime_type_parts[0] ) {
case 'image':
$image_size = 'full';
$image_size = 'large';
/**
* Filter the image URL returned for each post.
*
* @param array|false $thumbnail The image URL, or false if no image is available.
* @param int $id The attachment ID.
* @param string $image_size The image size to retrieve. Set to 'full' by default.
* @param string $image_size The image size to retrieve. Set to 'large' by default.
*/
$thumbnail = apply_filters(
'activitypub_get_image',
@ -488,16 +533,16 @@ class Post extends Base {
* Return details about an image attachment.
*
* @param int $id The attachment ID.
* @param string $image_size The image size to retrieve. Set to 'full' by default.
* @param string $image_size The image size to retrieve. Set to 'large' by default.
*
* @return array|false Array of image data, or boolean false if no image is available.
*/
protected static function get_wordpress_attachment( $id, $image_size = 'full' ) {
protected static function get_wordpress_attachment( $id, $image_size = 'large' ) {
/**
* Hook into the image retrieval process. Before image retrieval.
*
* @param int $id The attachment ID.
* @param string $image_size The image size to retrieve. Set to 'full' by default.
* @param string $image_size The image size to retrieve. Set to 'large' by default.
*/
do_action( 'activitypub_get_image_pre', $id, $image_size );
@ -507,7 +552,7 @@ class Post extends Base {
* Hook into the image retrieval process. After image retrieval.
*
* @param int $id The attachment ID.
* @param string $image_size The image size to retrieve. Set to 'full' by default.
* @param string $image_size The image size to retrieve. Set to 'large' by default.
*/
do_action( 'activitypub_get_image_post', $id, $image_size );
@ -536,7 +581,7 @@ class Post extends Base {
}
// Default to Article.
$object_type = 'Article';
$object_type = 'Note';
$post_format = 'standard';
if ( \get_theme_support( 'post-formats' ) ) {
@ -547,18 +592,12 @@ class Post extends Base {
switch ( $post_type ) {
case 'post':
switch ( $post_format ) {
case 'aside':
case 'status':
case 'quote':
case 'note':
case 'gallery':
case 'image':
case 'video':
case 'audio':
$object_type = 'Note';
case 'standard':
case '':
$object_type = 'Article';
break;
default:
$object_type = 'Article';
$object_type = 'Note';
break;
}
break;
@ -566,7 +605,7 @@ class Post extends Base {
$object_type = 'Page';
break;
default:
$object_type = 'Article';
$object_type = 'Note';
break;
}
@ -593,6 +632,16 @@ class Post extends Base {
return $cc;
}
public function get_audience() {
if ( is_single_user() ) {
return null;
} else {
$blog = new Blog();
return $blog->get_id();
}
}
/**
* Returns a list of Tags, used in the Post.
*
@ -708,6 +757,8 @@ class Post extends Base {
*/
do_action( 'activitypub_before_get_content', $post );
add_filter( 'render_block_core/embed', array( self::class, 'revert_embed_links' ), 10, 2 );
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$post = $this->wp_object;
$content = $this->get_post_content_template();
@ -792,4 +843,21 @@ class Post extends Base {
*/
return apply_filters( 'activitypub_post_locale', $lang, $post_id, $this->wp_object );
}
/**
* Transform Embed blocks to block level link.
*
* Remote servers will simply drop iframe elements, rendering incomplete content.
*
* @see https://www.w3.org/TR/activitypub/#security-sanitizing-content
* @see https://www.w3.org/wiki/ActivityPub/Primer/HTML
*
* @param string $block_content The block content (html)
* @param object $block The block object
*
* @return string A block level link
*/
public static function revert_embed_links( $block_content, $block ) {
return '<p><a href="' . esc_url( $block['attrs']['url'] ) . '">' . $block['attrs']['url'] . '</a></p>';
}
}

View File

@ -28,10 +28,12 @@ class Enable_Mastodon_Apps {
public static function init() {
\add_filter( 'mastodon_api_account_followers', array( self::class, 'api_account_followers' ), 10, 2 );
\add_filter( 'mastodon_api_account', array( self::class, 'api_account_add_followers' ), 20, 2 );
\add_filter( 'mastodon_api_account', array( self::class, 'api_account_external' ), 10, 2 );
\add_filter( 'mastodon_api_account', array( self::class, 'api_account_external' ), 15, 2 );
\add_filter( 'mastodon_api_search', array( self::class, 'api_search' ), 40, 2 );
\add_filter( 'mastodon_api_search', array( self::class, 'api_search_by_url' ), 40, 2 );
\add_filter( 'mastodon_api_get_posts_query_args', array( self::class, 'api_get_posts_query_args' ) );
\add_filter( 'mastodon_api_statuses', array( self::class, 'api_statuses_external' ), 10, 2 );
\add_filter( 'mastodon_api_status_context', array( self::class, 'api_get_replies' ), 10, 23 );
}
/**
@ -102,11 +104,11 @@ class Enable_Mastodon_Apps {
* @return Enable_Mastodon_Apps\Entity\Account The filtered Account
*/
public static function api_account_add_followers( $account, $user_id ) {
if ( ! $account instanceof Account || ! is_numeric( $user_id ) ) {
if ( ! $account instanceof Account ) {
return $account;
}
$user = Users::get_by_id( $user_id );
$user = Users::get_by_various( $user_id );
if ( ! $user || is_wp_error( $user ) ) {
return $account;
@ -130,7 +132,7 @@ class Enable_Mastodon_Apps {
$account->acct = $user->get_preferred_username();
$account->note = $user->get_summary();
$account->followers_count = Followers::count_followers( $user_id );
$account->followers_count = Followers::count_followers( $user->get__id() );
return $account;
}
@ -143,7 +145,7 @@ class Enable_Mastodon_Apps {
* @return Enable_Mastodon_Apps\Entity\Account The filtered Account
*/
public static function api_account_external( $user_data, $user_id ) {
if ( $user_data || is_numeric( $user_id ) ) {
if ( $user_data || ( is_numeric( $user_id ) && $user_id ) ) {
// Only augment.
return $user_data;
}
@ -201,12 +203,38 @@ class Enable_Mastodon_Apps {
$account->header = $data['image']['url'];
$account->header_static = $data['image']['url'];
}
if ( ! isset( $data['published'] ) ) {
$data['published'] = 'now';
}
$account->created_at = new DateTime( $data['published'] );
return $account;
}
public static function api_search_by_url( $search_data, $request ) {
$p = \wp_parse_url( $request->get_param( 'q' ) );
if ( ! $p || ! isset( $p['host'] ) ) {
return $search_data;
}
$object = Http::get_remote_object( $request->get_param( 'q' ), true );
if ( is_wp_error( $object ) || ! isset( $object['attributedTo'] ) ) {
return $search_data;
}
$account = self::get_account_for_actor( $object['attributedTo'] );
if ( ! $account ) {
return $search_data;
}
$status = self::activity_to_status( $object, $account );
if ( $status ) {
$search_data['statuses'][] = $status;
}
return $search_data;
}
public static function api_search( $search_data, $request ) {
$user_id = \get_current_user_id();
if ( ! $user_id ) {
@ -254,7 +282,7 @@ class Enable_Mastodon_Apps {
return $search_data;
}
public function api_get_posts_query_args( $args ) {
public static function api_get_posts_query_args( $args ) {
if ( isset( $args['author'] ) && is_string( $args['author'] ) ) {
$uri = Webfinger_Util::resolve( $args['author'] );
if ( $uri && ! is_wp_error( $uri ) ) {
@ -266,47 +294,19 @@ class Enable_Mastodon_Apps {
return $args;
}
public static function api_statuses_external( $statuses, $args ) {
if ( ! isset( $args['activitypub'] ) ) {
return $statuses;
}
$data = get_remote_metadata_by_actor( $args['activitypub'] );
if ( ! $data || is_wp_error( $data ) || ! isset( $data['outbox'] ) ) {
return $statuses;
}
$response = Http::get( $data['outbox'], true );
if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) !== 200 ) {
return $statuses;
}
$outbox = json_decode( wp_remote_retrieve_body( $response ), true );
if ( ! $outbox || is_wp_error( $outbox ) || ! isset( $outbox['first'] ) ) {
return $statuses;
}
$account = self::get_account_for_actor( $args['activitypub'] );
if ( ! $account ) {
return $statuses;
}
$response = Http::get( $outbox['first'], true );
if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) !== 200 ) {
return $statuses;
}
$posts = json_decode( wp_remote_retrieve_body( $response ), true );
$activitypub_statuses = array_map(
function ( $item ) use ( $account ) {
private static function activity_to_status( $item, $account ) {
if ( isset( $item['object'] ) ) {
$object = $item['object'];
} else {
$object = $item;
}
if ( ! isset( $object['type'] ) || 'Note' !== $object['type'] ) {
return null;
}
$status = new Status();
$status->id = Mastodon_API::remap_url( $object['id'] );
$status->id = $object['id'];
$status->created_at = new DateTime( $object['published'] );
$status->content = $object['content'];
$status->account = $account;
@ -318,8 +318,12 @@ class Enable_Mastodon_Apps {
if ( ! empty( $object['visibility'] ) ) {
$status->visibility = $object['visibility'];
}
if ( ! empty( $object['url'] ) ) {
$status->url = $object['url'];
$status->uri = $object['url'];
} else {
$status->uri = $object['id'];
}
if ( ! empty( $object['attachment'] ) ) {
$status->media_attachments = array_map(
@ -336,12 +340,15 @@ class Enable_Mastodon_Apps {
$attachment = array_merge( $default_attachment, $attachment );
$media_attachment = new Media_Attachment();
$media_attachment->id = Mastodon_API::remap_url( $attachment['url'], $attachment );
$media_attachment->id = $attachment['url'];
$media_attachment->type = strtok( $attachment['mediaType'], '/' );
$media_attachment->url = $attachment['url'];
$media_attachment->preview_url = $attachment['url'];
$media_attachment->description = $attachment['name'];
if ( $attachment['blurhash'] ) {
$media_attachment->blurhash = $attachment['blurhash'];
}
if ( $attachment['width'] > 0 && $attachment['height'] > 0 ) {
$media_attachment->meta = array(
'original' => array(
'width' => $attachment['width'],
@ -349,7 +356,7 @@ class Enable_Mastodon_Apps {
'size' => $attachment['width'] . 'x' . $attachment['height'],
'aspect' => $attachment['width'] / $attachment['height'],
),
);
);}
return $media_attachment;
},
$object['attachment']
@ -357,10 +364,99 @@ class Enable_Mastodon_Apps {
}
return $status;
}
public static function api_statuses_external( $statuses, $args ) {
if ( ! isset( $args['activitypub'] ) ) {
return $statuses;
}
$data = get_remote_metadata_by_actor( $args['activitypub'] );
if ( ! $data || is_wp_error( $data ) || ! isset( $data['outbox'] ) ) {
return $statuses;
}
$outbox = Http::get_remote_object( $data['outbox'], true );
if ( is_wp_error( $outbox ) || ! isset( $outbox['first'] ) ) {
return $statuses;
}
$account = self::get_account_for_actor( $args['activitypub'] );
if ( ! $account ) {
return $statuses;
}
$limit = 10;
if ( isset( $args['posts_per_page'] ) ) {
$limit = $args['posts_per_page'];
}
if ( $limit > 40 ) {
$limit = 40;
}
$activitypub_statuses = array();
$url = $outbox['first'];
$tries = 0;
while ( $url ) {
if ( ++$tries > 3 ) {
break;
}
$posts = Http::get_remote_object( $url, true );
if ( is_wp_error( $posts ) ) {
return $statuses;
}
$new_statuses = array_map(
function ( $item ) use ( $account, $args ) {
if ( $args['exclude_replies'] ) {
if ( isset( $item['object']['inReplyTo'] ) && $item['object']['inReplyTo'] ) {
return null;
}
}
return self::activity_to_status( $item, $account );
},
$posts['orderedItems']
);
$activitypub_statuses = array_merge( $activitypub_statuses, array_filter( $new_statuses ) );
$url = $posts['next'];
return $activitypub_statuses;
if ( count( $activitypub_statuses ) >= $limit ) {
break;
}
}
return array_slice( $activitypub_statuses, 0, $limit );
}
public static function api_get_replies( $context, $post_id, $url ) {
$meta = Http::get_remote_object( $url, true );
if ( is_wp_error( $meta ) || ! isset( $meta['replies']['first']['next'] ) ) {
return $context;
}
$replies_url = $meta['replies']['first']['next'];
$replies = Http::get_remote_object( $replies_url, true );
if ( is_wp_error( $replies ) || ! isset( $replies['items'] ) ) {
return $context;
}
foreach ( $replies['items'] as $url ) {
$response = Http::get( $url, true );
if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) !== 200 ) {
continue;
}
$status = json_decode( wp_remote_retrieve_body( $response ), true );
if ( ! $status || is_wp_error( $status ) ) {
continue;
}
$account = self::get_account_for_actor( $status['attributedTo'] );
$status = self::activity_to_status( $status, $account );
if ( $status ) {
$context['descendants'][ $status->id ] = $status;
}
}
return $context;
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace Activitypub\Integration;
class Jetpack {
public static function init() {
\add_filter( 'jetpack_sync_post_meta_whitelist', [ __CLASS__, 'add_sync_meta' ] );
}
public static function add_sync_meta( $whitelist ) {
if ( ! is_array( $whitelist ) ) {
return $whitelist;
}
$activitypub_meta_keys = [
'activitypub_user_id',
'activitypub_inbox',
'activitypub_actor_json',
];
return \array_merge( $whitelist, $activitypub_meta_keys );
}
}

View File

@ -3,7 +3,7 @@ Contributors: automattic, pfefferle, mediaformat, mattwiebe, akirk, jeherve, nur
Tags: OStatus, fediverse, activitypub, activitystream
Requires at least: 5.5
Tested up to: 6.5
Stable tag: 2.3.1
Stable tag: 2.4.0
Requires PHP: 5.6
License: MIT
License URI: http://opensource.org/licenses/MIT
@ -133,6 +133,32 @@ For reasons of data protection, it is not possible to see the followers of other
== Changelog ==
= 2.4.0 =
* Added: A core/embed block filter to transform iframes to links
* Added: Basic support of incoming `Announce`s
* Added: Improve attachment handling
* Added: Notifications: Introduce general class and use it for new follows
* Added: Always fall back to `get_by_username` if one of the above fail
* Added: Notification support for Jetpack
* Added: EMA: Support for fetching external statuses without replies
* Added: EMA: Remote context
* Added: EMA: Allow searching for URLs
* Added: EMA: Ensuring numeric ids is now done in EMA directly
* Added: Podcast support
* Added: Follower count to "At a Glance" dashboard widget
* Improved: Use `Note` as default Object-Type, instead of `Article`
* Improved: Improve `AUTHORIZED_FETCH`
* Improved: Only send Mentions to comments in the direct hierarchy
* Improved: Improve transformer
* Improved: Improve Lemmy compatibility
* Improved: Updated JS dependencies
* Fixed: EMA: Add missing static keyword and try to lookup if the id is 0
* Fixed: Blog-wide account when WordPress is in subdirectory
* Fixed: Funkwhale URLs
* Fixed: Prevent infinite loops in `get_comment_ancestors`
* Fixed: Better Content-Negotiation handling
= 2.3.1 =
* Added: Enable Mastodon Apps: Add remote outbox fetching

View File

@ -1,5 +1,5 @@
<?php
$user = new \Activitypub\Model\Blog_User();
$user = new \Activitypub\Model\Blog();
/*
* Action triggerd prior to the ActivityPub profile being created and sent to the client

View File

@ -1,36 +1,23 @@
<?php
$comment = \get_comment( \get_query_var( 'c', null ) ); // phpcs:ignore
$transformer = \Activitypub\Transformer\Factory::get_transformer( $comment );
$object = \Activitypub\Transformer\Factory::get_transformer( $comment );
$json = \array_merge( array( '@context' => \Activitypub\get_context() ), $object->to_object()->to_array() );
// filter output
$json = \apply_filters( 'activitypub_json_comment_array', $json );
if ( \is_wp_error( $transformer ) ) {
\wp_die(
\esc_html( $transformer->get_error_message() ),
404
);
}
/*
* Action triggerd prior to the ActivityPub profile being created and sent to the client
*/
\do_action( 'activitypub_json_comment_pre' );
$options = 0;
// JSON_PRETTY_PRINT added in PHP 5.4
if ( \get_query_var( 'pretty' ) ) {
$options |= \JSON_PRETTY_PRINT; // phpcs:ignore
}
$options |= \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT;
/*
* Options to be passed to json_encode()
*
* @param int $options The current options flags
*/
$options = \apply_filters( 'activitypub_json_comment_options', $options );
\header( 'Content-Type: application/activity+json' );
echo \wp_json_encode( $json, $options );
echo $transformer->to_object()->to_json(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
/*
* Action triggerd after the ActivityPub profile has been created and sent to the client
*/
\do_action( 'activitypub_json_comment_comment' );
\do_action( 'activitypub_json_comment_post' );

View File

@ -1,8 +1,15 @@
<?php
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$post = \get_post();
$transformer = \Activitypub\Transformer\Factory::get_transformer( $post );
if ( \is_wp_error( $transformer ) ) {
\wp_die(
esc_html( $transformer->get_error_message() ),
404
);
}
$post_object = \Activitypub\Transformer\Factory::get_transformer( $post )->to_object();
/*
* Action triggerd prior to the ActivityPub profile being created and sent to the client
@ -10,7 +17,7 @@ $post_object = \Activitypub\Transformer\Factory::get_transformer( $post )->to_ob
\do_action( 'activitypub_json_post_pre' );
\header( 'Content-Type: application/activity+json' );
echo $post_object->to_json(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $transformer->to_object()->to_json(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
/*
* Action triggerd after the ActivityPub profile has been created and sent to the client

View File

@ -52,7 +52,7 @@
</th>
<td>
<label for="activitypub_blog_user_identifier">
<input class="blog-user-identifier" name="activitypub_blog_user_identifier" id="activitypub_blog_user_identifier" type="text" value="<?php echo esc_attr( \get_option( 'activitypub_blog_user_identifier', \Activitypub\Model\Blog_User::get_default_username() ) ); ?>" />
<input class="blog-user-identifier" name="activitypub_blog_user_identifier" id="activitypub_blog_user_identifier" type="text" value="<?php echo esc_attr( \get_option( 'activitypub_blog_user_identifier', \Activitypub\Model\Blog::get_default_username() ) ); ?>" />
@<?php echo esc_html( \wp_parse_url( \home_url(), PHP_URL_HOST ) ); ?>
</label>
<p class="description">

View File

@ -19,7 +19,7 @@
<?php
if ( ! \Activitypub\is_user_disabled( \Activitypub\Collection\Users::BLOG_USER_ID ) ) :
$blog_user = new \Activitypub\Model\Blog_User();
$blog_user = new \Activitypub\Model\Blog();
?>
<div class="box">
<h3><?php \esc_html_e( 'Blog profile', 'activitypub' ); ?></h3>