diff --git a/wp-content/plugins/activitypub/activitypub.php b/wp-content/plugins/activitypub/activitypub.php index 478486ef..145f5182 100644 --- a/wp-content/plugins/activitypub/activitypub.php +++ b/wp-content/plugins/activitypub/activitypub.php @@ -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.6.1 + * Version: 3.3.3 * Author: Matthias Pfefferle & Automattic * Author URI: https://automattic.com/ * License: MIT @@ -11,17 +11,18 @@ * Requires PHP: 7.0 * Text Domain: activitypub * Domain Path: /languages + * + * @package Activitypub */ namespace Activitypub; -use function Activitypub\is_blog_public; -use function Activitypub\site_supports_blocks; +use WP_CLI; require_once __DIR__ . '/includes/compat.php'; require_once __DIR__ . '/includes/functions.php'; -\define( 'ACTIVITYPUB_PLUGIN_VERSION', '2.6.1' ); +\define( 'ACTIVITYPUB_PLUGIN_VERSION', '3.3.3' ); /** * Initialize the plugin constants. @@ -32,10 +33,13 @@ require_once __DIR__ . '/includes/functions.php'; \defined( 'ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS' ) || \define( 'ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS', 3 ); \defined( 'ACTIVITYPUB_HASHTAGS_REGEXP' ) || \define( 'ACTIVITYPUB_HASHTAGS_REGEXP', '(?:(?<=\s)|(?<=
)|(?<=
)|^)#([A-Za-z0-9_]+)(?:(?=\s|[[:punct:]]|$))' );
\defined( 'ACTIVITYPUB_USERNAME_REGEXP' ) || \define( 'ACTIVITYPUB_USERNAME_REGEXP', '(?:([A-Za-z0-9\._-]+)@((?:[A-Za-z0-9_-]+\.)+[A-Za-z]+))' );
+\defined( 'ACTIVITYPUB_URL_REGEXP' ) || \define( 'ACTIVITYPUB_URL_REGEXP', '(https?:|www\.)\S+[\w\/]' );
\defined( 'ACTIVITYPUB_CUSTOM_POST_CONTENT' ) || \define( 'ACTIVITYPUB_CUSTOM_POST_CONTENT', "
yourusername@example.com
)","activitypub"),{code:(0,i.createElement)("code",null)})),(0,i.createElement)("div",{className:"activitypub-dialog__button-group"},(0,i.createElement)("input",{type:"text",value:N,onKeyDown:e=>{"Enter"===e?.code&&$()},onChange:e=>R(e.target.value)}),(0,i.createElement)(u.Button,{onClick:$},(0,i.createElement)(k,{icon:a}),b)),l&&(0,i.createElement)("div",{className:"activitypub-dialog__remember"},(0,i.createElement)(u.CheckboxControl,{checked:U,label:(0,s.__)("Remember me for easier comments","activitypub"),onChange:()=>{P(!U)}}))))}const{namespace:N}=window._activityPubOptions,R={avatar:"",webfinger:"@well@hello.dolly",name:(0,s.__)("Hello Dolly Fan Account","activitypub"),url:"#"};function U(e){if(!e)return R;const t={...R,...e};return t.avatar=t?.icon?.url,t}function P({profile:e,popupStyles:t,userId:r}){const{avatar:o,name:n,webfinger:l}=e;return(0,i.createElement)("div",{className:"activitypub-profile"},(0,i.createElement)("img",{className:"activitypub-profile__avatar",src:o,alt:n}),(0,i.createElement)("div",{className:"activitypub-profile__content"},(0,i.createElement)("div",{className:"activitypub-profile__name"},n),(0,i.createElement)("div",{className:"activitypub-profile__handle",title:l},l)),(0,i.createElement)(I,{profile:e,popupStyles:t,userId:r}))}function I({profile:e,popupStyles:t,userId:r}){const[o,n]=(0,m.useState)(!1),l=(0,s.sprintf)((0,s.__)("Follow %s","activitypub"),e?.name);return(0,i.createElement)(i.Fragment,null,(0,i.createElement)(u.Button,{className:"activitypub-profile__follow",onClick:()=>n(!0)},(0,s.__)("Follow","activitypub")),o&&(0,i.createElement)(u.Modal,{className:"activitypub-profile__confirm activitypub__modal",onRequestClose:()=>n(!1),title:l},(0,i.createElement)($,{profile:e,userId:r}),(0,i.createElement)("style",null,t)))}function $({profile:e,userId:t}){const{webfinger:r}=e,o=(0,s.__)("Follow","activitypub"),n=`/${N}/actors/${t}/remote-follow?resource=`,l=(0,s.__)("Copy and paste my profile into the search field of your favorite fediverse app or server.","activitypub");return(0,i.createElement)(C,{actionText:o,copyDescription:l,handle:r,resourceUrl:n})}function T({selectedUser:e,style:t,backgroundColor:r,id:o,useId:n=!1,profileData:l=!1}){const[a,c]=(0,m.useState)(U()),s="site"===e?0:e,u=function(e){return w(".apfmd__button-group .components-button",b(e?.elements?.link?.color?.text)||"#111","#fff",b(e?.elements?.link?.[":hover"]?.color?.text)||"#333")}(t),p=n?{id:o}:{};function v(e){c(U(e))}return(0,m.useEffect)((()=>{if(l)return v(l);(function(e){const t={headers:{Accept:"application/activity+json"},path:`/${N}/actors/${e}`};return f()(t)})(s).then(v)}),[s,l]),(0,i.createElement)("div",{...p},(0,i.createElement)(h,{selector:`#${o}`,style:t,backgroundColor:r}),(0,i.createElement)(P,{profile:a,userId:s,popupStyles:u}))}(0,o.registerBlockType)("activitypub/follow-me",{edit:function({attributes:e,setAttributes:t}){const r=(0,c.useBlockProps)({className:"activitypub-follow-me-block-wrapper"}),o=function(){const e=v?.users?(0,p.useSelect)((e=>e("core").getUsers({who:"authors"}))):[];return(0,m.useMemo)((()=>{if(!e)return[];const t=v?.site?[{label:(0,s.__)("Whole Site","activitypub"),value:"site"}]:[];return e.reduce(((e,t)=>(e.push({label:t.name,value:`${t.id}`}),e)),t)}),[e])}(),{selectedUser:n}=e;return(0,m.useEffect)((()=>{o.length&&(o.find((({value:e})=>e===n))||t({selectedUser:o[0].value}))}),[n,o]),(0,i.createElement)("div",{...r},o.length>1&&(0,i.createElement)(c.InspectorControls,{key:"setting"},(0,i.createElement)(u.PanelBody,{title:(0,s.__)("Followers Options","activitypub")},(0,i.createElement)(u.SelectControl,{label:(0,s.__)("Select User","activitypub"),value:e.selectedUser,options:o,onChange:e=>t({selectedUser:e})}))),(0,i.createElement)(T,{...e,id:r.id}))},save:()=>null,icon:a})},20:(e,t,r)=>{var o=r(609),n=Symbol.for("react.element"),l=(Symbol.for("react.fragment"),Object.prototype.hasOwnProperty),a=o.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,i={key:!0,ref:!0,__self:!0,__source:!0};t.jsx=function(e,t,r){var o,c={},s=null,u=null;for(o in void 0!==r&&(s=""+r),void 0!==t.key&&(s=""+t.key),void 0!==t.ref&&(u=t.ref),t)l.call(t,o)&&!i.hasOwnProperty(o)&&(c[o]=t[o]);if(e&&e.defaultProps)for(o in t=e.defaultProps)void 0===c[o]&&(c[o]=t[o]);return{$$typeof:n,type:e,key:s,ref:u,props:c,_owner:a.current}}},848:(e,t,r)=>{e.exports=r(20)},609:e=>{e.exports=window.React}},r={};function o(e){var n=r[e];if(void 0!==n)return n.exports;var l=r[e]={exports:{}};return t[e](l,l.exports,o),l.exports}o.m=t,e=[],o.O=(t,r,n,l)=>{if(!r){var a=1/0;for(u=0;u@yourusername@example.com
)","activitypub"),{code:(0,i.createElement)("code",null)})),(0,i.createElement)("div",{className:"activitypub-dialog__button-group"},(0,i.createElement)("input",{type:"text",value:I,onKeyDown:e=>{"Enter"===e?.code&&T()},onChange:e=>N(e.target.value)}),(0,i.createElement)(m.Button,{onClick:T},(0,i.createElement)(x,{icon:l}),b)),a&&(0,i.createElement)("div",{className:"activitypub-dialog__remember"},(0,i.createElement)(m.CheckboxControl,{checked:R,label:(0,s.__)("Remember me for easier comments","activitypub"),onChange:()=>{U(!R)}}))))}const{namespace:N}=window._activityPubOptions,R={avatar:"",webfinger:"@well@hello.dolly",name:(0,s.__)("Hello Dolly Fan Account","activitypub"),url:"#"};function U(e){if(!e)return R;const t={...R,...e};return t.avatar=t?.icon?.url,t}function P({profile:e,popupStyles:t,userId:r}){const{webfinger:o,avatar:n,name:a}=e,l=o.startsWith("@")?o:`@${o}`;return(0,i.createElement)("div",{className:"activitypub-profile"},(0,i.createElement)("img",{className:"activitypub-profile__avatar",src:n,alt:a}),(0,i.createElement)("div",{className:"activitypub-profile__content"},(0,i.createElement)("div",{className:"activitypub-profile__name"},a),(0,i.createElement)("div",{className:"activitypub-profile__handle",title:l},l)),(0,i.createElement)(T,{profile:e,popupStyles:t,userId:r}))}function T({profile:e,popupStyles:t,userId:r}){const[o,n]=(0,d.useState)(!1),a=(0,s.sprintf)((0,s.__)("Follow %s","activitypub"),e?.name);return(0,i.createElement)(i.Fragment,null,(0,i.createElement)(m.Button,{className:"activitypub-profile__follow",onClick:()=>n(!0)},(0,s.__)("Follow","activitypub")),o&&(0,i.createElement)(m.Modal,{className:"activitypub-profile__confirm activitypub__modal",onRequestClose:()=>n(!1),title:a},(0,i.createElement)($,{profile:e,userId:r}),(0,i.createElement)("style",null,t)))}function $({profile:e,userId:t}){const{webfinger:r}=e,o=(0,s.__)("Follow","activitypub"),n=`/${N}/actors/${t}/remote-follow?resource=`,a=(0,s.__)("Copy and paste my profile into the search field of your favorite fediverse app or server.","activitypub"),l=r.startsWith("@")?r:`@${r}`;return(0,i.createElement)(I,{actionText:o,copyDescription:a,handle:l,resourceUrl:n})}function j({selectedUser:e,style:t,backgroundColor:r,id:o,useId:n=!1,profileData:a=!1}){const[l,c]=(0,d.useState)(U()),s="site"===e?0:e,u=function(e){return h(".apfmd__button-group .components-button",_(e?.elements?.link?.color?.text)||"#111","#fff",_(e?.elements?.link?.[":hover"]?.color?.text)||"#333")}(t),p=n?{id:o}:{};function m(e){c(U(e))}return(0,d.useEffect)((()=>{if(a)return m(a);(function(e){const t={headers:{Accept:"application/activity+json"},path:`/${N}/actors/${e}`};return y()(t)})(s).then(m)}),[s,a]),(0,i.createElement)("div",{...p},(0,i.createElement)(g,{selector:`#${o}`,style:t,backgroundColor:r}),(0,i.createElement)(P,{profile:l,userId:s,popupStyles:u}))}function B({name:e}){const t=(0,s.sprintf)(/* translators: %s: block name */
+"This %s block will adapt to the page it is on, displaying the user profile associated with a post author (in a loop) or a user archive. It will be empty in other non-author contexts.",e);return(0,i.createElement)(m.Card,null,(0,i.createElement)(m.CardBody,null,(0,d.createInterpolateElement)(t,{strong:(0,i.createElement)("strong",null)})))}(0,o.registerBlockType)("activitypub/follow-me",{edit:function({attributes:e,setAttributes:t,context:{postType:r,postId:o}}){const n=(0,c.useBlockProps)({className:"activitypub-follow-me-block-wrapper"}),a=function({withInherit:e=!1}){const t=v?.users?(0,u.useSelect)((e=>e("core").getUsers({who:"authors"}))):[];return(0,d.useMemo)((()=>{if(!t)return[];const r=[];return v?.site&&r.push({label:(0,s.__)("Site","activitypub"),value:"site"}),e&&v?.users&&r.push({label:(0,s.__)("Dynamic User","activitypub"),value:"inherit"}),t.reduce(((e,t)=>(e.push({label:t.name,value:`${t.id}`}),e)),r)}),[t])}({withInherit:!0}),{selectedUser:l}=e,f="inherit"===l,y=(0,u.useSelect)((e=>{const{getEditedEntityRecord:t}=e(p.store),n=t("postType",r,o)?.author;return null!=n?n:null}),[r,o]);return(0,d.useEffect)((()=>{a.length&&(a.find((({value:e})=>e===l))||t({selectedUser:a[0].value}))}),[l,a]),(0,i.createElement)("div",{...n},a.length>1&&(0,i.createElement)(c.InspectorControls,{key:"setting"},(0,i.createElement)(m.PanelBody,{title:(0,s.__)("Followers Options","activitypub")},(0,i.createElement)(m.SelectControl,{label:(0,s.__)("Select User","activitypub"),value:e.selectedUser,options:a,onChange:e=>t({selectedUser:e})}))),f?y?(0,i.createElement)(j,{...e,id:n.id,selectedUser:y}):(0,i.createElement)(B,{name:(0,s.__)("Follow Me","activitypub")}):(0,i.createElement)(j,{...e,id:n.id}))},save:()=>null,icon:l})},20:(e,t,r)=>{var o=r(609),n=Symbol.for("react.element"),a=(Symbol.for("react.fragment"),Object.prototype.hasOwnProperty),l=o.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,i={key:!0,ref:!0,__self:!0,__source:!0};t.jsx=function(e,t,r){var o,c={},s=null,u=null;for(o in void 0!==r&&(s=""+r),void 0!==t.key&&(s=""+t.key),void 0!==t.ref&&(u=t.ref),t)a.call(t,o)&&!i.hasOwnProperty(o)&&(c[o]=t[o]);if(e&&e.defaultProps)for(o in t=e.defaultProps)void 0===c[o]&&(c[o]=t[o]);return{$$typeof:n,type:e,key:s,ref:u,props:c,_owner:l.current}}},848:(e,t,r)=>{e.exports=r(20)},609:e=>{e.exports=window.React}},r={};function o(e){var n=r[e];if(void 0!==n)return n.exports;var a=r[e]={exports:{}};return t[e](a,a.exports,o),a.exports}o.m=t,e=[],o.O=(t,r,n,a)=>{if(!r){var l=1/0;for(u=0;uyourusername@example.com
)","activitypub"),{code:(0,o.createElement)("code",null)})),(0,o.createElement)("div",{className:"activitypub-dialog__button-group"},(0,o.createElement)("input",{type:"text",value:N,onKeyDown:e=>{"Enter"===e?.code&&j()},onChange:e=>R(e.target.value)}),(0,o.createElement)(s.Button,{onClick:j},(0,o.createElement)(h,{icon:g}),y)),i&&(0,o.createElement)("div",{className:"activitypub-dialog__remember"},(0,o.createElement)(s.CheckboxControl,{checked:I,label:(0,u.__)("Remember me for easier comments","activitypub"),onChange:()=>{P(!I)}}))))}const{namespace:S}=window._activityPubOptions,C={avatar:"",webfinger:"@well@hello.dolly",name:(0,u.__)("Hello Dolly Fan Account","activitypub"),url:"#"};function N(e){if(!e)return C;const t={...C,...e};return t.avatar=t?.icon?.url,t}function R({profile:e,popupStyles:t,userId:r}){const{avatar:n,name:a,webfinger:l}=e;return(0,o.createElement)("div",{className:"activitypub-profile"},(0,o.createElement)("img",{className:"activitypub-profile__avatar",src:n,alt:a}),(0,o.createElement)("div",{className:"activitypub-profile__content"},(0,o.createElement)("div",{className:"activitypub-profile__name"},a),(0,o.createElement)("div",{className:"activitypub-profile__handle",title:l},l)),(0,o.createElement)(I,{profile:e,popupStyles:t,userId:r}))}function I({profile:e,popupStyles:t,userId:r}){const[a,l]=(0,n.useState)(!1),i=(0,u.sprintf)((0,u.__)("Follow %s","activitypub"),e?.name);return(0,o.createElement)(o.Fragment,null,(0,o.createElement)(s.Button,{className:"activitypub-profile__follow",onClick:()=>l(!0)},(0,u.__)("Follow","activitypub")),a&&(0,o.createElement)(s.Modal,{className:"activitypub-profile__confirm activitypub__modal",onRequestClose:()=>l(!1),title:i},(0,o.createElement)(P,{profile:e,userId:r}),(0,o.createElement)("style",null,t)))}function P({profile:e,userId:t}){const{webfinger:r}=e,n=(0,u.__)("Follow","activitypub"),a=`/${S}/actors/${t}/remote-follow?resource=`,l=(0,u.__)("Copy and paste my profile into the search field of your favorite fediverse app or server.","activitypub");return(0,o.createElement)(O,{actionText:n,copyDescription:l,handle:r,resourceUrl:a})}function $({selectedUser:e,style:t,backgroundColor:r,id:a,useId:l=!1,profileData:i=!1}){const[s,u]=(0,n.useState)(N()),p="site"===e?0:e,v=function(e){return d(".apfmd__button-group .components-button",m(e?.elements?.link?.color?.text)||"#111","#fff",m(e?.elements?.link?.[":hover"]?.color?.text)||"#333")}(t),y=l?{id:a}:{};function _(e){u(N(e))}return(0,n.useEffect)((()=>{if(i)return _(i);(function(e){const t={headers:{Accept:"application/activity+json"},path:`/${S}/actors/${e}`};return c()(t)})(p).then(_)}),[p,i]),(0,o.createElement)("div",{...y},(0,o.createElement)(f,{selector:`#${a}`,style:t,backgroundColor:r}),(0,o.createElement)(R,{profile:s,userId:p,popupStyles:v}))}let j=1;l()((()=>{[].forEach.call(document.querySelectorAll(".activitypub-follow-me-block-wrapper"),(e=>{const t=JSON.parse(e.dataset.attrs);(0,n.createRoot)(e).render((0,o.createElement)($,{...t,id:"activitypub-follow-me-block-"+j++,useId:!0}))}))}))},20:(e,t,r)=>{var o=r(609),n=Symbol.for("react.element"),a=(Symbol.for("react.fragment"),Object.prototype.hasOwnProperty),l=o.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,i={key:!0,ref:!0,__self:!0,__source:!0};t.jsx=function(e,t,r){var o,c={},s=null,u=null;for(o in void 0!==r&&(s=""+r),void 0!==t.key&&(s=""+t.key),void 0!==t.ref&&(u=t.ref),t)a.call(t,o)&&!i.hasOwnProperty(o)&&(c[o]=t[o]);if(e&&e.defaultProps)for(o in t=e.defaultProps)void 0===c[o]&&(c[o]=t[o]);return{$$typeof:n,type:e,key:s,ref:u,props:c,_owner:l.current}}},848:(e,t,r)=>{e.exports=r(20)},609:e=>{e.exports=window.React}},r={};function o(e){var n=r[e];if(void 0!==n)return n.exports;var a=r[e]={exports:{}};return t[e](a,a.exports,o),a.exports}o.m=t,e=[],o.O=(t,r,n,a)=>{if(!r){var l=1/0;for(u=0;u@yourusername@example.com
)","activitypub"),{code:(0,o.createElement)("code",null)})),(0,o.createElement)("div",{className:"activitypub-dialog__button-group"},(0,o.createElement)("input",{type:"text",value:N,onKeyDown:e=>{"Enter"===e?.code&&j()},onChange:e=>R(e.target.value)}),(0,o.createElement)(s.Button,{onClick:j},(0,o.createElement)(h,{icon:g}),y)),i&&(0,o.createElement)("div",{className:"activitypub-dialog__remember"},(0,o.createElement)(s.CheckboxControl,{checked:I,label:(0,u.__)("Remember me for easier comments","activitypub"),onChange:()=>{$(!I)}}))))}const{namespace:S}=window._activityPubOptions,C={avatar:"",webfinger:"@well@hello.dolly",name:(0,u.__)("Hello Dolly Fan Account","activitypub"),url:"#"};function N(e){if(!e)return C;const t={...C,...e};return t.avatar=t?.icon?.url,t}function R({profile:e,popupStyles:t,userId:r}){const{webfinger:n,avatar:a,name:l}=e,i=n.startsWith("@")?n:`@${n}`;return(0,o.createElement)("div",{className:"activitypub-profile"},(0,o.createElement)("img",{className:"activitypub-profile__avatar",src:a,alt:l}),(0,o.createElement)("div",{className:"activitypub-profile__content"},(0,o.createElement)("div",{className:"activitypub-profile__name"},l),(0,o.createElement)("div",{className:"activitypub-profile__handle",title:i},i)),(0,o.createElement)(I,{profile:e,popupStyles:t,userId:r}))}function I({profile:e,popupStyles:t,userId:r}){const[a,l]=(0,n.useState)(!1),i=(0,u.sprintf)((0,u.__)("Follow %s","activitypub"),e?.name);return(0,o.createElement)(o.Fragment,null,(0,o.createElement)(s.Button,{className:"activitypub-profile__follow",onClick:()=>l(!0)},(0,u.__)("Follow","activitypub")),a&&(0,o.createElement)(s.Modal,{className:"activitypub-profile__confirm activitypub__modal",onRequestClose:()=>l(!1),title:i},(0,o.createElement)($,{profile:e,userId:r}),(0,o.createElement)("style",null,t)))}function $({profile:e,userId:t}){const{webfinger:r}=e,n=(0,u.__)("Follow","activitypub"),a=`/${S}/actors/${t}/remote-follow?resource=`,l=(0,u.__)("Copy and paste my profile into the search field of your favorite fediverse app or server.","activitypub"),i=r.startsWith("@")?r:`@${r}`;return(0,o.createElement)(O,{actionText:n,copyDescription:l,handle:i,resourceUrl:a})}function P({selectedUser:e,style:t,backgroundColor:r,id:a,useId:l=!1,profileData:i=!1}){const[s,u]=(0,n.useState)(N()),p="site"===e?0:e,v=function(e){return d(".apfmd__button-group .components-button",m(e?.elements?.link?.color?.text)||"#111","#fff",m(e?.elements?.link?.[":hover"]?.color?.text)||"#333")}(t),y=l?{id:a}:{};function _(e){u(N(e))}return(0,n.useEffect)((()=>{if(i)return _(i);(function(e){const t={headers:{Accept:"application/activity+json"},path:`/${S}/actors/${e}`};return c()(t)})(p).then(_)}),[p,i]),(0,o.createElement)("div",{...y},(0,o.createElement)(f,{selector:`#${a}`,style:t,backgroundColor:r}),(0,o.createElement)(R,{profile:s,userId:p,popupStyles:v}))}let j=1;l()((()=>{[].forEach.call(document.querySelectorAll(".activitypub-follow-me-block-wrapper"),(e=>{const t=JSON.parse(e.dataset.attrs);(0,n.createRoot)(e).render((0,o.createElement)(P,{...t,id:"activitypub-follow-me-block-"+j++,useId:!0}))}))}))},20:(e,t,r)=>{var o=r(609),n=Symbol.for("react.element"),a=(Symbol.for("react.fragment"),Object.prototype.hasOwnProperty),l=o.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,i={key:!0,ref:!0,__self:!0,__source:!0};t.jsx=function(e,t,r){var o,c={},s=null,u=null;for(o in void 0!==r&&(s=""+r),void 0!==t.key&&(s=""+t.key),void 0!==t.ref&&(u=t.ref),t)a.call(t,o)&&!i.hasOwnProperty(o)&&(c[o]=t[o]);if(e&&e.defaultProps)for(o in t=e.defaultProps)void 0===c[o]&&(c[o]=t[o]);return{$$typeof:n,type:e,key:s,ref:u,props:c,_owner:l.current}}},848:(e,t,r)=>{e.exports=r(20)},609:e=>{e.exports=window.React}},r={};function o(e){var n=r[e];if(void 0!==n)return n.exports;var a=r[e]={exports:{}};return t[e](a,a.exports,o),a.exports}o.m=t,e=[],o.O=(t,r,n,a)=>{if(!r){var l=1/0;for(u=0;uyourusername@example.com
)","activitypub"),{code:(0,o.createElement)("code",null)})),(0,o.createElement)("div",{className:"activitypub-dialog__button-group"},(0,o.createElement)("input",{type:"text",value:L,onKeyDown:e=>{"Enter"===e?.code&&j()},onChange:e=>S(e.target.value)}),(0,o.createElement)(l.Button,{onClick:j},(0,o.createElement)(s,{icon:f}),C)),m&&(0,o.createElement)("div",{className:"activitypub-dialog__remember"},(0,o.createElement)(l.CheckboxControl,{checked:U,label:(0,c.__)("Remember me for easier comments","activitypub"),onChange:()=>{N(!U)}}))))}const{namespace:C}=window._activityPubOptions;function R({selectedComment:e,commentId:t}){const r=(0,c.__)("Reply","activitypub"),a=`/${C}/comments/${t}/remote-reply?resource=`,n=(0,c.__)("Copy and paste the Comment URL into the search field of your favorite fediverse app or server.","activitypub");return(0,o.createElement)(E,{actionText:r,copyDescription:n,handle:e,resourceUrl:a,myProfile:(0,c.__)("Original Comment URL","activitypub"),rememberProfile:!0})}function x({profileURL:e,template:t,commentURL:r,deleteRemoteUser:a}){return(0,o.createElement)(o.Fragment,null,(0,o.createElement)(l.Button,{variant:"link",className:"comment-reply-link activitypub-remote-reply__button",onClick:()=>{const e=t.replace("{uri}",r);window.open(e,"_blank")}},(0,c.sprintf)((0,c.__)("Reply as %s","activitypub"),e)),(0,o.createElement)(l.Button,{className:"activitypub-remote-profile-delete",onClick:a,title:(0,c.__)("Delete Remote Profile","activitypub")},(0,o.createElement)(s,{icon:u,size:18})))}function O({selectedComment:e,commentId:t}){const[r,n]=(0,a.useState)(!1),i=(0,c.__)("Remote Reply","activitypub"),{profileURL:s,template:m,deleteRemoteUser:p}=h(),u=s&&m;return(0,o.createElement)(o.Fragment,null,u?(0,o.createElement)(x,{profileURL:s,template:m,commentURL:e,deleteRemoteUser:p}):(0,o.createElement)(l.Button,{variant:"link",className:"comment-reply-link activitypub-remote-reply__button",onClick:()=>n(!0)},(0,c.__)("Reply on the Fediverse","activitypub")),r&&(0,o.createElement)(l.Modal,{className:"activitypub-remote-reply__modal activitypub__modal",onRequestClose:()=>n(!1),title:i},(0,o.createElement)(R,{selectedComment:e,commentId:t})))}let k=1;i()((()=>{[].forEach.call(document.querySelectorAll(".activitypub-remote-reply"),(e=>{const t=JSON.parse(e.dataset.attrs);(0,a.createRoot)(e).render((0,o.createElement)(O,{...t,id:"activitypub-remote-reply-link-"+k++,useId:!0}))}))}))},20:(e,t,r)=>{var o=r(609),a=Symbol.for("react.element"),n=(Symbol.for("react.fragment"),Object.prototype.hasOwnProperty),i=o.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,l={key:!0,ref:!0,__self:!0,__source:!0};t.jsx=function(e,t,r){var o,c={},s=null,m=null;for(o in void 0!==r&&(s=""+r),void 0!==t.key&&(s=""+t.key),void 0!==t.ref&&(m=t.ref),t)n.call(t,o)&&!l.hasOwnProperty(o)&&(c[o]=t[o]);if(e&&e.defaultProps)for(o in t=e.defaultProps)void 0===c[o]&&(c[o]=t[o]);return{$$typeof:a,type:e,key:s,ref:m,props:c,_owner:i.current}}},848:(e,t,r)=>{e.exports=r(20)},609:e=>{e.exports=window.React}},r={};function o(e){var a=r[e];if(void 0!==a)return a.exports;var n=r[e]={exports:{}};return t[e](n,n.exports,o),n.exports}o.m=t,e=[],o.O=(t,r,a,n)=>{if(!r){var i=1/0;for(m=0;m@yourusername@example.com
)","activitypub"),{code:(0,o.createElement)("code",null)})),(0,o.createElement)("div",{className:"activitypub-dialog__button-group"},(0,o.createElement)("input",{type:"text",value:L,onKeyDown:e=>{"Enter"===e?.code&&j()},onChange:e=>S(e.target.value)}),(0,o.createElement)(l.Button,{onClick:j},(0,o.createElement)(s,{icon:f}),C)),m&&(0,o.createElement)("div",{className:"activitypub-dialog__remember"},(0,o.createElement)(l.CheckboxControl,{checked:U,label:(0,c.__)("Remember me for easier comments","activitypub"),onChange:()=>{N(!U)}}))))}const{namespace:C}=window._activityPubOptions;function R({selectedComment:e,commentId:t}){const r=(0,c.__)("Reply","activitypub"),a=`/${C}/comments/${t}/remote-reply?resource=`,n=(0,c.__)("Copy and paste the Comment URL into the search field of your favorite fediverse app or server.","activitypub");return(0,o.createElement)(E,{actionText:r,copyDescription:n,handle:e,resourceUrl:a,myProfile:(0,c.__)("Original Comment URL","activitypub"),rememberProfile:!0})}function x({profileURL:e,template:t,commentURL:r,deleteRemoteUser:a}){return(0,o.createElement)(o.Fragment,null,(0,o.createElement)(l.Button,{variant:"link",className:"comment-reply-link activitypub-remote-reply__button",onClick:()=>{const e=t.replace("{uri}",r);window.open(e,"_blank")}},(0,c.sprintf)((0,c.__)("Reply as %s","activitypub"),e)),(0,o.createElement)(l.Button,{className:"activitypub-remote-profile-delete",onClick:a,title:(0,c.__)("Delete Remote Profile","activitypub")},(0,o.createElement)(s,{icon:u,size:18})))}function O({selectedComment:e,commentId:t}){const[r,n]=(0,a.useState)(!1),i=(0,c.__)("Remote Reply","activitypub"),{profileURL:s,template:m,deleteRemoteUser:p}=h(),u=s&&m;return(0,o.createElement)(o.Fragment,null,u?(0,o.createElement)(x,{profileURL:s,template:m,commentURL:e,deleteRemoteUser:p}):(0,o.createElement)(l.Button,{variant:"link",className:"comment-reply-link activitypub-remote-reply__button",onClick:()=>n(!0)},(0,c.__)("Reply on the Fediverse","activitypub")),r&&(0,o.createElement)(l.Modal,{className:"activitypub-remote-reply__modal activitypub__modal",onRequestClose:()=>n(!1),title:i},(0,o.createElement)(R,{selectedComment:e,commentId:t})))}let k=1;i()((()=>{[].forEach.call(document.querySelectorAll(".activitypub-remote-reply"),(e=>{const t=JSON.parse(e.dataset.attrs);(0,a.createRoot)(e).render((0,o.createElement)(O,{...t,id:"activitypub-remote-reply-link-"+k++,useId:!0}))}))}))},20:(e,t,r)=>{var o=r(609),a=Symbol.for("react.element"),n=(Symbol.for("react.fragment"),Object.prototype.hasOwnProperty),i=o.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,l={key:!0,ref:!0,__self:!0,__source:!0};t.jsx=function(e,t,r){var o,c={},s=null,m=null;for(o in void 0!==r&&(s=""+r),void 0!==t.key&&(s=""+t.key),void 0!==t.ref&&(m=t.ref),t)n.call(t,o)&&!l.hasOwnProperty(o)&&(c[o]=t[o]);if(e&&e.defaultProps)for(o in t=e.defaultProps)void 0===c[o]&&(c[o]=t[o]);return{$$typeof:a,type:e,key:s,ref:m,props:c,_owner:i.current}}},848:(e,t,r)=>{e.exports=r(20)},609:e=>{e.exports=window.React}},r={};function o(e){var a=r[e];if(void 0!==a)return a.exports;var n=r[e]={exports:{}};return t[e](n,n.exports,o),n.exports}o.m=t,e=[],o.O=(t,r,a,n)=>{if(!r){var i=1/0;for(m=0;m', + esc_url( $attrs['url'] ), + esc_attr__( 'This post is a response to the referenced content.', 'activitypub' ), + // translators: %s is the URL of the post being replied to. + sprintf( __( '↬%s', 'activitypub' ), \str_replace( array( 'https://', 'http://' ), '', $attrs['url'] ) ) + ), + $attrs + ); + } + + /** + * Render a follower. + * + * @param \Activitypub\Model\Follower $follower The follower to render. + * + * @return string The HTML to render. + */ public static function render_follower( $follower ) { $external_svg = ''; - $template = + $template = ' diff --git a/wp-content/plugins/activitypub/includes/class-cli.php b/wp-content/plugins/activitypub/includes/class-cli.php new file mode 100644 index 00000000..64b4b8ab --- /dev/null +++ b/wp-content/plugins/activitypub/includes/class-cli.php @@ -0,0 +1,201 @@ + $value ) { + WP_CLI::line( $key . ': ' . $value ); + } + } + + /** + * Remove the entire blog from the Fediverse. + * + * ## EXAMPLES + * + * $ wp activitypub self-destruct + * + * @param array|null $args The arguments. + * @param array|null $assoc_args The associative arguments. + * + * @return void + */ + public function self_destruct( $args, $assoc_args ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + WP_CLI::warning( __( 'Self-Destructing is not implemented yet.', 'activitypub' ) ); + } + + /** + * Delete or Update a Post, Page, Custom Post Type or Attachment. + * + * ## OPTIONS + * + *
%s
was replaced, this is often done by plugins.',
'activitypub'
@@ -186,11 +192,11 @@ class Health_Check {
);
}
- // try to access author URL
+ // Try to access author URL.
$response = \wp_remote_get(
$author_url,
array(
- 'headers' => array( 'Accept' => 'application/activity+json' ),
+ 'headers' => array( 'Accept' => 'application/activity+json' ),
'redirection' => 0,
)
);
@@ -199,7 +205,7 @@ class Health_Check {
return new WP_Error(
'author_url_not_accessible',
\sprintf(
- // translators: %s: Author URL
+ // translators: %s: Author URL.
\__(
'Your author URL %s
is not accessible. Please check your WordPress setup or permalink structure. If the setup seems fine, maybe check if a plugin might restrict the access.',
'activitypub'
@@ -211,12 +217,12 @@ class Health_Check {
$response_code = \wp_remote_retrieve_response_code( $response );
- // check for redirects
+ // Check for redirects.
if ( \in_array( $response_code, array( 301, 302, 307, 308 ), true ) ) {
return new WP_Error(
'author_url_not_accessible',
\sprintf(
- // translators: %s: Author URL
+ // translators: %s: Author URL.
\__(
'Your author URL %s
is redirecting to another page, this is often done by SEO plugins like "Yoast SEO".',
'activitypub'
@@ -226,14 +232,14 @@ class Health_Check {
);
}
- // check if response is JSON
+ // Check if response is JSON.
$body = \wp_remote_retrieve_body( $response );
if ( ! \is_string( $body ) || ! \is_array( \json_decode( $body, true ) ) ) {
return new WP_Error(
'author_url_not_accessible',
\sprintf(
- // translators: %s: Author URL
+ // translators: %s: Author URL.
\__(
'Your author URL %s
does not return valid JSON for application/activity+json
. Please check if your hosting supports alternate Accept
headers.',
'activitypub'
@@ -252,14 +258,15 @@ class Health_Check {
* @return boolean|WP_Error
*/
public static function is_webfinger_endpoint_accessible() {
- $user = Users::get_by_id( Users::APPLICATION_USER_ID );
+ $user = Users::get_by_id( Users::APPLICATION_USER_ID );
$resource = $user->get_webfinger();
$url = Webfinger::resolve( $resource );
if ( \is_wp_error( $url ) ) {
$allowed = array( 'code' => array() );
+
$not_accessible = wp_kses(
- // translators: %s: Author URL
+ // translators: %s: Author URL.
\__(
'Your WebFinger endpoint %s
is not accessible. Please check your WordPress setup or permalink structure.',
'activitypub'
@@ -267,7 +274,7 @@ class Health_Check {
$allowed
);
$invalid_response = wp_kses(
- // translators: %s: Author URL
+ // translators: %s: Author URL.
\__(
'Your WebFinger endpoint %s
does not return valid JSON for application/jrd+json
.',
'activitypub'
@@ -276,20 +283,21 @@ class Health_Check {
);
$health_messages = array(
- 'webfinger_url_not_accessible' => \sprintf(
+ 'webfinger_url_not_accessible' => \sprintf(
$not_accessible,
- $url->get_error_data()
+ $url->get_error_data()['data']
),
'webfinger_url_invalid_response' => \sprintf(
- // translators: %s: Author URL
+ // translators: %s: Author URL.
$invalid_response,
- $url->get_error_data()
+ $url->get_error_data()['data']
),
);
- $message = null;
+ $message = null;
if ( isset( $health_messages[ $url->get_error_code() ] ) ) {
$message = $health_messages[ $url->get_error_code() ];
}
+
return new WP_Error(
$url->get_error_code(),
$message,
@@ -303,7 +311,7 @@ class Health_Check {
/**
* Retrieve the URL to the author page for the user with the ID provided.
*
- * @global WP_Rewrite $wp_rewrite WordPress rewrite component.
+ * @global \WP_Rewrite $wp_rewrite WordPress rewrite component.
*
* @param int $author_id Author ID.
* @param string $author_nicename Optional. The author's nicename (slug). Default empty.
@@ -312,8 +320,9 @@ class Health_Check {
*/
public static function get_author_posts_url( $author_id, $author_nicename = '' ) {
global $wp_rewrite;
+
$auth_id = (int) $author_id;
- $link = $wp_rewrite->get_author_permastruct();
+ $link = $wp_rewrite->get_author_permastruct();
if ( empty( $link ) ) {
$file = home_url( '/' );
@@ -342,12 +351,12 @@ class Health_Check {
$info['activitypub'] = array(
'label' => __( 'ActivityPub', 'activitypub' ),
'fields' => array(
- 'webfinger' => array(
+ 'webfinger' => array(
'label' => __( 'WebFinger Resource', 'activitypub' ),
'value' => Webfinger::get_user_resource( wp_get_current_user()->ID ),
'private' => true,
),
- 'author_url' => array(
+ 'author_url' => array(
'label' => __( 'Author URL', 'activitypub' ),
'value' => get_author_posts_url( wp_get_current_user()->ID ),
'private' => true,
diff --git a/wp-content/plugins/activitypub/includes/class-http.php b/wp-content/plugins/activitypub/includes/class-http.php
index 2a8ce7d0..133a3469 100644
--- a/wp-content/plugins/activitypub/includes/class-http.php
+++ b/wp-content/plugins/activitypub/includes/class-http.php
@@ -1,11 +1,15 @@
100,
+ $args = array(
+ 'timeout' => 100,
'limit_response_size' => 1048576,
- 'redirection' => 3,
- 'user-agent' => "$user_agent; ActivityPub",
- 'headers' => array(
- 'Accept' => 'application/activity+json',
+ 'redirection' => 3,
+ 'user-agent' => "$user_agent; ActivityPub",
+ 'headers' => array(
+ 'Accept' => 'application/activity+json',
'Content-Type' => 'application/activity+json',
- 'Digest' => $digest,
- 'Signature' => $signature,
- 'Date' => $date,
+ 'Digest' => $digest,
+ 'Signature' => $signature,
+ 'Date' => $date,
),
- 'body' => $body,
+ 'body' => $body,
);
$response = \wp_safe_remote_post( $url, $args );
@@ -58,18 +62,26 @@ class Http {
$response = new WP_Error( $code, __( 'Failed HTTP Request', 'activitypub' ), array( 'status' => $code ) );
}
+ /**
+ * Action to save the response of the remote POST request.
+ *
+ * @param array|WP_Error $response The response of the remote POST request.
+ * @param string $url The URL endpoint.
+ * @param string $body The Post Body.
+ * @param int $user_id The WordPress User-ID.
+ */
\do_action( 'activitypub_safe_remote_post_response', $response, $url, $body, $user_id );
return $response;
}
/**
- * Send a GET Request with the needed HTTP Headers
+ * Send a GET Request with the needed HTTP Headers.
*
- * @param string $url The URL endpoint
- * @param bool|int $cached If the result should be cached, or its duration. Default: 1hr.
+ * @param string $url The URL endpoint.
+ * @param bool|int $cached Optional. Whether the result should be cached, or its duration. Default false.
*
- * @return array|WP_Error The GET Response or an WP_ERROR
+ * @return array|WP_Error The GET Response or a WP_Error.
*/
public static function get( $url, $cached = false ) {
\do_action( 'activitypub_pre_http_get', $url );
@@ -80,13 +92,19 @@ class Http {
$response = \get_transient( $transient_key );
if ( $response ) {
+ /**
+ * Action to save the response of the remote GET request.
+ *
+ * @param array|WP_Error $response The response of the remote GET request.
+ * @param string $url The URL endpoint.
+ */
\do_action( 'activitypub_safe_remote_get_response', $response, $url );
return $response;
}
}
- $date = \gmdate( 'D, d M Y H:i:s T' );
+ $date = \gmdate( 'D, d M Y H:i:s T' );
$signature = Signature::generate_signature( Users::APPLICATION_USER_ID, 'get', $url, $date );
$wp_version = get_masked_wp_version();
@@ -99,15 +117,15 @@ class Http {
$user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . \get_bloginfo( 'url' ) );
$args = array(
- 'timeout' => apply_filters( 'activitypub_remote_get_timeout', 100 ),
+ 'timeout' => apply_filters( 'activitypub_remote_get_timeout', 100 ),
'limit_response_size' => 1048576,
- 'redirection' => 3,
- 'user-agent' => "$user_agent; ActivityPub",
- 'headers' => array(
- 'Accept' => 'application/activity+json',
+ 'redirection' => 3,
+ 'user-agent' => "$user_agent; ActivityPub",
+ 'headers' => array(
+ 'Accept' => 'application/activity+json',
'Content-Type' => 'application/activity+json',
- 'Signature' => $signature,
- 'Date' => $date,
+ 'Signature' => $signature,
+ 'Date' => $date,
),
);
@@ -118,6 +136,12 @@ class Http {
$response = new WP_Error( $code, __( 'Failed HTTP Request', 'activitypub' ), array( 'status' => $code ) );
}
+ /**
+ * Action to save the response of the remote GET request.
+ *
+ * @param array|WP_Error $response The response of the remote GET request.
+ * @param string $url The URL endpoint.
+ */
\do_action( 'activitypub_safe_remote_get_response', $response, $url );
if ( $cached ) {
@@ -139,6 +163,11 @@ class Http {
* @return bool True if the URL is a tombstone.
*/
public static function is_tombstone( $url ) {
+ /**
+ * Action before checking if the URL is a tombstone.
+ *
+ * @param string $url The URL to check.
+ */
\do_action( 'activitypub_pre_http_is_tombstone', $url );
$response = \wp_safe_remote_get( $url );
@@ -151,15 +180,22 @@ class Http {
return false;
}
+ /**
+ * Generate a cache key for the URL.
+ *
+ * @param string $url The URL to generate the cache key for.
+ *
+ * @return string The cache key.
+ */
public static function generate_cache_key( $url ) {
return 'activitypub_http_' . \md5( $url );
}
/**
- * Requests the Data from the Object-URL or Object-Array
+ * 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.
+ * @param bool $cached Optional. Whether the result should be cached. Default true.
*
* @return array|WP_Error The Object data as array or WP_Error on failure.
*/
@@ -204,7 +240,7 @@ class Http {
$transient_key = self::generate_cache_key( $url );
- // only check the cache if needed.
+ // Only check the cache if needed.
if ( $cached ) {
$data = \get_transient( $transient_key );
diff --git a/wp-content/plugins/activitypub/includes/class-link.php b/wp-content/plugins/activitypub/includes/class-link.php
new file mode 100644
index 00000000..3c53f3bb
--- /dev/null
+++ b/wp-content/plugins/activitypub/includes/class-link.php
@@ -0,0 +1,127 @@
+%s%s%s' . $content . '
'; + } + + /** + * Checks if the user is the blog user. + * + * @param int $user_id The user ID. + * @return bool True if the user is the blog user, otherwise false. + */ + private static function is_blog( $user_id ) { + return Users::BLOG_USER_ID === $user_id; + } +} diff --git a/wp-content/plugins/activitypub/includes/collection/class-followers.php b/wp-content/plugins/activitypub/includes/collection/class-followers.php index b51224fd..76611ff9 100644 --- a/wp-content/plugins/activitypub/includes/collection/class-followers.php +++ b/wp-content/plugins/activitypub/includes/collection/class-followers.php @@ -1,32 +1,36 @@ get_var( $wpdb->prepare( "SELECT DISTINCT p.ID FROM $wpdb->posts p INNER JOIN $wpdb->postmeta pm ON p.ID = pm.post_id WHERE p.post_type = %s AND pm.meta_key = 'activitypub_user_id' AND pm.meta_value = %d AND p.guid = %s", @@ -111,16 +115,16 @@ class Followers { } /** - * Get a Follower by Actor indepenent from the User. + * Get a Follower by Actor independent of the User. * * @param string $actor The Actor URL. * - * @return \Activitypub\Model\Follower|null The Follower object or null + * @return \Activitypub\Activity\Base_Object|WP_Error|null */ public static function get_follower_by_actor( $actor ) { global $wpdb; - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + // phpcs:ignore WordPress.DB.DirectDatabaseQuery $post_id = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE guid=%s", @@ -137,12 +141,12 @@ class Followers { } /** - * Get the Followers of a given user + * Get the Followers of a given user. * - * @param int $user_id The ID of the WordPress User. - * @param int $number Maximum number of results to return. - * @param int $page Page number. - * @param array $args The WP_Query arguments. + * @param int $user_id The ID of the WordPress User. + * @param int $number Maximum number of results to return. + * @param int $page Page number. + * @param array $args The WP_Query arguments. * @return array List of `Follower` objects. */ public static function get_followers( $user_id, $number = -1, $page = null, $args = array() ) { @@ -153,14 +157,17 @@ class Followers { /** * Get the Followers of a given user, along with a total count for pagination purposes. * - * @param int $user_id The ID of the WordPress User. - * @param int $number Maximum number of results to return. - * @param int $page Page number. - * @param array $args The WP_Query arguments. + * @param int $user_id The ID of the WordPress User. + * @param int $number Maximum number of results to return. + * @param int $page Page number. + * @param array $args The WP_Query arguments. * - * @return array - * followers List of `Follower` objects. - * total Total number of followers. + * @return array { + * Data about the followers. + * + * @type array $followers List of `Follower` objects. + * @type int $total Total number of followers. + * } */ public static function get_followers_with_count( $user_id, $number = -1, $page = null, $args = array() ) { $defaults = array( @@ -178,22 +185,21 @@ class Followers { ), ); - $args = wp_parse_args( $args, $defaults ); - $query = new WP_Query( $args ); - $total = $query->found_posts; + $args = wp_parse_args( $args, $defaults ); + $query = new WP_Query( $args ); + $total = $query->found_posts; $followers = array_map( function ( $post ) { return Follower::init_from_cpt( $post ); }, $query->get_posts() ); + return compact( 'followers', 'total' ); } /** - * Get all Followers - * - * @param array $args The WP_Query arguments. + * Get all Followers. * * @return array The Term list of Followers. */ @@ -219,7 +225,7 @@ class Followers { /** * Count the total number of followers * - * @param int $user_id The ID of the WordPress User + * @param int $user_id The ID of the WordPress User. * * @return int The number of Followers */ @@ -251,21 +257,21 @@ class Followers { } /** - * Returns all Inboxes fo a Users Followers + * Returns all Inboxes for a Users Followers. * - * @param int $user_id The ID of the WordPress User + * @param int $user_id The ID of the WordPress User. * - * @return array The list of Inboxes + * @return array The list of Inboxes. */ public static function get_inboxes( $user_id ) { $cache_key = sprintf( self::CACHE_KEY_INBOXES, $user_id ); - $inboxes = wp_cache_get( $cache_key, 'activitypub' ); + $inboxes = wp_cache_get( $cache_key, 'activitypub' ); if ( $inboxes ) { return $inboxes; } - // get all Followers of a ID of the WordPress User + // Get all Followers of a ID of the WordPress User. $posts = new WP_Query( array( 'nopaging' => true, @@ -316,13 +322,12 @@ class Followers { } /** - * Get all Followers that have not been updated for a given time + * Get all Followers that have not been updated for a given time. * - * @param enum $output The output format, supported ARRAY_N, OBJECT and ACTIVITYPUB_OBJECT. - * @param int $number Limits the result. - * @param int $older_than The time in seconds. + * @param int $number Optional. Limits the result. Default 50. + * @param int $older_than Optional. The time in seconds. Default 86400 (1 day). * - * @return mixed The Term list of Followers, the format depends on $output. + * @return array The Term list of Followers. */ public static function get_outdated_followers( $number = 50, $older_than = 86400 ) { $args = array( @@ -330,7 +335,7 @@ class Followers { 'posts_per_page' => $number, 'orderby' => 'modified', 'order' => 'ASC', - 'post_status' => 'any', // 'any' includes 'trash + 'post_status' => 'any', // 'any' includes 'trash'. 'date_query' => array( array( 'column' => 'post_modified_gmt', @@ -343,19 +348,18 @@ class Followers { $items = array(); foreach ( $posts->get_posts() as $follower ) { - $items[] = Follower::init_from_cpt( $follower ); // phpcs:ignore + $items[] = Follower::init_from_cpt( $follower ); } return $items; } /** - * Get all Followers that had errors + * Get all Followers that had errors. * - * @param enum $output The output format, supported ARRAY_N, OBJECT and ACTIVITYPUB_OBJECT - * @param integer $number The number of Followers to return. + * @param int $number Optional. The number of Followers to return. Default 20. * - * @return mixed The Term list of Followers, the format depends on $output. + * @return array The Term list of Followers. */ public static function get_faulty_followers( $number = 20 ) { $args = array( @@ -393,7 +397,7 @@ class Followers { $items = array(); foreach ( $posts->get_posts() as $follower ) { - $items[] = Follower::init_from_cpt( $follower ); // phpcs:ignore + $items[] = Follower::init_from_cpt( $follower ); } return $items; @@ -403,8 +407,7 @@ class Followers { * This function is used to store errors that occur when * sending an ActivityPub message to a Follower. * - * The error will be stored in the - * post meta. + * The error will be stored in post meta. * * @param int $post_id The ID of the WordPress Custom-Post-Type. * @param mixed $error The error message. Can be a string or a WP_Error. diff --git a/wp-content/plugins/activitypub/includes/collection/class-interactions.php b/wp-content/plugins/activitypub/includes/collection/class-interactions.php index c29cf259..36262e86 100644 --- a/wp-content/plugins/activitypub/includes/collection/class-interactions.php +++ b/wp-content/plugins/activitypub/includes/collection/class-interactions.php @@ -1,8 +1,14 @@ comment_post_ID; } - // not a reply to a post or comment + // Not a reply to a post or comment. if ( ! $comment_post_id ) { return false; } - $actor = object_to_uri( $activity['actor'] ); - $meta = get_remote_metadata_by_actor( $actor ); + $commentdata['comment_post_ID'] = $comment_post_id; + $commentdata['comment_parent'] = $parent_comment_id ? $parent_comment_id : 0; - if ( ! $meta || \is_wp_error( $meta ) ) { - return false; - } - - $url = object_to_uri( $meta['url'] ); - - $commentdata = array( - 'comment_post_ID' => $comment_post_id, - 'comment_author' => isset( $meta['name'] ) ? \esc_attr( $meta['name'] ) : \esc_attr( $meta['preferredUsername'] ), - 'comment_author_url' => \esc_url_raw( $url ), - 'comment_content' => \addslashes( $activity['object']['content'] ), - 'comment_type' => 'comment', - 'comment_author_email' => '', - 'comment_parent' => $parent_comment_id ? $parent_comment_id : 0, - 'comment_meta' => array( - 'source_id' => \esc_url_raw( $activity['object']['id'] ), - 'protocol' => 'activitypub', - ), - ); - - if ( isset( $meta['icon']['url'] ) ) { - $commentdata['comment_meta']['avatar_url'] = \esc_url_raw( $meta['icon']['url'] ); - } - - if ( isset( $activity['object']['url'] ) ) { - $commentdata['comment_meta']['source_url'] = \esc_url_raw( object_to_uri( $activity['object']['url'] ) ); - } - - // disable flood control - \remove_action( 'check_comment_flood', 'check_comment_flood_db', 10 ); - // do not require email for AP entries - \add_filter( 'pre_option_require_name_email', '__return_false' ); - // No nonce possible for this submission route - \add_filter( - 'akismet_comment_nonce', - function () { - return 'inactive'; - } - ); - \add_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ), 10, 2 ); - - $comment = \wp_new_comment( $commentdata, true ); - - \remove_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ), 10 ); - \remove_filter( 'pre_option_require_name_email', '__return_false' ); - // re-add flood control - \add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 ); - - return $comment; + return self::persist( $commentdata, self::INSERT ); } /** - * Update a comment + * Update a comment. * - * @param array $activity The activity-object + * @param array $activity The activity object. * - * @return array|string|int|\WP_Error|false The commentdata or false on failure + * @return array|string|int|\WP_Error|false The comment data or false on failure. */ public static function update_comment( $activity ) { $meta = get_remote_metadata_by_actor( $activity['actor'] ); - //Determine comment_ID + // Determine comment_ID. $comment = object_id_to_comment( \esc_url_raw( $activity['object']['id'] ) ); $commentdata = \get_comment( $comment, ARRAY_A ); @@ -119,44 +75,63 @@ class Interactions { return false; } - //found a local comment id - $commentdata['comment_author'] = \esc_attr( $meta['name'] ? $meta['name'] : $meta['preferredUsername'] ); + // Found a local comment id. + $commentdata['comment_author'] = \esc_attr( $meta['name'] ? $meta['name'] : $meta['preferredUsername'] ); $commentdata['comment_content'] = \addslashes( $activity['object']['content'] ); - if ( isset( $meta['icon']['url'] ) ) { - $commentdata['comment_meta']['avatar_url'] = \esc_url_raw( $meta['icon']['url'] ); + + return self::persist( $commentdata, self::UPDATE ); + } + + /** + * Adds an incoming Like, Announce, ... as a comment to a post. + * + * @param array $activity Activity array. + * + * @return array|false Comment data or `false` on failure. + */ + public static function add_reaction( $activity ) { + $commentdata = self::activity_to_comment( $activity ); + + if ( ! $commentdata ) { + return false; } - // disable flood control - \remove_action( 'check_comment_flood', 'check_comment_flood_db', 10 ); - // do not require email for AP entries - \add_filter( 'pre_option_require_name_email', '__return_false' ); - // No nonce possible for this submission route - \add_filter( - 'akismet_comment_nonce', - function () { - return 'inactive'; - } - ); - \add_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ), 10, 2 ); + $url = object_to_uri( $activity['object'] ); + $comment_post_id = url_to_postid( $url ); + $parent_comment_id = url_to_commentid( $url ); - $state = \wp_update_comment( $commentdata, true ); - - \remove_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ), 10 ); - \remove_filter( 'pre_option_require_name_email', '__return_false' ); - // re-add flood control - \add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 ); - - if ( 1 === $state ) { - return $commentdata; - } else { - return $state; // Either `false` or a `WP_Error` instance or `0` or `1`! + if ( ! $comment_post_id && $parent_comment_id ) { + $parent_comment = get_comment( $parent_comment_id ); + $comment_post_id = $parent_comment->comment_post_ID; } + + if ( ! $comment_post_id ) { + // Not a reply to a post or comment. + return false; + } + + $type = $activity['type']; + + if ( ! Comment::is_registered_comment_type( $type ) ) { + // Not a valid comment type. + return false; + } + + $comment_type = Comment::get_comment_type( $type ); + $comment_content = $comment_type['excerpt']; + + $commentdata['comment_post_ID'] = $comment_post_id; + $commentdata['comment_content'] = \esc_html( $comment_content ); + $commentdata['comment_type'] = \esc_attr( $comment_type['type'] ); + $commentdata['comment_meta']['source_id'] = \esc_url_raw( $activity['id'] ); + + return self::persist( $commentdata, self::INSERT ); } /** * Get interaction(s) for a given URL/ID. * - * @param strin $url The URL/ID to get interactions for. + * @param string $url The URL/ID to get interactions for. * * @return array The interactions as WP_Comment objects. */ @@ -198,19 +173,19 @@ class Interactions { public static function get_interactions_by_actor( $actor ) { $meta = get_remote_metadata_by_actor( $actor ); - // get URL, because $actor seems to be the ID + // Get URL, because $actor seems to be the ID. if ( $meta && ! is_wp_error( $meta ) && isset( $meta['url'] ) ) { $actor = object_to_uri( $meta['url'] ); } - $args = array( + $args = array( 'nopaging' => true, 'author_url' => $actor, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'meta_query' => array( array( - 'key' => 'protocol', - 'value' => 'activitypub', + 'key' => 'protocol', + 'value' => 'activitypub', 'compare' => '=', ), ), @@ -223,7 +198,7 @@ class Interactions { * Adds line breaks to the list of allowed comment tags. * * @param array $allowed_tags Allowed HTML tags. - * @param string $context Context. + * @param string $context Optional. Context. Default empty. * * @return array Filtered tag list. */ @@ -244,4 +219,103 @@ class Interactions { return $allowed_tags; } + + /** + * Convert an Activity to a WP_Comment + * + * @param array $activity The Activity array. + * + * @return array|false The comment data or false on failure. + */ + public static function activity_to_comment( $activity ) { + $comment_content = null; + $actor = object_to_uri( $activity['actor'] ); + $actor = get_remote_metadata_by_actor( $actor ); + + // Check Actor-Meta. + if ( ! $actor || is_wp_error( $actor ) ) { + return false; + } + + // Check Actor-Name. + if ( isset( $actor['name'] ) ) { + $comment_author = $actor['name']; + } elseif ( isset( $actor['preferredUsername'] ) ) { + $comment_author = $actor['preferredUsername']; + } else { + return false; + } + + $url = object_to_uri( $actor['url'] ); + + if ( ! $url ) { + object_to_uri( $actor['id'] ); + } + + if ( isset( $activity['object']['content'] ) ) { + $comment_content = \addslashes( $activity['object']['content'] ); + } + + $commentdata = array( + 'comment_author' => \esc_attr( $comment_author ), + 'comment_author_url' => \esc_url_raw( $url ), + 'comment_content' => $comment_content, + 'comment_type' => 'comment', + 'comment_author_email' => '', + 'comment_meta' => array( + 'source_id' => \esc_url_raw( object_to_uri( $activity['object'] ) ), + 'protocol' => 'activitypub', + ), + ); + + if ( isset( $actor['icon']['url'] ) ) { + $commentdata['comment_meta']['avatar_url'] = \esc_url_raw( $actor['icon']['url'] ); + } + + if ( isset( $activity['object']['url'] ) ) { + $commentdata['comment_meta']['source_url'] = \esc_url_raw( object_to_uri( $activity['object']['url'] ) ); + } + + return $commentdata; + } + + /** + * Persist a comment. + * + * @param array $commentdata The commentdata array. + * @param string $action Optional. Either 'insert' or 'update'. Default 'insert'. + * + * @return array|string|int|\WP_Error|false The comment data or false on failure + */ + public static function persist( $commentdata, $action = self::INSERT ) { + // Disable flood control. + \remove_action( 'check_comment_flood', 'check_comment_flood_db', 10 ); + // Do not require email for AP entries. + \add_filter( 'pre_option_require_name_email', '__return_false' ); + // No nonce possible for this submission route. + \add_filter( + 'akismet_comment_nonce', + function () { + return 'inactive'; + } + ); + \add_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ), 10, 2 ); + + if ( self::INSERT === $action ) { + $state = \wp_new_comment( $commentdata, true ); + } else { + $state = \wp_update_comment( $commentdata, true ); + } + + \remove_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ), 10 ); + \remove_filter( 'pre_option_require_name_email', '__return_false' ); + // Restore flood control. + \add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 ); + + if ( 1 === $state ) { + return $commentdata; + } else { + return $state; // Either WP_Comment, false, a WP_Error, 0, or 1! + } + } } diff --git a/wp-content/plugins/activitypub/includes/collection/class-replies.php b/wp-content/plugins/activitypub/includes/collection/class-replies.php new file mode 100644 index 00000000..34a5aa02 --- /dev/null +++ b/wp-content/plugins/activitypub/includes/collection/class-replies.php @@ -0,0 +1,181 @@ + 'approve', + 'orderby' => 'comment_date_gmt', + 'order' => 'ASC', + ); + + if ( $wp_object instanceof WP_Post ) { + $args['parent'] = 0; // TODO: maybe this is unnecessary. + $args['post_id'] = $wp_object->ID; + } elseif ( $wp_object instanceof WP_Comment ) { + $args['parent'] = $wp_object->comment_ID; + } else { + return new WP_Error(); + } + + return $args; + } + + /** + * Adds pagination args comments query. + * + * @param array $args Query args built by self::build_args. + * @param int $page The current pagination page. + * @param int $comments_per_page The number of comments per page. + */ + private static function add_pagination_args( $args, $page, $comments_per_page ) { + $args['number'] = $comments_per_page; + + $offset = intval( $page ) * $comments_per_page; + $args['offset'] = $offset; + + return $args; + } + + + /** + * Get the replies collections ID. + * + * @param WP_Post|WP_Comment $wp_object The post or comment to fetch replies for. + * + * @return string|WP_Error The rest URL of the replies collection or WP_Error if the object is not a post or comment. + */ + private static function get_id( $wp_object ) { + if ( $wp_object instanceof WP_Post ) { + return get_rest_url_by_path( sprintf( 'posts/%d/replies', $wp_object->ID ) ); + } elseif ( $wp_object instanceof WP_Comment ) { + return get_rest_url_by_path( sprintf( 'comments/%d/replies', $wp_object->comment_ID ) ); + } else { + return new WP_Error(); + } + } + + /** + * Get the replies collection. + * + * @param WP_Post|WP_Comment $wp_object The post or comment to fetch replies for. + * + * @return array An associative array containing the replies collection without JSON-LD context. + */ + public static function get_collection( $wp_object ) { + $id = self::get_id( $wp_object ); + + if ( ! $id ) { + return null; + } + + $replies = array( + 'id' => $id, + 'type' => 'Collection', + ); + + $replies['first'] = self::get_collection_page( $wp_object, 0, $replies['id'] ); + + return $replies; + } + + /** + * Get the ActivityPub ID's from a list of comments. + * + * It takes only federated/non-local comments into account, others also do not have an + * ActivityPub ID available. + * + * @param WP_Comment[] $comments The comments to retrieve the ActivityPub ids from. + * + * @return string[] A list of the ActivityPub ID's. + */ + private static function get_reply_ids( $comments ) { + $comment_ids = array(); + // Only add external comments from the fediverse. + // Maybe use the Comment class more and the function is_local_comment etc. + foreach ( $comments as $comment ) { + if ( is_local_comment( $comment ) ) { + continue; + } + + $public_comment_id = Comment::get_source_id( $comment->comment_ID ); + if ( $public_comment_id ) { + $comment_ids[] = $public_comment_id; + } + } + return $comment_ids; + } + + /** + * Returns a replies collection page as an associative array. + * + * @link https://www.w3.org/TR/activitystreams-vocabulary/#dfn-collectionpage + * + * @param WP_Post|WP_Comment $wp_object The post of comment the replies are for. + * @param int $page The current pagination page. + * @param string $part_of The collection id/url the returned CollectionPage belongs to. + * + * @return array A CollectionPage as an associative array. + */ + public static function get_collection_page( $wp_object, $page, $part_of = null ) { + // Build initial arguments for fetching approved comments. + $args = self::build_args( $wp_object ); + + // Retrieve the partOf if not already given. + $part_of = $part_of ?? self::get_id( $wp_object ); + + // If the collection page does not exist. + if ( is_wp_error( $args ) || is_wp_error( $part_of ) ) { + return null; + } + + // Get to total replies count. + $total_replies = \get_comments( array_merge( $args, array( 'count' => true ) ) ); + + // Modify query args to retrieve paginated results. + $comments_per_page = \get_option( 'comments_per_page' ); + + // Fetch internal and external comments for current page. + $comments = get_comments( self::add_pagination_args( $args, $page, $comments_per_page ) ); + + // Get the ActivityPub ID's of the comments, without out local-only comments. + $comment_ids = self::get_reply_ids( $comments ); + + // Build the associative CollectionPage array. + $collection_page = array( + 'id' => \add_query_arg( 'page', $page, $part_of ), + 'type' => 'CollectionPage', + 'partOf' => $part_of, + 'items' => $comment_ids, + ); + + if ( $total_replies / $comments_per_page > $page + 1 ) { + $collection_page['next'] = \add_query_arg( 'page', $page + 1, $part_of ); + } + + return $collection_page; + } +} diff --git a/wp-content/plugins/activitypub/includes/collection/class-users.php b/wp-content/plugins/activitypub/includes/collection/class-users.php index e4596b87..036d210a 100644 --- a/wp-content/plugins/activitypub/includes/collection/class-users.php +++ b/wp-content/plugins/activitypub/includes/collection/class-users.php @@ -1,4 +1,10 @@ false, - 'number' => 1, - 'hide_empty' => true, - 'fields' => 'ID', + 'count_total' => false, + 'number' => 1, + 'hide_empty' => true, + 'fields' => 'ID', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query - 'meta_query' => array( + 'meta_query' => array( 'relation' => 'OR', array( 'key' => 'activitypub_user_identifier', @@ -110,7 +119,7 @@ class Users { $username = str_replace( array( '*', '%' ), '', $username ); - // check for login or nicename. + // Check for login or nicename. $user = new WP_User_Query( array( 'count_total' => false, @@ -136,26 +145,26 @@ class Users { /** * Get the User by resource. * - * @param string $resource The User-Resource. + * @param string $uri The User-Resource. * - * @return \Acitvitypub\Model\User The User. + * @return User|WP_Error The User or WP_Error if user not found. */ - public static function get_by_resource( $resource ) { - $resource = object_to_uri( $resource ); + public static function get_by_resource( $uri ) { + $uri = object_to_uri( $uri ); $scheme = 'acct'; - $match = array(); - // try to extract the scheme and the host - if ( preg_match( '/^([a-zA-Z^:]+):(.*)$/i', $resource, $match ) ) { - // extract the scheme + $match = array(); + // Try to extract the scheme and the host. + if ( preg_match( '/^([a-zA-Z^:]+):(.*)$/i', $uri, $match ) ) { + // Extract the scheme. $scheme = \esc_attr( $match[1] ); } switch ( $scheme ) { - // check for http(s) URIs + // Check for http(s) URIs. case 'http': case 'https': - $resource_path = \wp_parse_url( $resource, PHP_URL_PATH ); + $resource_path = \wp_parse_url( $uri, PHP_URL_PATH ); if ( $resource_path ) { $blog_path = \wp_parse_url( \home_url(), PHP_URL_PATH ); @@ -166,7 +175,7 @@ class Users { $resource_path = \trim( $resource_path, '/' ); - // check for http(s)://blog.example.com/@username + // Check for http(s)://blog.example.com/@username. if ( str_starts_with( $resource_path, '@' ) ) { $identifier = \str_replace( '@', '', $resource_path ); $identifier = \trim( $identifier, '/' ); @@ -175,17 +184,17 @@ class Users { } } - // check for http(s)://blog.example.com/author/username - $user_id = url_to_authorid( $resource ); + // Check for http(s)://blog.example.com/author/username. + $user_id = url_to_authorid( $uri ); if ( $user_id ) { return self::get_by_id( $user_id ); } - // check for http(s)://blog.example.com/ + // Check for http(s)://blog.example.com/. if ( - normalize_url( site_url() ) === normalize_url( $resource ) || - normalize_url( home_url() ) === normalize_url( $resource ) + normalize_url( site_url() ) === normalize_url( $uri ) || + normalize_url( home_url() ) === normalize_url( $uri ) ) { return self::get_by_id( self::BLOG_USER_ID ); } @@ -195,11 +204,11 @@ class Users { \__( 'User not found', 'activitypub' ), array( 'status' => 404 ) ); - // check for acct URIs + // Check for acct URIs. case 'acct': - $resource = \str_replace( 'acct:', '', $resource ); - $identifier = \substr( $resource, 0, \strrpos( $resource, '@' ) ); - $host = normalize_host( \substr( \strrchr( $resource, '@' ), 1 ) ); + $uri = \str_replace( 'acct:', '', $uri ); + $identifier = \substr( $uri, 0, \strrpos( $uri, '@' ) ); + $host = normalize_host( \substr( \strrchr( $uri, '@' ), 1 ) ); $blog_host = normalize_host( \wp_parse_url( \home_url( '/' ), \PHP_URL_HOST ) ); if ( $blog_host !== $host ) { @@ -210,7 +219,7 @@ class Users { ); } - // prepare wildcards https://github.com/mastodon/mastodon/issues/22213 + // Prepare wildcards https://github.com/mastodon/mastodon/issues/22213. if ( in_array( $identifier, array( '_', '*', '' ), true ) ) { return self::get_by_id( self::BLOG_USER_ID ); } @@ -228,9 +237,9 @@ class Users { /** * Get the User by resource. * - * @param string $resource The User-Resource. + * @param string $id The User-Resource. * - * @return \Acitvitypub\Model\User The User. + * @return User|Blog|Application|WP_Error The User or WP_Error if user not found. */ public static function get_by_various( $id ) { $user = null; @@ -238,11 +247,11 @@ class Users { if ( is_numeric( $id ) ) { $user = self::get_by_id( $id ); } elseif ( - // is URL + // Is URL. filter_var( $id, FILTER_VALIDATE_URL ) || - // is acct + // Is acct. str_starts_with( $id, 'acct:' ) || - // is email + // Is email. filter_var( $id, FILTER_VALIDATE_EMAIL ) ) { $user = self::get_by_resource( $id ); diff --git a/wp-content/plugins/activitypub/includes/compat.php b/wp-content/plugins/activitypub/includes/compat.php index 8c274c88..fa8627b7 100644 --- a/wp-content/plugins/activitypub/includes/compat.php +++ b/wp-content/plugins/activitypub/includes/compat.php @@ -1,6 +1,8 @@ $v ) { + foreach ( $input as $k => $v ) { if ( ++$next_key !== $k ) { return false; } diff --git a/wp-content/plugins/activitypub/includes/debug.php b/wp-content/plugins/activitypub/includes/debug.php index d42b2a9a..b289c80d 100644 --- a/wp-content/plugins/activitypub/includes/debug.php +++ b/wp-content/plugins/activitypub/includes/debug.php @@ -1,17 +1,22 @@ 404, 'actor' => $actor ) ); + return new WP_Error( + 'activitypub_no_valid_actor_identifier', + \__( 'The "actor" identifier is not valid', 'activitypub' ), + array( + 'status' => 404, + 'actor' => $actor, + ) + ); } } @@ -69,7 +94,14 @@ function get_remote_metadata_by_actor( $actor, $cached = true ) { } if ( ! $actor ) { - return new WP_Error( 'activitypub_no_valid_actor_identifier', \__( 'The "actor" identifier is not valid', 'activitypub' ), array( 'status' => 404, 'actor' => $actor ) ); + return new WP_Error( + 'activitypub_no_valid_actor_identifier', + \__( 'The "actor" identifier is not valid', 'activitypub' ), + array( + 'status' => 404, + 'actor' => $actor, + ) + ); } if ( is_wp_error( $actor ) ) { @@ -78,7 +110,7 @@ function get_remote_metadata_by_actor( $actor, $cached = true ) { $transient_key = 'activitypub_' . $actor; - // only check the cache if needed. + // Only check the cache if needed. if ( $cached ) { $metadata = \get_transient( $transient_key ); @@ -88,7 +120,14 @@ function get_remote_metadata_by_actor( $actor, $cached = true ) { } if ( ! \wp_http_validate_url( $actor ) ) { - $metadata = new WP_Error( 'activitypub_no_valid_actor_url', \__( 'The "actor" is no valid URL', 'activitypub' ), array( 'status' => 400, 'actor' => $actor ) ); + $metadata = new WP_Error( + 'activitypub_no_valid_actor_url', + \__( 'The "actor" is no valid URL', 'activitypub' ), + array( + 'status' => 400, + 'actor' => $actor, + ) + ); return $metadata; } @@ -102,7 +141,14 @@ function get_remote_metadata_by_actor( $actor, $cached = true ) { $metadata = \json_decode( $metadata, true ); if ( ! $metadata ) { - $metadata = new WP_Error( 'activitypub_invalid_json', \__( 'No valid JSON data', 'activitypub' ), array( 'status' => 400, 'actor' => $actor ) ); + $metadata = new WP_Error( + 'activitypub_invalid_json', + \__( 'No valid JSON data', 'activitypub' ), + array( + 'status' => 400, + 'actor' => $actor, + ) + ); return $metadata; } @@ -114,7 +160,7 @@ function get_remote_metadata_by_actor( $actor, $cached = true ) { /** * Returns the followers of a given user. * - * @param int $user_id The User-ID. + * @param int $user_id The user ID. * * @return array The followers. */ @@ -125,7 +171,7 @@ function get_followers( $user_id ) { /** * Count the number of followers for a given user. * - * @param int $user_id The User-ID. + * @param int $user_id The user ID. * * @return int The number of followers. */ @@ -145,12 +191,12 @@ function count_followers( $user_id ) { function url_to_authorid( $url ) { global $wp_rewrite; - // check if url hase the same host + // Check if url hase the same host. if ( \wp_parse_url( \home_url(), \PHP_URL_HOST ) !== \wp_parse_url( $url, \PHP_URL_HOST ) ) { return 0; } - // first, check to see if there is a 'author=N' to match against + // First, check to see if there is a 'author=N' to match against. if ( \preg_match( '/[?&]author=(\d+)/i', $url, $values ) ) { $id = \absint( $values[1] ); if ( $id ) { @@ -158,19 +204,19 @@ function url_to_authorid( $url ) { } } - // check to see if we are using rewrite rules + // Check to see if we are using rewrite rules. $rewrite = $wp_rewrite->wp_rewrite_rules(); - // not using rewrite rules, and 'author=N' method failed, so we're out of options + // Not using rewrite rules, and 'author=N' method failed, so we're out of options. if ( empty( $rewrite ) ) { return 0; } - // generate rewrite rule for the author url + // Generate rewrite rule for the author url. $author_rewrite = $wp_rewrite->get_author_permastruct(); - $author_regexp = \str_replace( '%author%', '', $author_rewrite ); + $author_regexp = \str_replace( '%author%', '', $author_rewrite ); - // match the rewrite rule with the passed url + // Match the rewrite rule with the passed url. if ( \preg_match( '/https?:\/\/(.+)' . \preg_quote( $author_regexp, '/' ) . '([^\/]+)/i', $url, $match ) ) { $user = \get_user_by( 'slug', $match[2] ); if ( $user ) { @@ -182,10 +228,9 @@ function url_to_authorid( $url ) { } /** - * Verify if url is a wp_ap_comment, - * Or if it is a previously received remote comment + * Verify that url is a wp_ap_comment or a previously received remote comment. * - * @return int comment_id + * @return int|bool Comment ID or false if not found. */ function is_comment() { $comment_id = get_query_var( 'c', null ); @@ -193,8 +238,7 @@ function is_comment() { if ( ! is_null( $comment_id ) ) { $comment = \get_comment( $comment_id ); - // Only return local origin comments - if ( $comment && $comment->user_id ) { + if ( $comment ) { return $comment_id; } } @@ -203,13 +247,13 @@ function is_comment() { } /** - * Check for Tombstone Objects + * Check for Tombstone Objects. * * @see https://www.w3.org/TR/activitypub/#delete-activity-outbox * - * @param WP_Error $wp_error A WP_Error-Response of an HTTP-Request + * @param WP_Error $wp_error A WP_Error-Response of an HTTP-Request. * - * @return boolean true if HTTP-Code is 410 or 404 + * @return boolean True if HTTP-Code is 410 or 404. */ function is_tombstone( $wp_error ) { if ( ! is_wp_error( $wp_error ) ) { @@ -226,13 +270,13 @@ function is_tombstone( $wp_error ) { /** * Get the REST URL relative to this plugin's namespace. * - * @param string $path Optional. REST route path. Otherwise this plugin's namespaced root. + * @param string $path Optional. REST route path. Default ''. * * @return string REST URL relative to this plugin's namespace. */ function get_rest_url_by_path( $path = '' ) { - // we'll handle the leading slash. - $path = ltrim( $path, '/' ); + // We'll handle the leading slash. + $path = ltrim( $path, '/' ); $namespaced_path = sprintf( '/%s/%s', ACTIVITYPUB_REST_NAMESPACE, $path ); return \get_rest_url( null, $namespaced_path ); } @@ -240,37 +284,35 @@ function get_rest_url_by_path( $path = '' ) { /** * Convert a string from camelCase to snake_case. * - * @param string $string The string to convert. + * @param string $input The string to convert. * * @return string The converted string. */ -// phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.stringFound -function camel_to_snake_case( $string ) { - return strtolower( preg_replace( '/(?query_vars['activitypub'] ) ) { return true; } @@ -363,7 +403,7 @@ function is_activitypub_request() { /** * This function checks if a user is disabled for ActivityPub. * - * @param int $user_id The User-ID. + * @param int $user_id The user ID. * * @return boolean True if the user is disabled, false otherwise. */ @@ -405,6 +445,12 @@ function is_user_disabled( $user_id ) { break; } + /** + * Allow plugins to disable users for ActivityPub. + * + * @param boolean $return True if the user is disabled, false otherwise. + * @param int $user_id The User-ID. + */ return apply_filters( 'activitypub_is_user_disabled', $return, $user_id ); } @@ -414,7 +460,7 @@ function is_user_disabled( $user_id ) { * This function is used to check if the 'blog' or 'user' * type is disabled for ActivityPub. * - * @param enum $type Can be 'blog' or 'user'. + * @param string $type User type. 'blog' or 'user'. * * @return boolean True if the user type is disabled, false otherwise. */ @@ -461,10 +507,20 @@ function is_user_type_disabled( $type ) { $return = false; break; default: - $return = new WP_Error( 'activitypub_wrong_user_type', __( 'Wrong user type', 'activitypub' ), array( 'status' => 400 ) ); + $return = new WP_Error( + 'activitypub_wrong_user_type', + __( 'Wrong user type', 'activitypub' ), + array( 'status' => 400 ) + ); break; } + /** + * Allow plugins to disable user types for ActivityPub. + * + * @param boolean $return True if the user type is disabled, false otherwise. + * @param string $type The User-Type. + */ return apply_filters( 'activitypub_is_user_type_disabled', $return, $type ); } @@ -519,20 +575,25 @@ function is_json( $data ) { } /** - * Check if a blog is public based on the `blog_public` option + * Check whther a blog is public based on the `blog_public` option. * - * @return bollean True if public, false if not + * @return bool True if public, false if not */ function is_blog_public() { + /** + * Filter whether the blog is public. + * + * @param bool $public Whether the blog is public. + */ return (bool) apply_filters( 'activitypub_is_blog_public', \get_option( 'blog_public', 1 ) ); } /** - * Sanitize a URL + * Sanitize a URL. * - * @param string $value The URL to sanitize + * @param string $value The URL to sanitize. * - * @return string|null The sanitized URL or null if invalid + * @return string|null The sanitized URL or null if invalid. */ function sanitize_url( $value ) { if ( filter_var( $value, FILTER_VALIDATE_URL ) === false ) { @@ -543,11 +604,11 @@ function sanitize_url( $value ) { } /** - * Extract recipient URLs from Activity object + * Extract recipient URLs from Activity object. * - * @param array $data + * @param array $data The Activity object as array. * - * @return array The list of user URLs + * @return array The list of user URLs. */ function extract_recipients_from_activity( $data ) { $recipient_items = array(); @@ -574,10 +635,10 @@ function extract_recipients_from_activity( $data ) { $recipients = array(); - // flatten array + // Flatten array. foreach ( $recipient_items as $recipient ) { if ( is_array( $recipient ) ) { - // check if recipient is an object + // Check if recipient is an object. if ( array_key_exists( 'id', $recipient ) ) { $recipients[] = $recipient['id']; } @@ -590,11 +651,11 @@ function extract_recipients_from_activity( $data ) { } /** - * Check if passed Activity is Public + * Check if passed Activity is Public. * - * @param array $data The Activity object as array + * @param array $data The Activity object as array. * - * @return boolean True if public, false if not + * @return boolean True if public, false if not. */ function is_activity_public( $data ) { $recipients = extract_recipients_from_activity( $data ); @@ -603,53 +664,58 @@ function is_activity_public( $data ) { } /** - * Get active users based on a given duration + * Get active users based on a given duration. * - * @param int $duration The duration to check in month(s) + * @param int $duration Optional. The duration to check in month(s). Default 1. * - * @return int The number of active users + * @return int The number of active users. */ function get_active_users( $duration = 1 ) { - $duration = intval( $duration ); + $duration = intval( $duration ); $transient_key = sprintf( 'monthly_active_users_%d', $duration ); - $count = get_transient( $transient_key ); + $count = get_transient( $transient_key ); if ( false === $count ) { global $wpdb; - $query = "SELECT COUNT( DISTINCT post_author ) FROM {$wpdb->posts} WHERE post_type = 'post' AND post_status = 'publish' AND post_date <= DATE_SUB( NOW(), INTERVAL %d MONTH )"; - $query = $wpdb->prepare( $query, $duration ); - $count = $wpdb->get_var( $query ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery + $count = $wpdb->get_var( + $wpdb->prepare( + "SELECT COUNT( DISTINCT post_author ) FROM {$wpdb->posts} WHERE post_type = 'post' AND post_status = 'publish' AND post_date <= DATE_SUB( NOW(), INTERVAL %d MONTH )", + $duration + ) + ); set_transient( $transient_key, $count, DAY_IN_SECONDS ); } - // if 0 authors where active + // If 0 authors where active. if ( 0 === $count ) { return 0; } - // if single user mode + // If single user mode. if ( is_single_user() ) { return 1; } - // if blog user is disabled + // If blog user is disabled. if ( is_user_disabled( Users::BLOG_USER_ID ) ) { - return $count; + return (int) $count; } - // also count blog user - return $count + 1; + // Also count blog user. + return (int) $count + 1; } /** - * Get the total number of users + * Get the total number of users. * - * @return int The total number of users + * @return int The total number of users. */ function get_total_users() { - // if single user mode + // If single user mode. if ( is_single_user() ) { return 1; } @@ -666,12 +732,12 @@ function get_total_users() { $users = 1; } - // if blog user is disabled + // If blog user is disabled. if ( is_user_disabled( Users::BLOG_USER_ID ) ) { - return $users; + return (int) $users; } - return $users + 1; + return (int) $users + 1; } /** @@ -679,60 +745,66 @@ function get_total_users() { * * @param string $id ActivityPub object ID (usually a URL) to check. * - * @return int|boolean Comment ID, or false on failure. + * @return \WP_Comment|boolean Comment, or false on failure. */ function object_id_to_comment( $id ) { return Comment::object_id_to_comment( $id ); } /** - * Verify if URL is a local comment, - * Or if it is a previously received remote comment + * Verify that URL is a local comment or a previously received remote comment. * (For threading comments locally) * * @param string $url The URL to check. * - * @return int comment_ID or null if not found + * @return string|null Comment ID or null if not found */ function url_to_commentid( $url ) { return Comment::url_to_commentid( $url ); } /** - * Get the URI of an ActivityPub object + * Get the URI of an ActivityPub object. * - * @param array $object The ActivityPub object + * @param array|string $data The ActivityPub object. * * @return string The URI of the ActivityPub object */ -function object_to_uri( $object ) { - // check if it is already simple - if ( ! $object || is_string( $object ) ) { - return $object; +function object_to_uri( $data ) { + // Check whether it is already simple. + if ( ! $data || is_string( $data ) ) { + return $data; } - // check if it is a list, then take first item - // this plugin does not support collections - if ( array_is_list( $object ) ) { - $object = $object[0]; + /* + * Check if it is a list, then take first item. + * This plugin does not support collections. + */ + if ( array_is_list( $data ) ) { + $data = $data[0]; } - // check if it is simplified now - if ( is_string( $object ) ) { - return $object; + // Check if it is simplified now. + if ( is_string( $data ) ) { + return $data; } - // return part of Object that makes most sense - switch ( $object['type'] ) { + $type = 'Object'; + if ( isset( $data['type'] ) ) { + $type = $data['type']; + } + + // Return part of Object that makes most sense. + switch ( $type ) { case 'Link': - $object = $object['href']; + $data = $data['href']; break; default: - $object = $object['id']; + $data = $data['id']; break; } - return $object; + return $data; } /** @@ -794,9 +866,8 @@ function is_local_comment( $comment ) { /** * Mark a WordPress object as federated. * - * @param WP_Comment|WP_Post|mixed $wp_object - * - * @return void + * @param \WP_Comment|\WP_Post $wp_object The WordPress object. + * @param string $state The state of the object. */ function set_wp_object_state( $wp_object, $state ) { $meta_key = 'activitypub_status'; @@ -806,6 +877,12 @@ function set_wp_object_state( $wp_object, $state ) { } elseif ( $wp_object instanceof \WP_Comment ) { \update_comment_meta( $wp_object->comment_ID, $meta_key, $state ); } else { + /** + * Allow plugins to mark WordPress objects as federated. + * + * @param \WP_Comment|\WP_Post $wp_object The WordPress object. + * @param string $state The state of the object. + */ \apply_filters( 'activitypub_mark_wp_object_as_federated', $wp_object ); } } @@ -813,7 +890,7 @@ function set_wp_object_state( $wp_object, $state ) { /** * Get the federation state of a WordPress object. * - * @param WP_Comment|WP_Post|mixed $wp_object + * @param \WP_Comment|\WP_Post $wp_object The WordPress object. * * @return string|false The state of the object or false if not found. */ @@ -825,6 +902,11 @@ function get_wp_object_state( $wp_object ) { } elseif ( $wp_object instanceof \WP_Comment ) { return \get_comment_meta( $wp_object->comment_ID, $meta_key, true ); } else { + /** + * Allow plugins to get the federation state of a WordPress object. + * + * @param \WP_Comment|\WP_Post $wp_object The WordPress object. + */ return \apply_filters( 'activitypub_get_wp_object_state', false, $wp_object ); } } @@ -834,7 +916,7 @@ function get_wp_object_state( $wp_object ) { * * Set some default descriptions for the default post types. * - * @param WP_Post_Type $post_type The post type object. + * @param \WP_Post_Type $post_type The post type object. * * @return string The description of the post type. */ @@ -857,6 +939,12 @@ function get_post_type_description( $post_type ) { } } + /** + * Allow plugins to get the description of a post type. + * + * @param string $description The description of the post type. + * @param \WP_Post_Type $post_type The post type object. + */ return apply_filters( 'activitypub_post_type_description', $description, $post_type->name, $post_type ); } @@ -866,9 +954,9 @@ function get_post_type_description( $post_type ) { * @return string The masked version. */ function get_masked_wp_version() { - // only show the major and minor version + // Only show the major and minor version. $version = get_bloginfo( 'version' ); - // strip the RC or beta part + // Strip the RC or beta part. $version = preg_replace( '/-.*$/', '', $version ); $version = explode( '.', $version ); $version = array_slice( $version, 0, 2 ); @@ -884,7 +972,7 @@ function get_masked_wp_version() { * @return array The enclosures. */ function get_enclosures( $post_id ) { - $enclosures = get_post_meta( $post_id, 'enclosure' ); + $enclosures = get_post_meta( $post_id, 'enclosure', false ); if ( ! $enclosures ) { return array(); @@ -899,8 +987,8 @@ function get_enclosures( $post_id ) { } return array( - 'url' => $attributes[0], - 'length' => isset( $attributes[1] ) ? trim( $attributes[1] ) : null, + 'url' => $attributes[0], + 'length' => isset( $attributes[1] ) ? trim( $attributes[1] ) : null, 'mediaType' => isset( $attributes[2] ) ? trim( $attributes[2] ) : null, ); }, @@ -917,15 +1005,14 @@ function get_enclosures( $post_id ) { * * @see https://developer.wordpress.org/reference/functions/get_post_ancestors/ * - * @param int|WP_Comment $comment Comment ID or comment object. + * @param int|\WP_Comment $comment Comment ID or comment object. * - * @return WP_Comment[] Array of ancestor comments or empty array if there are none. + * @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 ) { + if ( ! $comment || empty( $comment->comment_parent ) || (int) $comment->comment_parent === (int) $comment->comment_ID ) { return array(); } @@ -934,9 +1021,8 @@ function get_comment_ancestors( $comment ) { $id = (int) $comment->comment_parent; $ancestors[] = $id; - // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition while ( $id > 0 ) { - $ancestor = \get_comment( $id ); + $ancestor = \get_comment( $id ); $parent_id = (int) $ancestor->comment_parent; // Loop detection: If the ancestor has been seen before, break. @@ -977,13 +1063,13 @@ function custom_large_numbers( $formatted, $number, $decimals ) { $thousands_sep = $wp_locale->number_format['thousands_sep']; } - if ( $number < 1000 ) { // any number less than a Thousand. + 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 + } 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 + } 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 + } else { // At least a billion. return \number_format( $number / 1000000000, $decimals, $decimal_point, $thousands_sep ) . 'B'; } @@ -991,6 +1077,37 @@ function custom_large_numbers( $formatted, $number, $decimals ) { return $formatted; } +/** + * Registers a ActivityPub comment type. + * + * @param string $comment_type Key for comment type. + * @param array $args Optional. Array of arguments for registering a comment type. Default empty array. + * + * @return array The registered Activitypub comment type. + */ +function register_comment_type( $comment_type, $args = array() ) { + global $activitypub_comment_types; + + if ( ! is_array( $activitypub_comment_types ) ) { + $activitypub_comment_types = array(); + } + + // Sanitize comment type name. + $comment_type = sanitize_key( $comment_type ); + + $activitypub_comment_types[ $comment_type ] = $args; + + /** + * Fires after a ActivityPub comment type is registered. + * + * @param string $comment_type Comment type. + * @param array $args Arguments used to register the comment type. + */ + do_action( 'activitypub_registered_comment_type', $comment_type, $args ); + + return $args; +} + /** * Normalize a URL. * @@ -1019,27 +1136,173 @@ function normalize_host( $host ) { } /** - * Get the Extra Fields of an Actor + * Get the reply intent URI. * - * @param int $user_id The User-ID. - * - * @return array The extra fields. + * @return string The reply intent URI. */ -function get_actor_extra_fields( $user_id ) { - $extra_fields = new WP_Query( - array( - 'post_type' => 'ap_extrafield', - 'nopaging' => true, - 'status' => 'publish', - 'author' => $user_id, - ) +function get_reply_intent_uri() { + return sprintf( + 'javascript:(()=>{window.open(\'%s\'+encodeURIComponent(window.location.href));})();', + esc_url( \admin_url( 'post-new.php?in_reply_to=' ) ) ); +} - if ( $extra_fields->have_posts() ) { - $extra_fields = $extra_fields->posts; - } else { - $extra_fields = array(); +/** + * Replace content with links, mentions or hashtags by Regex callback and not affect protected tags. + * + * @param string $content The content that should be changed. + * @param string $regex The regex to use. + * @param callable $regex_callback Callback for replacement logic. + * + * @return string The content with links, mentions, hashtags, etc. + */ +function enrich_content_data( $content, $regex, $regex_callback ) { + // Small protection against execution timeouts: limit to 1 MB. + if ( mb_strlen( $content ) > MB_IN_BYTES ) { + return $content; + } + $tag_stack = array(); + $protected_tags = array( + 'pre', + 'code', + 'textarea', + 'style', + 'a', + ); + $content_with_links = ''; + $in_protected_tag = false; + foreach ( wp_html_split( $content ) as $chunk ) { + if ( preg_match( '#^$#i', $chunk, $m ) ) { + $content_with_links .= $chunk; + continue; + } + + if ( preg_match( '#^<(/)?([a-z-]+)\b[^>]*>$#i', $chunk, $m ) ) { + $tag = strtolower( $m[2] ); + if ( '/' === $m[1] ) { + // Closing tag. + $i = array_search( $tag, $tag_stack, true ); + // We can only remove the tag from the stack if it is in the stack. + if ( false !== $i ) { + $tag_stack = array_slice( $tag_stack, 0, $i ); + } + } else { + // Opening tag, add it to the stack. + $tag_stack[] = $tag; + } + + // If we're in a protected tag, the tag_stack contains at least one protected tag string. + // The protected tag state can only change when we encounter a start or end tag. + $in_protected_tag = array_intersect( $tag_stack, $protected_tags ); + + // Never inspect tags. + $content_with_links .= $chunk; + continue; + } + + if ( $in_protected_tag ) { + // Don't inspect a chunk inside an inspected tag. + $content_with_links .= $chunk; + continue; + } + + // Only reachable when there is no protected tag in the stack. + $content_with_links .= \preg_replace_callback( $regex, $regex_callback, $chunk ); } - return apply_filters( 'activitypub_get_actor_extra_fields', $extra_fields, $user_id ); + return $content_with_links; +} + +/** + * Generate a summary of a post. + * + * This function generates a summary of a post by extracting: + * + * 1. The post excerpt if it exists. + * 2. The first part of the post content if it contains the tag. + * 3. An excerpt of the post content if it is longer than the specified length. + * + * @param int|\WP_Post $post The post ID or post object. + * @param integer $length The maximum length of the summary. + * Default is 500. It will be ignored if the post excerpt + * and the content above the tag. + * + * @return string The generated post summary. + */ +function generate_post_summary( $post, $length = 500 ) { + $post = get_post( $post ); + + if ( ! $post ) { + return ''; + } + + $content = \sanitize_post_field( 'post_excerpt', $post->post_excerpt, $post->ID ); + + if ( $content ) { + /** + * Filters the post excerpt. + * + * @param string $content The post excerpt. + */ + return \apply_filters( 'the_excerpt', $content ); + } + + $content = \sanitize_post_field( 'post_content', $post->post_content, $post->ID ); + $content_parts = \get_extended( $content ); + + /** + * Filters the excerpt more value. + * + * @param string $excerpt_more The excerpt more. + */ + $excerpt_more = \apply_filters( 'activitypub_excerpt_more', '[…]' ); + $length = $length - strlen( $excerpt_more ); + + // Check for the tag. + if ( + ! empty( $content_parts['extended'] ) && + ! empty( $content_parts['main'] ) + ) { + $content = $content_parts['main'] . ' ' . $excerpt_more; + $length = null; + } + + $content = \html_entity_decode( $content ); + $content = \wp_strip_all_tags( $content ); + $content = \trim( $content ); + $content = \preg_replace( '/\R+/m', "\n\n", $content ); + $content = \preg_replace( '/[\r\t]/', '', $content ); + + if ( $length && \strlen( $content ) > $length ) { + $content = \wordwrap( $content, $length, '' ); + $content = \explode( '', $content, 2 ); + $content = $content[0] . ' ' . $excerpt_more; + } + + /* + Removed until this is merged: https://github.com/mastodon/mastodon/pull/28629 + return \apply_filters( 'the_excerpt', $content ); + */ + return $content; +} + +/** + * Get the content warning of a post. + * + * @param int|\WP_Post $post_id The post ID or post object. + * + * @return string|false The content warning or false if not found. + */ +function get_content_warning( $post_id ) { + $post = get_post( $post_id ); + if ( ! $post ) { + return false; + } + + $warning = get_post_meta( $post->ID, 'activitypub_content_warning', true ); + if ( empty( $warning ) ) { + return false; + } + + return $warning; } diff --git a/wp-content/plugins/activitypub/includes/handler/class-announce.php b/wp-content/plugins/activitypub/includes/handler/class-announce.php index 46db2a3b..377a3c6e 100644 --- a/wp-content/plugins/activitypub/includes/handler/class-announce.php +++ b/wp-content/plugins/activitypub/includes/handler/class-announce.php @@ -1,16 +1,25 @@ get_json_params(); + + if ( + 'Create' !== $json_params['type'] || + is_wp_error( $request ) + ) { + return $valid; + } + + $object = $json_params['object']; + $required = array( + 'id', + 'inReplyTo', + 'content', + ); + + if ( array_intersect( $required, array_keys( $object ) ) !== $required ) { + return false; + } + + return $valid; } } diff --git a/wp-content/plugins/activitypub/includes/handler/class-delete.php b/wp-content/plugins/activitypub/includes/handler/class-delete.php index 3cc732bf..5b514dfa 100644 --- a/wp-content/plugins/activitypub/includes/handler/class-delete.php +++ b/wp-content/plugins/activitypub/includes/handler/class-delete.php @@ -1,7 +1,12 @@ delete(); self::maybe_delete_interactions( $activity ); @@ -110,7 +129,7 @@ class Delete { * @param array $activity The delete activity. */ public static function maybe_delete_interactions( $activity ) { - // verify if Actor is deleted. + // Verify that Actor is deleted. if ( Http::is_tombstone( $activity['actor'] ) ) { \wp_schedule_single_event( \time(), @@ -123,7 +142,7 @@ class Delete { /** * Delete comments from an Actor. * - * @param array $comments The comments to delete. + * @param array $actor The actor whose comments to delete. */ public static function delete_interactions( $actor ) { $comments = Interactions::get_interactions_by_actor( $actor ); @@ -139,8 +158,6 @@ class Delete { * Delete a Reaction if URL is a Tombstone. * * @param array $activity The delete activity. - * - * @return void */ public static function maybe_delete_interaction( $activity ) { if ( is_array( $activity['object'] ) ) { diff --git a/wp-content/plugins/activitypub/includes/handler/class-follow.php b/wp-content/plugins/activitypub/includes/handler/class-follow.php index ed29dadf..fb94ba6d 100644 --- a/wp-content/plugins/activitypub/includes/handler/class-follow.php +++ b/wp-content/plugins/activitypub/includes/handler/class-follow.php @@ -1,4 +1,10 @@ get__id(); - // save follower + // Save follower. $follower = Followers::add_follower( $user_id, $activity['actor'] @@ -59,7 +63,7 @@ class Follow { $follower ); - // send notification + // Send notification. $notification = new Notification( 'follow', $activity['actor'], @@ -70,25 +74,22 @@ class Follow { } /** - * Send Accept response + * Send Accept response. * - * @param string $actor The Actor URL - * @param array $object The Activity object - * @param int $user_id The ID of the WordPress User - * @param Activitypub\Model\Follower $follower The Follower object - * - * @return void + * @param string $actor The Actor URL. + * @param array $activity_object The Activity object. + * @param int $user_id The ID of the WordPress User. + * @param \Activitypub\Model\Follower $follower The Follower object. */ - public static function send_follow_response( $actor, $object, $user_id, $follower ) { + public static function send_follow_response( $actor, $activity_object, $user_id, $follower ) { if ( \is_wp_error( $follower ) ) { - // it is not even possible to send a "Reject" because - // we can not get the Remote-Inbox + // Impossible to send a "Reject" because we can not get the Remote-Inbox. return; } - // only send minimal data - $object = array_intersect_key( - $object, + // Only send minimal data. + $activity_object = array_intersect_key( + $activity_object, array_flip( array( 'id', @@ -101,13 +102,13 @@ class Follow { $user = Users::get_by_id( $user_id ); - // get inbox + // Get inbox. $inbox = $follower->get_shared_inbox(); - // send "Accept" activity + // Send "Accept" activity. $activity = new Activity(); $activity->set_type( 'Accept' ); - $activity->set_object( $object ); + $activity->set_object( $activity_object ); $activity->set_actor( $user->get_id() ); $activity->set_to( $actor ); $activity->set_id( $user->get_id() . '#follow-' . \preg_replace( '~^https?://~', '', $actor ) . '-' . \time() ); diff --git a/wp-content/plugins/activitypub/includes/handler/class-like.php b/wp-content/plugins/activitypub/includes/handler/class-like.php new file mode 100644 index 00000000..1e951576 --- /dev/null +++ b/wp-content/plugins/activitypub/includes/handler/class-like.php @@ -0,0 +1,70 @@ +add_help_tab( array( @@ -54,17 +59,17 @@ '' . \__( 'The Fediverse is a new word made of two words: "federation" + "universe"', 'activitypub' ) . '
' . '' . \__( 'It is a federated social network running on free open software on a myriad of computers across the globe. Many independent servers are interconnected and allow people to interact with one another. There\'s no one central site: you choose a server to register. This ensures some decentralization and sovereignty of data. Fediverse (also called Fedi) has no built-in advertisements, no tricky algorithms, no one big corporation dictating the rules. Instead we have small cozy communities of like-minded people. Welcome!', 'activitypub' ) . '
' . - '' . \__( 'For more informations please visit fediverse.party', 'activitypub' ) . '
' . + '' . \__( 'For more information please visit fediverse.party', 'activitypub' ) . '
' . '' . \__( 'ActivityPub is a decentralized social networking protocol based on the ActivityStreams 2.0 data format. ActivityPub is an official W3C recommended standard published by the W3C Social Web Working Group. It provides a client to server API for creating, updating and deleting content, as well as a federated server to server API for delivering notifications and subscribing to content.', 'activitypub' ) . '
' . '' . \__( 'WebFinger is used to discover information about people or other entities on the Internet that are identified by a URI using standard Hypertext Transfer Protocol (HTTP) methods over a secure transport. A WebFinger resource returns a JavaScript Object Notation (JSON) object describing the entity that is queried. The JSON object is referred to as the JSON Resource Descriptor (JRD).', 'activitypub' ) . '
' . '' . \__( 'For a person, the type of information that might be discoverable via WebFinger includes a personal profile address, identity service, telephone number, or preferred avatar. For other entities on the Internet, a WebFinger resource might return JRDs containing link relations that enable a client to discover, for example, that a printer can print in color on A4 paper, the physical location of a server, or other static information.', 'activitypub' ) . '
' . '' . \__( 'On Mastodon [and other Plattforms], user profiles can be hosted either locally on the same website as yours, or remotely on a completely different website. The same username may be used on a different domain. Therefore, a Mastodon user\'s full mention consists of both the username and the domain, in the form @username@domain
. In practical terms, @user@example.com
is not the same as @user@example.org
. If the domain is not included, Mastodon will try to find a local user named @username
. However, in order to deliver to someone over ActivityPub, the @username@domain
mention is not enough – mentions must be translated to an HTTPS URI first, so that the remote actor\'s inbox and outbox can be found. (This paragraph is copied from the Mastodon Documentation)', 'activitypub' ) . '
' . \__( 'For more informations please visit webfinger.net', 'activitypub' ) . '
' . + '' . \__( 'For more information please visit webfinger.net', 'activitypub' ) . '
' . '' . \__( 'NodeInfo is an effort to create a standardized way of exposing metadata about a server running one of the distributed social networks. The two key goals are being able to get better insights into the user base of distributed social networking and the ability to build tools that allow users to choose the best fitting software and server for their needs.', 'activitypub' ) . '
' . - '' . \__( 'For more informations please visit nodeinfo.diaspora.software', 'activitypub' ) . '
', + '' . \__( 'For more information please visit nodeinfo.diaspora.software', 'activitypub' ) . '
', ) ); diff --git a/wp-content/plugins/activitypub/includes/model/class-application.php b/wp-content/plugins/activitypub/includes/model/class-application.php index 35c57d64..b7d38af2 100644 --- a/wp-content/plugins/activitypub/includes/model/class-application.php +++ b/wp-content/plugins/activitypub/includes/model/class-application.php @@ -1,4 +1,10 @@ + * @var string */ protected $webfinger; + /** + * Returns the type of the object. + * + * @return string The type of the object. + */ public function get_type() { return 'Application'; } + /** + * Returns whether the Application manually approves followers. + * + * @return true Whether the Application manually approves followers. + */ public function get_manually_approves_followers() { return true; } + /** + * Returns the ID of the Application. + * + * @return string The ID of the Application. + */ public function get_id() { return get_rest_url_by_path( 'application' ); } @@ -73,24 +97,34 @@ class Application extends Actor { return $this->get_url(); } + /** + * Get the Username. + * + * @return string The Username. + */ public function get_name() { return 'application'; } + /** + * Get the preferred username. + * + * @return string The preferred username. + */ 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 + // Try site icon first. $icon_id = get_option( 'site_icon' ); - // try custom logo second + // Try custom logo second. if ( ! $icon_id ) { $icon_id = get_theme_mod( 'custom_logo' ); } @@ -105,7 +139,7 @@ class Application extends Actor { } if ( ! $icon_url ) { - // fallback to default icon + // Fallback to default icon. $icon_url = plugins_url( '/assets/img/wp-logo.png', ACTIVITYPUB_PLUGIN_FILE ); } @@ -131,6 +165,11 @@ class Application extends Actor { return null; } + /** + * Get the first published date. + * + * @return string The published date. + */ public function get_published() { $first_post = new WP_Query( array( @@ -176,18 +215,23 @@ class Application extends Actor { return $this->get_preferred_username() . '@' . \wp_parse_url( \home_url(), \PHP_URL_HOST ); } + /** + * Returns the public key. + * + * @return array The public key. + */ public function get_public_key() { return array( - 'id' => $this->get_id() . '#main-key', - 'owner' => $this->get_id(), + 'id' => $this->get_id() . '#main-key', + 'owner' => $this->get_id(), 'publicKeyPem' => Signature::get_public_key_for( Users::APPLICATION_USER_ID ), ); } /** - * Get the User-Description. + * Get the User description. * - * @return string The User-Description. + * @return string The User description. */ public function get_summary() { return \wpautop( @@ -198,6 +242,11 @@ class Application extends Actor { ); } + /** + * Returns the canonical URL of the object. + * + * @return string|null The canonical URL of the object. + */ public function get_canonical_url() { return \home_url(); } diff --git a/wp-content/plugins/activitypub/includes/model/class-blog.php b/wp-content/plugins/activitypub/includes/model/class-blog.php index 3c52abff..3cbd6f91 100644 --- a/wp-content/plugins/activitypub/includes/model/class-blog.php +++ b/wp-content/plugins/activitypub/includes/model/class-blog.php @@ -1,17 +1,27 @@ + * @var string */ protected $webfinger; /** - * If the User is discoverable. + * Whether the User is discoverable. * * @see https://docs.joinmastodon.org/spec/activitypub/#discoverable * @@ -71,7 +81,7 @@ class Blog extends Actor { protected $discoverable; /** - * Restrict posting to mods + * Restrict posting to mods. * * @see https://join-lemmy.org/docs/contributors/05-federation.html * @@ -79,18 +89,28 @@ class Blog extends Actor { */ protected $posting_restricted_to_mods; + /** + * Whether the User manually approves followers. + * + * @return false + */ public function get_manually_approves_followers() { return false; } + /** + * Whether the User is discoverable. + * + * @return boolean + */ public function get_discoverable() { return true; } /** - * Get the User-ID. + * Get the User ID. * - * @return string The User-ID. + * @return string The User ID. */ public function get_id() { return $this->get_url(); @@ -112,9 +132,9 @@ class Blog extends Actor { } /** - * Get the User-Name. + * Get the Username. * - * @return string The User-Name. + * @return string The Username. */ public function get_name() { return \wp_strip_all_tags( @@ -127,23 +147,29 @@ class Blog extends Actor { } /** - * Get the User-Description. + * Get the User description. * - * @return string The User-Description. + * @return string The User description. */ public function get_summary() { + $summary = \get_option( 'activitypub_blog_description', null ); + + if ( ! $summary ) { + $summary = \get_bloginfo( 'description' ); + } + return \wpautop( \wp_kses( - \get_bloginfo( 'description' ), + $summary, 'default' ) ); } /** - * Get the User-Url. + * Get the User url. * - * @return string The User-Url. + * @return string The User url. */ public function get_url() { return \esc_url( \trailingslashit( get_home_url() ) . '@' . $this->get_preferred_username() ); @@ -164,12 +190,12 @@ class Blog extends Actor { * @return string The auto-generated Username. */ public static function get_default_username() { - // check if domain host has a subdomain + // Check if domain host has a subdomain. $host = \wp_parse_url( \get_home_url(), \PHP_URL_HOST ); $host = \preg_replace( '/^www\./i', '', $host ); /** - * Filter the default blog username. + * Filters the default blog username. * * @param string $host The default username. */ @@ -177,12 +203,12 @@ class Blog extends Actor { } /** - * Get the preferred User-Name. + * Get the preferred Username. * - * @return string The User-Name. + * @return string The Username. */ public function get_preferred_username() { - $username = \get_option( 'activitypub_blog_user_identifier' ); + $username = \get_option( 'activitypub_blog_identifier' ); if ( $username ) { return $username; @@ -192,15 +218,15 @@ class Blog extends Actor { } /** - * Get the User-Icon. + * Get the User icon. * - * @return array The User-Icon. + * @return array The User icon. */ public function get_icon() { - // try site icon first + // Try site_logo, falling back to site_icon, first. $icon_id = get_option( 'site_icon' ); - // try custom logo second + // Try custom logo second. if ( ! $icon_id ) { $icon_id = get_theme_mod( 'custom_logo' ); } @@ -215,7 +241,7 @@ class Blog extends Actor { } if ( ! $icon_url ) { - // fallback to default icon + // Fallback to default icon. $icon_url = plugins_url( '/assets/img/wp-logo.png', ACTIVITYPUB_PLUGIN_FILE ); } @@ -231,16 +257,32 @@ class Blog extends Actor { * @return array|null The User-Header-Image. */ public function get_image() { - if ( \has_header_image() ) { + $header_image = get_option( 'activitypub_header_image' ); + $image_url = null; + + if ( $header_image ) { + $image_url = \wp_get_attachment_url( $header_image ); + } + + if ( ! $image_url && \has_header_image() ) { + $image_url = \get_header_image(); + } + + if ( $image_url ) { return array( 'type' => 'Image', - 'url' => esc_url( \get_header_image() ), + 'url' => esc_url( $image_url ), ); } return null; } + /** + * Get the published date. + * + * @return string The published date. + */ public function get_published() { $first_post = new WP_Query( array( @@ -259,10 +301,20 @@ class Blog extends Actor { return \gmdate( 'Y-m-d\TH:i:s\Z', $time ); } + /** + * Get the canonical URL. + * + * @return string|null The canonical URL. + */ public function get_canonical_url() { return \home_url(); } + /** + * Get the Moderators endpoint. + * + * @return string|null The Moderators endpoint. + */ public function get_moderators() { if ( is_single_user() || 'Group' !== $this->get_type() ) { return null; @@ -271,6 +323,11 @@ class Blog extends Actor { return get_rest_url_by_path( 'collections/moderators' ); } + /** + * Get attributedTo value. + * + * @return string|null The attributedTo value. + */ public function get_attributed_to() { if ( is_single_user() || 'Group' !== $this->get_type() ) { return null; @@ -279,14 +336,24 @@ class Blog extends Actor { return get_rest_url_by_path( 'collections/moderators' ); } + /** + * Get the public key information. + * + * @return array The public key. + */ public function get_public_key() { return array( - 'id' => $this->get_id() . '#main-key', - 'owner' => $this->get_id(), + 'id' => $this->get_id() . '#main-key', + 'owner' => $this->get_id(), 'publicKeyPem' => Signature::get_public_key_for( $this->get__id() ), ); } + /** + * Returns whether posting is restricted to mods. + * + * @return bool|null True if posting is restricted to mods, null if not applicable. + */ public function get_posting_restricted_to_mods() { if ( 'Group' === $this->get_type() ) { return true; @@ -331,6 +398,11 @@ class Blog extends Actor { return get_rest_url_by_path( sprintf( 'actors/%d/following', $this->get__id() ) ); } + /** + * Returns endpoints. + * + * @return array|null The endpoints. + */ public function get_endpoints() { $endpoints = null; @@ -361,45 +433,101 @@ class Blog extends Actor { return get_rest_url_by_path( sprintf( 'actors/%d/collections/featured', $this->get__id() ) ); } + /** + * Returns whether the site is indexable. + * + * @return bool Whether the site is indexable. + */ public function get_indexable() { - if ( \get_option( 'blog_public', 1 ) ) { + if ( is_blog_public() ) { return true; } else { return false; } } + /** + * Update the Username. + * + * @param mixed $value The new value. + * @return bool True if the attribute was updated, false otherwise. + */ + public function update_name( $value ) { + return \update_option( 'blogname', $value ); + } + + /** + * Update the User description. + * + * @param mixed $value The new value. + * @return bool True if the attribute was updated, false otherwise. + */ + public function update_summary( $value ) { + return \update_option( 'blogdescription', $value ); + } + + /** + * Update the User icon. + * + * @param mixed $value The new value. + * @return bool True if the attribute was updated, false otherwise. + */ + public function update_icon( $value ) { + if ( ! wp_attachment_is_image( $value ) ) { + return false; + } + return \update_option( 'site_icon', $value ); + } + + /** + * Update the User-Header-Image. + * + * @param mixed $value The new value. + * @return bool True if the attribute was updated, false otherwise. + */ + public function update_header( $value ) { + if ( ! wp_attachment_is_image( $value ) ) { + return false; + } + return \update_option( 'activitypub_header_image', $value ); + } + + /** + * Get the User - Hashtags. + * + * @see https://docs.joinmastodon.org/spec/activitypub/#Hashtag + * + * @return array The User - Hashtags. + */ + public function get_tag() { + $hashtags = array(); + + $args = array( + 'orderby' => 'count', + 'order' => 'DESC', + 'number' => 10, + ); + + $tags = get_tags( $args ); + + foreach ( $tags as $tag ) { + $hashtags[] = array( + 'type' => 'Hashtag', + 'href' => \get_tag_link( $tag->term_id ), + 'name' => esc_hashtag( $tag->name ), + ); + } + + return $hashtags; + } + /** * Extend the User-Output with Attachments. * * @return array The extended User-Output. */ public function get_attachment() { - $array = array(); - - $array[] = array( - 'type' => 'PropertyValue', - 'name' => \__( 'Blog', 'activitypub' ), - 'value' => \html_entity_decode( - sprintf( - '%s', - \esc_attr( \home_url( '/' ) ), - \esc_url( \home_url( '/' ) ), - \wp_parse_url( \home_url( '/' ), \PHP_URL_HOST ) - ), - \ENT_QUOTES, - 'UTF-8' - ), - ); - - // Add support for FEP-fb2a, for more information see FEDERATION.md - $array[] = array( - 'type' => 'Link', - 'name' => \__( 'Blog', 'activitypub' ), - 'href' => \esc_url( \home_url( '/' ) ), - 'rel' => array( 'me' ), - ); - - return $array; + $extra_fields = Extra_Fields::get_actor_fields( $this->_id ); + return Extra_Fields::fields_to_attachments( $extra_fields ); } } diff --git a/wp-content/plugins/activitypub/includes/model/class-follower.php b/wp-content/plugins/activitypub/includes/model/class-follower.php index 4590ea49..45ef6157 100644 --- a/wp-content/plugins/activitypub/includes/model/class-follower.php +++ b/wp-content/plugins/activitypub/includes/model/class-follower.php @@ -1,13 +1,18 @@ _id, 'activitypub_errors' ); + return get_post_meta( $this->_id, 'activitypub_errors', false ); } /** * Get the Summary. * - * @return int The Summary. + * @return string The Summary. */ public function get_summary() { if ( isset( $this->summary ) ) { @@ -51,7 +56,7 @@ class Follower extends Actor { * Getter for URL attribute. * * Falls back to ID, if no URL is set. This is relevant for - * Plattforms like Lemmy, where the ID is the URL. + * Platforms like Lemmy, where the ID is the URL. * * @return string The URL. */ @@ -65,8 +70,6 @@ class Follower extends Actor { /** * Reset (delete) all errors. - * - * @return void */ public function reset_errors() { delete_post_meta( $this->_id, 'activitypub_errors' ); @@ -103,21 +106,19 @@ class Follower extends Actor { } /** - * Update the current Follower-Object. - * - * @return void + * Update the current Follower object. */ public function update() { $this->save(); } /** - * Validate the current Follower-Object. + * Validate the current Follower object. * * @return boolean True if the verification was successful. */ public function is_valid() { - // the minimum required attributes + // The minimum required attributes. $required_attributes = array( 'id', 'preferredUsername', @@ -136,9 +137,9 @@ class Follower extends Actor { } /** - * Save the current Follower-Object. + * Save the current Follower object. * - * @return int|WP_Error The Post-ID or an WP_Error. + * @return int|WP_Error The post ID or an WP_Error. */ public function save() { if ( ! $this->is_valid() ) { @@ -148,7 +149,7 @@ class Follower extends Actor { if ( ! $this->get__id() ) { global $wpdb; - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching + // phpcs:ignore WordPress.DB.DirectDatabaseQuery $post_id = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE guid=%s", @@ -177,37 +178,35 @@ class Follower extends Actor { ); if ( ! empty( $post_id ) ) { - // If this is an update, prevent the "followed" date from being - // overwritten by the current date. + // If this is an update, prevent the "followed" date from being overwritten by the current date. $post = get_post( $post_id ); $args['post_date'] = $post->post_date; $args['post_date_gmt'] = $post->post_date_gmt; } - $post_id = wp_insert_post( $args ); + $post_id = wp_insert_post( $args ); $this->_id = $post_id; return $post_id; } /** - * Upsert the current Follower-Object. + * Upsert the current Follower object. * - * @return int|WP_Error The Post-ID or an WP_Error. + * @return int|WP_Error The post ID or an WP_Error. */ public function upsert() { return $this->save(); } /** - * Delete the current Follower-Object. + * Delete the current Follower object. * * Beware that this os deleting a Follower for ALL users!!! * * To delete only the User connection (unfollow) - * @see \Activitypub\Rest\Followers::remove_follower() * - * @return void + * @see \Activitypub\Rest\Followers::remove_follower() */ public function delete() { wp_delete_post( $this->_id ); @@ -215,12 +214,10 @@ class Follower extends Actor { /** * Update the post meta. - * - * @return void */ protected function get_post_meta_input() { - $meta_input = array(); - $meta_input['activitypub_inbox'] = $this->get_shared_inbox(); + $meta_input = array(); + $meta_input['activitypub_inbox'] = $this->get_shared_inbox(); $meta_input['activitypub_actor_json'] = $this->to_json(); return $meta_input; @@ -239,9 +236,9 @@ class Follower extends Actor { } return array( - 'type' => 'Image', + 'type' => 'Image', 'mediaType' => 'image/jpeg', - 'url' => ACTIVITYPUB_PLUGIN_URL . 'assets/img/mp.jpg', + 'url' => ACTIVITYPUB_PLUGIN_URL . 'assets/img/mp.jpg', ); } @@ -278,7 +275,7 @@ class Follower extends Actor { } /** - * Get the Icon URL (Avatar) + * Get the Icon URL (Avatar). * * @return string The URL to the Avatar. */ @@ -297,7 +294,7 @@ class Follower extends Actor { } /** - * Get the Icon URL (Avatar) + * Get the Icon URL (Avatar). * * @return string The URL to the Avatar. */ @@ -333,13 +330,12 @@ class Follower extends Actor { /** * Convert a Custom-Post-Type input to an Activitypub\Model\Follower. * - * @return string The JSON string. - * - * @return array Activitypub\Model\Follower + * @param \WP_Post $post The post object. + * @return \Activitypub\Activity\Base_Object|WP_Error */ public static function init_from_cpt( $post ) { $actor_json = get_post_meta( $post->ID, 'activitypub_actor_json', true ); - $object = self::init_from_json( $actor_json ); + $object = self::init_from_json( $actor_json ); $object->set__id( $post->ID ); $object->set_id( $post->guid ); $object->set_name( $post->post_title ); @@ -370,10 +366,10 @@ class Follower extends Actor { if ( $path ) { if ( \strpos( $name, '@' ) !== false ) { - // expected: https://example.com/@user (default URL pattern) + // Expected: https://example.com/@user (default URL pattern). $name = \preg_replace( '|^/@?|', '', $path ); } else { - // expected: https://example.com/users/user (default ID pattern) + // Expected: https://example.com/users/user (default ID pattern). $parts = \explode( '/', $path ); $name = \array_pop( $parts ); } @@ -383,7 +379,7 @@ class Follower extends Actor { \strpos( $name, 'acct' ) === 0 || \strpos( $name, '@' ) === 0 ) { - // expected: user@example.com or acct:user@example (WebFinger) + // Expected: user@example.com or acct:user@example (WebFinger). $name = \ltrim( $name, '@' ); $name = \ltrim( $name, 'acct:' ); $parts = \explode( '@', $name ); diff --git a/wp-content/plugins/activitypub/includes/model/class-post.php b/wp-content/plugins/activitypub/includes/model/class-post.php deleted file mode 100644 index a4229539..00000000 --- a/wp-content/plugins/activitypub/includes/model/class-post.php +++ /dev/null @@ -1,136 +0,0 @@ -post = $post; - $this->object = $transformer->to_object(); - } - } - - /** - * Returns the User ID. - * - * @return int the User ID. - */ - public function get_user_id() { - return apply_filters( 'activitypub_post_user_id', $this->post->post_author, $this->post ); - } - - /** - * Converts this Object into an Array. - * - * @return array the array representation of a Post. - */ - public function to_array() { - return \apply_filters( 'activitypub_post', $this->object->to_array(), $this->post ); - } - - /** - * Returns the Actor of this Object. - * - * @return string The URL of the Actor. - */ - public function get_actor() { - $user = Users::get_by_id( $this->get_user_id() ); - - return $user->get_url(); - } - - /** - * Converts this Object into a JSON String - * - * @return string - */ - public function to_json() { - return \wp_json_encode( $this->to_array(), \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT ); - } - - /** - * Returns the URL of an Activity Object - * - * @return string - */ - public function get_url() { - return $this->object->get_url(); - } - - /** - * Returns the ID of an Activity Object - * - * @return string - */ - public function get_id() { - return $this->object->get_id(); - } - - /** - * Returns a list of Image Attachments - * - * @return array - */ - public function get_attachments() { - return $this->object->get_attachment(); - } - - /** - * Returns a list of Tags, used in the Post - * - * @return array - */ - public function get_tags() { - return $this->object->get_tag(); - } - - /** - * Returns the as2 object-type for a given post - * - * @return string the object-type - */ - public function get_object_type() { - return $this->object->get_type(); - } - - /** - * Returns the content for the ActivityPub Item. - * - * @return string the content - */ - public function get_content() { - return $this->object->get_content(); - } -} diff --git a/wp-content/plugins/activitypub/includes/model/class-user.php b/wp-content/plugins/activitypub/includes/model/class-user.php index dc6cb6c0..997c5210 100644 --- a/wp-content/plugins/activitypub/includes/model/class-user.php +++ b/wp-content/plugins/activitypub/includes/model/class-user.php @@ -1,18 +1,24 @@ + * @var string */ protected $webfinger; + /** + * The type of the object. + * + * @return string The type of the object. + */ public function get_type() { return 'Person'; } + /** + * Generate a User object from a WP_User. + * + * @param int $user_id The user ID. + * + * @return WP_Error|User The User object or WP_Error if user not found. + */ public static function from_wp_user( $user_id ) { if ( is_user_disabled( $user_id ) ) { return new WP_Error( @@ -75,37 +93,37 @@ class User extends Actor { ); } - $object = new static(); + $object = new static(); $object->_id = $user_id; return $object; } /** - * Get the User-ID. + * Get the user ID. * - * @return string The User-ID. + * @return string The user ID. */ public function get_id() { return $this->get_url(); } /** - * Get the User-Name. + * Get the Username. * - * @return string The User-Name. + * @return string The Username. */ public function get_name() { return \esc_attr( \get_the_author_meta( 'display_name', $this->_id ) ); } /** - * Get the User-Description. + * Get the User description. * - * @return string The User-Description. + * @return string The User description. */ public function get_summary() { - $description = get_user_meta( $this->_id, 'activitypub_user_description', true ); + $description = get_user_option( 'activitypub_description', $this->_id ); if ( empty( $description ) ) { $description = get_user_meta( $this->_id, 'description', true ); } @@ -113,28 +131,46 @@ class User extends Actor { } /** - * Get the User-Url. + * Get the User url. * - * @return string The User-Url. + * @return string The User url. */ public function get_url() { return \esc_url( \get_author_posts_url( $this->_id ) ); } /** - * Returns the User-URL with @-Prefix for the username. + * Returns the User URL with @-Prefix for the username. * - * @return string The User-URL with @-Prefix for the username. + * @return string The User URL with @-Prefix for the username. */ public function get_alternate_url() { return \esc_url( \trailingslashit( get_home_url() ) . '@' . $this->get_preferred_username() ); } + /** + * Get the preferred username. + * + * @return string The preferred username. + */ public function get_preferred_username() { return \esc_attr( \get_the_author_meta( 'login', $this->_id ) ); } + /** + * Get the User icon. + * + * @return array The User icon. + */ public function get_icon() { + $icon = \get_user_option( 'activitypub_icon', $this->_id ); + if ( wp_attachment_is_image( $icon ) ) { + return array( + 'type' => 'Image', + 'url' => esc_url( wp_get_attachment_url( $icon ) ), + ); + } + $icon = \esc_url( \get_avatar_url( $this->_id, @@ -148,26 +184,51 @@ class User extends Actor { ); } + /** + * Returns the header image. + * + * @return array|null The header image. + */ public function get_image() { - if ( \has_header_image() ) { - $image = \esc_url( \get_header_image() ); + $header_image = get_user_option( 'activitypub_header_image', $this->_id ); + $image_url = null; + + if ( ! $header_image && \has_header_image() ) { + $image_url = \get_header_image(); + } + + if ( $header_image ) { + $image_url = \wp_get_attachment_url( $header_image ); + } + + if ( $image_url ) { return array( 'type' => 'Image', - 'url' => $image, + 'url' => esc_url( $image_url ), ); } return null; } + /** + * Returns the date the user was created. + * + * @return false|string The date the user was created. + */ public function get_published() { return \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( \get_the_author_meta( 'registered', $this->_id ) ) ); } + /** + * Returns the public key. + * + * @return array The public key. + */ public function get_public_key() { return array( - 'id' => $this->get_id() . '#main-key', - 'owner' => $this->get_id(), + 'id' => $this->get_id() . '#main-key', + 'owner' => $this->get_id(), 'publicKeyPem' => Signature::get_public_key_for( $this->get__id() ), ); } @@ -217,6 +278,11 @@ class User extends Actor { return get_rest_url_by_path( sprintf( 'actors/%d/collections/featured', $this->get__id() ) ); } + /** + * Returns the endpoints. + * + * @return array|null The endpoints. + */ public function get_endpoints() { $endpoints = null; @@ -235,74 +301,8 @@ class User extends Actor { * @return array The extended User-Output. */ public function get_attachment() { - $extra_fields = get_actor_extra_fields( $this->_id ); - - $attachments = array(); - - foreach ( $extra_fields as $post ) { - $content = \get_the_content( null, false, $post ); - $content = \make_clickable( $content ); - $content = \do_blocks( $content ); - $content = \wptexturize( $content ); - $content = \wp_filter_content_tags( $content ); - // replace script and style elements - $content = \preg_replace( '@<(script|style)[^>]*?>.*?\\1>@si', '', $content ); - $content = \strip_shortcodes( $content ); - $content = \trim( \preg_replace( '/[\n\r\t]/', '', $content ) ); - - $attachments[] = array( - 'type' => 'PropertyValue', - 'name' => \get_the_title( $post ), - 'value' => \html_entity_decode( - $content, - \ENT_QUOTES, - 'UTF-8' - ), - ); - - $link_added = false; - - // Add support for FEP-fb2a, for more information see FEDERATION.md - if ( \class_exists( '\WP_HTML_Tag_Processor' ) ) { - $tags = new \WP_HTML_Tag_Processor( $content ); - $tags->next_tag(); - - if ( 'P' === $tags->get_tag() ) { - $tags->next_tag(); - } - - if ( 'A' === $tags->get_tag() ) { - $tags->set_bookmark( 'link' ); - if ( ! $tags->next_tag() ) { - $tags->seek( 'link' ); - $attachment = array( - 'type' => 'Link', - 'name' => \get_the_title( $post ), - 'href' => \esc_url( $tags->get_attribute( 'href' ) ), - 'rel' => explode( ' ', $tags->get_attribute( 'rel' ) ), - ); - - $link_added = true; - } - } - } - - if ( ! $link_added ) { - $attachment = array( - 'type' => 'Note', - 'name' => \get_the_title( $post ), - 'content' => \html_entity_decode( - $content, - \ENT_QUOTES, - 'UTF-8' - ), - ); - } - - $attachments[] = $attachment; - } - - return $attachments; + $extra_fields = Extra_Fields::get_actor_fields( $this->_id ); + return Extra_Fields::fields_to_attachments( $extra_fields ); } /** @@ -314,23 +314,93 @@ class User extends Actor { return $this->get_preferred_username() . '@' . \wp_parse_url( \home_url(), \PHP_URL_HOST ); } + /** + * Returns the canonical URL. + * + * @return string The canonical URL. + */ public function get_canonical_url() { return $this->get_url(); } + /** + * Returns the streams. + * + * @return null The streams. + */ public function get_streams() { return null; } + /** + * Returns the tag. + * + * @return array The tag. + */ public function get_tag() { return array(); } + /** + * Returns the indexable state. + * + * @return bool Whether the user is indexable. + */ public function get_indexable() { - if ( \get_option( 'blog_public', 1 ) ) { + if ( is_blog_public() ) { return true; } else { return false; } } + + /** + * Update the username. + * + * @param string $value The new value. + * @return int|WP_Error The updated user ID or WP_Error on failure. + */ + public function update_name( $value ) { + $userdata = array( + 'ID' => $this->_id, + 'display_name' => $value, + ); + return \wp_update_user( $userdata ); + } + + /** + * Update the User description. + * + * @param string $value The new value. + * @return bool True if the attribute was updated, false otherwise. + */ + public function update_summary( $value ) { + return \update_user_option( $this->_id, 'activitypub_description', $value ); + } + + /** + * Update the User icon. + * + * @param int $value The new value. Should be an attachment ID. + * @return bool True if the attribute was updated, false otherwise. + */ + public function update_icon( $value ) { + if ( ! wp_attachment_is_image( $value ) ) { + return false; + } + return update_user_option( $this->_id, 'activitypub_icon', $value ); + } + + /** + * Update the User-Header-Image. + * + * @param int $value The new value. Should be an attachment ID. + * @return bool True if the attribute was updated, false otherwise. + */ + public function update_header( $value ) { + if ( ! wp_attachment_is_image( $value ) ) { + return false; + } + return \update_user_option( $this->_id, 'activitypub_header_image', $value ); + } } diff --git a/wp-content/plugins/activitypub/includes/rest/class-actors.php b/wp-content/plugins/activitypub/includes/rest/class-actors.php index f5854a68..60f03d29 100644 --- a/wp-content/plugins/activitypub/includes/rest/class-actors.php +++ b/wp-content/plugins/activitypub/includes/rest/class-actors.php @@ -1,18 +1,22 @@ get_param( 'user_id' ); @@ -77,14 +81,17 @@ class Actors { return $user; } - // redirect to canonical URL if it is not an ActivityPub request + $link_header = sprintf( '<%1$s>; rel="alternate"; type="application/activity+json"', $user->get_id() ); + + // Redirect to canonical URL if it is not an ActivityPub request. if ( ! is_activitypub_request() ) { + header( 'Link: ' . $link_header ); header( 'Location: ' . $user->get_canonical_url(), true, 301 ); exit; } - /* - * Action triggerd prior to the ActivityPub profile being created and sent to the client + /** + * Action triggered prior to the ActivityPub profile being created and sent to the client. */ \do_action( 'activitypub_rest_users_pre' ); @@ -92,17 +99,18 @@ class Actors { $rest_response = new WP_REST_Response( $json, 200 ); $rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) ); + $rest_response->header( 'Link', $link_header ); return $rest_response; } /** - * Endpoint for remote follow UI/Block + * Endpoint for remote follow UI/Block. * * @param WP_REST_Request $request The request object. * - * @return void|string The URL to the remote follow page + * @return WP_REST_Response|\WP_Error The response object or WP_Error. */ public static function remote_follow_get( WP_REST_Request $request ) { $resource = $request->get_param( 'resource' ); @@ -123,15 +131,18 @@ class Actors { $url = str_replace( '{uri}', $resource, $template ); return new WP_REST_Response( - array( 'url' => $url, 'template' => $template ), + array( + 'url' => $url, + 'template' => $template, + ), 200 ); } /** - * The supported parameters + * The supported parameters. * - * @return array list of parameters + * @return array List of parameters, */ public static function request_parameters() { $params = array(); diff --git a/wp-content/plugins/activitypub/includes/rest/class-collection.php b/wp-content/plugins/activitypub/includes/rest/class-collection.php index 296789fb..02cc99bf 100644 --- a/wp-content/plugins/activitypub/includes/rest/class-collection.php +++ b/wp-content/plugins/activitypub/includes/rest/class-collection.php @@ -1,4 +1,10 @@ [\w\-\.]+)s/(?P)|(?<=
)|^)#([A-Za-z0-9_]+)(?:(?=\s|[[:punct:]]|$))`.
* `ACTIVITYPUB_USERNAME_REGEXP` - Change the default regex to detect @-replies in a text. Default: `(?:([A-Za-z0-9\._-]+)@((?:[A-Za-z0-9_-]+\.)+[A-Za-z]+))`.
+* `ACTIVITYPUB_URL_REGEXP` - Change the default regex to detect urls in a text. Default: `(www.|http:|https:)+[^\s]+[\w\/]`.
* `ACTIVITYPUB_CUSTOM_POST_CONTENT` - Change the default template for Activities. Default: `[ap_title]\n\n[ap_content]\n\n[ap_hashtags]\n\n[ap_shortlink]`.
* `ACTIVITYPUB_AUTHORIZED_FETCH` - Enable AUTHORIZED_FETCH. Default: `false`.
* `ACTIVITYPUB_DISABLE_REWRITES` - Disable auto generation of `mod_rewrite` rules. Default: `false`.
@@ -131,62 +138,95 @@ The followers of a user can be found in the menu under "Users" -> "Followers" or
For reasons of data protection, it is not possible to see the followers of other users.
+== Screenshots ==
+
+1. The "Follow me"-Block in the Block-Editor
+2. The "Followers"-Block in the Block-Editor
+3. The "Federated Reply"-Block in the Block-Editor
+4. A "Federated Reply" in a Post
+5. A Blog-Profile on Mastodon
+
== Changelog ==
-= 2.6.1 =
+= 3.3.3 =
-* Fixed: Extra Fields will generate wrong entries
+* Fixed: Sanitization callback
+* Improved: A lot of PHPCS cleanups
+* Improved: Prepare multi-lang support
-= 2.6.0 =
+= 3.3.2 =
-* Added: Support for FEP-fb2a
-* Added: CRUD support for Extra Fields
-* Improved: Remote-Follow UI and UX
-* Improved: Open Graph `fediverse:creator` implementation
-* Fixed: Compatibility issues with fed.brid.gy
-* Fixed: Remote-Reply endpoint
-* Fixed: WebFinger Error Codes (thanks to the FediTest project)
-* Fixed: Fatal Error when wp_schedule_single_event third argument is being passed as a string
+* Fixed: Keep priority of Icons
+* Fixed: Fatal error if remote-object is `WP_Error`
+* Improved: Adopt WordPress PHP Coding Standards
-= 2.5.0 =
+= 3.3.1 =
-* Added: WebFinger cors header
-* Added: WebFinger Content-Type
-* Added: The Fediverse creator of a post to OpenGraph
-* Improved: Try to lookup local users first for Enable Mastodon Apps
-* Improved: Send also Announces for deletes
-* Improved: Load time by adding `count_total=false` to `WP_User_Query`
-* Fixed: Several WebFinger issues
-* Fixed: Redirect issue for Application user
-* Fixed: Accessibilty issues with missing screen-reader-text on User overview page
+* Fixed: PHP Warnings
+* Fixed: PHPCS issues
-= 2.4.0 =
+= 3.3.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
+* Added: Content warning support
+* Added: Replies collection
+* Added: Enable Mastodon Apps: support profile editing, blog user
+* Added: Follow Me/Followers: add inherit mode for dynamic templating
+* Fixed: Cropping Header Images for users without the 'customize' capability
+* Improved: OpenSSL handling
+* Improved: Added missing @ in Follow-Me block
-See full Changelog on [GitHub](https://github.com/Automattic/wordpress-activitypub/blob/master/CHANGELOG.md).
+= 3.2.5 =
+
+* Fixed: Enable Mastodon Apps check
+* Fixed: Fediverse replies were not threaded properly
+
+= 3.2.4 =
+
+* Improved: Inbox validation
+
+= 3.2.3 =
+
+* Fixed: NodeInfo endpoint
+* Fixed: (Temporarily) Remove HTML from `summary`, because it seems that Mastodon has issues with it
+* Improved: Accessibility for Reply-Context
+* Improved: Use `Article` Object-Type as default
+
+= 3.2.2 =
+
+* Fixed: Extra-Fields check
+
+= 3.2.1 =
+
+* Fixed: Use `Excerpt` for Podcast Episodes
+
+= 3.2.0 =
+
+* Added: Support for Seriously Simple Podcasting
+* Added: Blog extra fields
+* Added: Support "read more" for Activity-Summary
+* Added: `Like` and `Announce` (Boost) handler
+* Added: Simple Remote-Reply endpoint
+* Added: "Stream" Plugin support
+* Added: New Fediverse symbol
+* Improved: Replace hashtags, urls and mentions in summary with links
+* Improved: Hide Bookmarklet if site does not support Blocks
+* Fixed: Link detection for extra fields when spaces after the link and fix when two links in the content
+* Fixed: `Undo` for `Likes` and `Announces`
+* Fixed: Show Avatars on `Likes` and `Announces`
+* Fixed: Remove proprietary WebFinger resource
+* Fixed: Wrong followers URL in "to" attribute of posts
+
+= 3.1.0 =
+
+* Added: `menu_order` to `ap_extrafield` so that user can decide in with order they will be displayed
+* Added: Line brakes to user biography
+* Added: Blueprint
+* Fixed: Changed missing `activitypub_user_description` to `activitypub_description`
+* Fixed: Undefined `get_sample_permalink`
+* Fixed: Only send Update for previously-published posts
+* Improved: Simplified WebFinger code
+
+See full Changelog on [GitHub](https://github.com/Automattic/wordpress-activitypub/blob/trunk/CHANGELOG.md).
== Upgrade Notice ==
diff --git a/wp-content/plugins/activitypub/templates/admin-header.php b/wp-content/plugins/activitypub/templates/admin-header.php
index 67b91ba9..c453328d 100644
--- a/wp-content/plugins/activitypub/templates/admin-header.php
+++ b/wp-content/plugins/activitypub/templates/admin-header.php
@@ -1,5 +1,20 @@
'',
+ 'settings' => '',
+ 'blog-profile' => '',
+ 'followers' => '',
+ )
+);
?>