diff --git a/wp-content/plugins/activitypub/activitypub.php b/wp-content/plugins/activitypub/activitypub.php
index 145f5182..a6dca243 100644
--- a/wp-content/plugins/activitypub/activitypub.php
+++ b/wp-content/plugins/activitypub/activitypub.php
@@ -1,14 +1,14 @@
)|(?<=
)|^)#([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: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;u@yourusername@example.com
)","activitypub"),{code:(0,i.createElement)("code",null)})),(0,i.createElement)("div",{className:"activitypub-dialog__button-group"},(0,i.createElement)("label",{htmlFor:"remote-profile",className:"screen-reader-text"},(0,u.__)("Enter your ActivityPub profile","activitypub")),(0,i.createElement)("input",{type:"text",id:"remote-profile",value:T,onKeyDown:e=>{"Enter"===e?.code&&z()},onChange:e=>N(e.target.value),"aria-invalid":y===v}),(0,i.createElement)(d.Button,{onClick:z,"aria-label":(0,u.__)("Submit profile","activitypub")},(0,i.createElement)(S,{icon:a}),y)),l&&(0,i.createElement)("div",{className:"activitypub-dialog__remember"},(0,i.createElement)(d.CheckboxControl,{checked:I,label:(0,u.__)("Remember me for easier comments","activitypub"),onChange:()=>{R(!I)}}))))}const N={avatar:"",webfinger:"@well@hello.dolly",name:(0,u.__)("Hello Dolly Fan Account","activitypub"),url:"#"};function I(e){if(!e)return N;const t={...N,...e};return t.avatar=t?.icon?.url,t}function R({profile:e,popupStyles:t,userId:o,buttonText:r,buttonOnly:n,buttonSize:l}){const{webfinger:a,avatar:c,name:u}=e,s=a.startsWith("@")?a:`@${a}`;return n?(0,i.createElement)("div",{className:"activitypub-profile"},(0,i.createElement)(U,{profile:e,popupStyles:t,userId:o,buttonText:r,buttonSize:l})):(0,i.createElement)("div",{className:"activitypub-profile"},(0,i.createElement)("img",{className:"activitypub-profile__avatar",src:c,alt:u}),(0,i.createElement)("div",{className:"activitypub-profile__content"},(0,i.createElement)("div",{className:"activitypub-profile__name"},u),(0,i.createElement)("div",{className:"activitypub-profile__handle",title:s},s)),(0,i.createElement)(U,{profile:e,popupStyles:t,userId:o,buttonText:r,buttonSize:l}))}function U({profile:e,popupStyles:t,userId:o,buttonText:r,buttonSize:n}){const[l,a]=(0,m.useState)(!1),c=(0,u.sprintf)(/* translators: %s: profile name */ /* translators: %s: profile name */
+(0,u.__)("Follow %s","activitypub"),e?.name);return(0,i.createElement)(i.Fragment,null,(0,i.createElement)(d.Button,{className:"activitypub-profile__follow",onClick:()=>a(!0),"aria-haspopup":"dialog","aria-expanded":l,"aria-label":(0,u.__)("Follow me on the Fediverse","activitypub"),size:n},r),l&&(0,i.createElement)(d.Modal,{className:"activitypub-profile__confirm activitypub__modal",onRequestClose:()=>a(!1),title:c,"aria-label":c,role:"dialog"},(0,i.createElement)(z,{profile:e,userId:o}),(0,i.createElement)("style",null,t)))}function z({profile:e,userId:t}){const{namespace:o}=v(),{webfinger:r}=e,n=(0,u.__)("Follow","activitypub"),l=`/${o}/actors/${t}/remote-follow?resource=`,a=(0,u.__)("Copy and paste my profile into the search field of your favorite fediverse app or server.","activitypub"),c=r.startsWith("@")?r:`@${r}`;return(0,i.createElement)(T,{actionText:n,copyDescription:a,handle:c,resourceUrl:l})}function $({selectedUser:e,style:t,backgroundColor:o,id:r,useId:n=!1,profileData:l=!1,buttonOnly:a=!1,buttonText:c=(0,u.__)("Follow","activitypub"),buttonSize:s="default"}){const[p,d]=(0,m.useState)(I()),b="site"===e?0:e,y=function(e){return w(".apfmd__button-group .components-button",_(e?.elements?.link?.color?.text)||"#111","#fff",_(e?.elements?.link?.[":hover"]?.color?.text)||"#333")}(t),h=n?{id:r}:{};return(0,m.useEffect)((()=>{l?d(I(l)):function(e){const{namespace:t}=v(),o={headers:{Accept:"application/activity+json"},path:`/${t}/actors/${e}`};return f()(o)}(b).then((e=>{d(I(e))}))}),[b,l]),(0,i.createElement)("div",{...h,className:"activitypub-follow-me-block-wrapper"},(0,i.createElement)(g,{selector:`#${r}`,style:t,backgroundColor:o}),(0,i.createElement)(R,{profile:p,userId:b,popupStyles:y,buttonText:c,buttonOnly:a,buttonSize:s}))}function P({name:e}){const{enabled:t}=v(),o=t?.site?"":(0,u.__)("It will be empty in other non-author contexts.","activitypub"),r=(0,u.sprintf)(/* translators: %1$s: block name, %2$s: extra information for non-author context */ /* translators: %1$s: block name, %2$s: extra information for non-author context */
+(0,u.__)("This %1$s block will adapt to the page it is on, displaying the user profile associated with a post author (in a loop) or a user archive. %2$s","activitypub"),e,o).trim();return(0,i.createElement)(d.Card,null,(0,i.createElement)(d.CardBody,null,(0,m.createInterpolateElement)(r,{strong:(0,i.createElement)("strong",null)})))}(0,r.registerBlockType)("activitypub/follow-me",{edit:function({attributes:e,setAttributes:t,context:{postType:o,postId:r}}){const n=(0,c.useBlockProps)({className:"activitypub-follow-me-block-wrapper"}),l=function({withInherit:e=!1}){const{enabled:t}=v(),o=t?.users?(0,s.useSelect)((e=>e("core").getUsers({who:"authors"}))):[];return(0,m.useMemo)((()=>{if(!o)return[];const r=[];return t?.site&&r.push({label:(0,u.__)("Site","activitypub"),value:"site"}),e&&t?.users&&r.push({label:(0,u.__)("Dynamic User","activitypub"),value:"inherit"}),o.reduce(((e,t)=>(e.push({label:t.name,value:`${t.id}`}),e)),r)}),[o])}({withInherit:!0}),{selectedUser:a,buttonOnly:b,buttonText:f,buttonSize:y}=e,_="inherit"===a,h=(0,s.useSelect)((e=>{const{getEditedEntityRecord:t}=e(p.store),n=t("postType",o,r)?.author;return null!=n?n:null}),[o,r]);return(0,m.useEffect)((()=>{l.length&&(l.find((({value:e})=>e===a))||t({selectedUser:l[0].value}))}),[a,l]),(0,i.createElement)("div",{...n},(0,i.createElement)(c.InspectorControls,{key:"activitypub-follow-me"},(0,i.createElement)(d.PanelBody,{title:(0,u.__)("Follow Me Options","activitypub")},l.length>1&&(0,i.createElement)(d.SelectControl,{label:(0,u.__)("Select User","activitypub"),value:e.selectedUser,options:l,onChange:e=>t({selectedUser:e})}),(0,i.createElement)(d.ToggleControl,{label:(0,u.__)("Button Only Mode","activitypub"),checked:b,onChange:e=>t({buttonOnly:e}),help:(0,u.__)("Only show the follow button without profile information","activitypub")}),(0,i.createElement)(d.TextControl,{label:(0,u.__)("Button Text","activitypub"),value:f,onChange:e=>t({buttonText:e})}),(0,i.createElement)(d.SelectControl,{label:(0,u.__)("Button Size","activitypub"),value:y,options:[{label:(0,u.__)("Default","activitypub"),value:"default"},{label:(0,u.__)("Compact","activitypub"),value:"compact"},{label:(0,u.__)("Small","activitypub"),value:"small"}],onChange:e=>t({buttonSize:e}),help:(0,u.__)("Choose the size of the follow button","activitypub")}))),_?h?(0,i.createElement)($,{...e,id:n.id,selectedUser:h}):(0,i.createElement)(P,{name:(0,u.__)("Follow Me","activitypub")}):(0,i.createElement)($,{...e,id:n.id}))},save:()=>null,icon:a})},20:(e,t,o)=>{var r=o(609),n=Symbol.for("react.element"),l=(Symbol.for("react.fragment"),Object.prototype.hasOwnProperty),a=r.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,i={key:!0,ref:!0,__self:!0,__source:!0};t.jsx=function(e,t,o){var r,c={},u=null,s=null;for(r in void 0!==o&&(u=""+o),void 0!==t.key&&(u=""+t.key),void 0!==t.ref&&(s=t.ref),t)l.call(t,r)&&!i.hasOwnProperty(r)&&(c[r]=t[r]);if(e&&e.defaultProps)for(r in t=e.defaultProps)void 0===c[r]&&(c[r]=t[r]);return{$$typeof:n,type:e,key:u,ref:s,props:c,_owner:a.current}}},848:(e,t,o)=>{e.exports=o(20)},609:e=>{e.exports=window.React}},o={};function r(e){var n=o[e];if(void 0!==n)return n.exports;var l=o[e]={exports:{}};return t[e](l,l.exports,r),l.exports}r.m=t,e=[],r.O=(t,o,n,l)=>{if(!o){var a=1/0;for(s=0;s@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;u@yourusername@example.com
)","activitypub"),{code:(0,o.createElement)("code",null)})),(0,o.createElement)("div",{className:"activitypub-dialog__button-group"},(0,o.createElement)("label",{htmlFor:"remote-profile",className:"screen-reader-text"},(0,s.__)("Enter your ActivityPub profile","activitypub")),(0,o.createElement)("input",{type:"text",id:"remote-profile",value:C,onKeyDown:e=>{"Enter"===e?.code&&z()},onChange:e=>R(e.target.value),"aria-invalid":b===v}),(0,o.createElement)(u.Button,{onClick:z,"aria-label":(0,s.__)("Submit profile","activitypub")},(0,o.createElement)(h,{icon:g}),b)),l&&(0,o.createElement)("div",{className:"activitypub-dialog__remember"},(0,o.createElement)(u.CheckboxControl,{checked:I,label:(0,s.__)("Remember me for easier comments","activitypub"),onChange:()=>{T(!I)}}))))}function O(){return window._activityPubOptions||{}}const N={avatar:"",webfinger:"@well@hello.dolly",name:(0,s.__)("Hello Dolly Fan Account","activitypub"),url:"#"};function C(e){if(!e)return N;const t={...N,...e};return t.avatar=t?.icon?.url,t}function R({profile:e,popupStyles:t,userId:r,buttonText:a,buttonOnly:n,buttonSize:i}){const{webfinger:l,avatar:c,name:u}=e,s=l.startsWith("@")?l:`@${l}`;return n?(0,o.createElement)("div",{className:"activitypub-profile"},(0,o.createElement)(I,{profile:e,popupStyles:t,userId:r,buttonText:a,buttonSize:i})):(0,o.createElement)("div",{className:"activitypub-profile"},(0,o.createElement)("img",{className:"activitypub-profile__avatar",src:c,alt:u}),(0,o.createElement)("div",{className:"activitypub-profile__content"},(0,o.createElement)("div",{className:"activitypub-profile__name"},u),(0,o.createElement)("div",{className:"activitypub-profile__handle",title:s},s)),(0,o.createElement)(I,{profile:e,popupStyles:t,userId:r,buttonText:a,buttonSize:i}))}function I({profile:e,popupStyles:t,userId:r,buttonText:n,buttonSize:i}){const[l,c]=(0,a.useState)(!1),p=(0,s.sprintf)(/* translators: %s: profile name */ /* translators: %s: profile name */
+(0,s.__)("Follow %s","activitypub"),e?.name);return(0,o.createElement)(o.Fragment,null,(0,o.createElement)(u.Button,{className:"activitypub-profile__follow",onClick:()=>c(!0),"aria-haspopup":"dialog","aria-expanded":l,"aria-label":(0,s.__)("Follow me on the Fediverse","activitypub"),size:i},n),l&&(0,o.createElement)(u.Modal,{className:"activitypub-profile__confirm activitypub__modal",onRequestClose:()=>c(!1),title:p,"aria-label":p,role:"dialog"},(0,o.createElement)(T,{profile:e,userId:r}),(0,o.createElement)("style",null,t)))}function T({profile:e,userId:t}){const{namespace:r}=O(),{webfinger:a}=e,n=(0,s.__)("Follow","activitypub"),i=`/${r}/actors/${t}/remote-follow?resource=`,l=(0,s.__)("Copy and paste my profile into the search field of your favorite fediverse app or server.","activitypub"),c=a.startsWith("@")?a:`@${a}`;return(0,o.createElement)(k,{actionText:n,copyDescription:l,handle:c,resourceUrl:i})}function $({selectedUser:e,style:t,backgroundColor:r,id:n,useId:i=!1,profileData:l=!1,buttonOnly:u=!1,buttonText:p=(0,s.__)("Follow","activitypub"),buttonSize:d="default"}){const[b,y]=(0,a.useState)(C()),_="site"===e?0:e,w=function(e){return v(".apfmd__button-group .components-button",m(e?.elements?.link?.color?.text)||"#111","#fff",m(e?.elements?.link?.[":hover"]?.color?.text)||"#333")}(t),h=i?{id:n}:{};return(0,a.useEffect)((()=>{l?y(C(l)):function(e){const{namespace:t}=O(),r={headers:{Accept:"application/activity+json"},path:`/${t}/actors/${e}`};return c()(r)}(_).then((e=>{y(C(e))}))}),[_,l]),(0,o.createElement)("div",{...h,className:"activitypub-follow-me-block-wrapper"},(0,o.createElement)(f,{selector:`#${n}`,style:t,backgroundColor:r}),(0,o.createElement)(R,{profile:b,userId:_,popupStyles:w,buttonText:p,buttonOnly:u,buttonSize:d}))}let z=1;i()((()=>{[].forEach.call(document.querySelectorAll(".activitypub-follow-me-block-wrapper"),(e=>{const t=JSON.parse(e.dataset.attrs);(0,a.createRoot)(e).render((0,o.createElement)($,{...t,id:"activitypub-follow-me-block-"+z++,useId:!0}))}))}))},20:(e,t,r)=>{var o=r(609),a=Symbol.for("react.element"),n=(Symbol.for("react.fragment"),Object.prototype.hasOwnProperty),i=o.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,l={key:!0,ref:!0,__self:!0,__source:!0};t.jsx=function(e,t,r){var o,c={},u=null,s=null;for(o in void 0!==r&&(u=""+r),void 0!==t.key&&(u=""+t.key),void 0!==t.ref&&(s=t.ref),t)n.call(t,o)&&!l.hasOwnProperty(o)&&(c[o]=t[o]);if(e&&e.defaultProps)for(o in t=e.defaultProps)void 0===c[o]&&(c[o]=t[o]);return{$$typeof:a,type:e,key:u,ref:s,props:c,_owner:i.current}}},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(s=0;s@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@yourusername@example.com
)","activitypub"),{code:(0,o.createElement)("code",null)})),(0,o.createElement)("div",{className:"activitypub-dialog__button-group"},(0,o.createElement)("label",{htmlFor:"remote-profile",className:"screen-reader-text"},(0,c.__)("Enter your ActivityPub profile","activitypub")),(0,o.createElement)("input",{type:"text",id:"remote-profile",value:L,onKeyDown:e=>{"Enter"===e?.code&&j()},onChange:e=>S(e.target.value),"aria-invalid":C===w}),(0,o.createElement)(l.Button,{onClick:j,"aria-label":(0,c.__)("Submit profile","activitypub")},(0,o.createElement)(s,{icon:f}),C)),m&&(0,o.createElement)("div",{className:"activitypub-dialog__remember"},(0,o.createElement)(l.CheckboxControl,{checked:U,label:(0,c.__)("Remember me for easier comments","activitypub"),onChange:()=>{N(!U)}}))))}function C({selectedComment:e,commentId:t}){const{namespace:r}=window._activityPubOptions||{},a=(0,c.__)("Reply","activitypub"),i=`/${r}/comments/${t}/remote-reply?resource=`,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:a,copyDescription:n,handle:e,resourceUrl:i,myProfile:(0,c.__)("Original Comment URL","activitypub"),rememberProfile:!0})}function R({profileURL:e,template:t,commentURL:r,deleteRemoteUser:a}){return(0,o.createElement)(o.Fragment,null,(0,o.createElement)(l.Button,{variant:"link",className:"comment-reply-link activitypub-remote-reply__button",onClick:()=>{const e=t.replace("{uri}",r);window.open(e,"_blank")}},/* translators: %s: profile name */ /* translators: %s: profile name */
+(0,c.sprintf)((0,c.__)("Reply as %s","activitypub"),e)),(0,o.createElement)(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 x({selectedComment:e,commentId:t}){const[r,i]=(0,a.useState)(!1),n=(0,c.__)("Remote Reply","activitypub"),{profileURL:s,template:m,deleteRemoteUser:p}=h(),u=s&&m;return(0,o.createElement)(o.Fragment,null,u?(0,o.createElement)(R,{profileURL:s,template:m,commentURL:e,deleteRemoteUser:p}):(0,o.createElement)(l.Button,{variant:"link",className:"comment-reply-link activitypub-remote-reply__button",onClick:()=>i(!0)},(0,c.__)("Reply on the Fediverse","activitypub")),r&&(0,o.createElement)(l.Modal,{className:"activitypub-remote-reply__modal activitypub__modal",onRequestClose:()=>i(!1),title:n},(0,o.createElement)(C,{selectedComment:e,commentId:t})))}let O=1;n()((()=>{[].forEach.call(document.querySelectorAll(".activitypub-remote-reply"),(e=>{const t=JSON.parse(e.dataset.attrs);(0,a.createRoot)(e).render((0,o.createElement)(x,{...t,id:"activitypub-remote-reply-link-"+O++,useId:!0}))}))}))},20:(e,t,r)=>{var o=r(609),a=Symbol.for("react.element"),i=(Symbol.for("react.fragment"),Object.prototype.hasOwnProperty),n=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)i.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:n.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 i=r[e]={exports:{}};return t[e](i,i.exports,o),i.exports}o.m=t,e=[],o.O=(t,r,a,i)=>{if(!r){var n=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 - ); + sprintf( __( '↬%s', 'activitypub' ), \str_replace( array( 'https://', 'http://' ), '', esc_url( $attrs['url'] ) ) ) + ); + } + + $html .= '
.*?
#is', $data['post_content'], $matches ); + $blocks = \array_map( + function ( $paragraph ) { + return '' . PHP_EOL . $paragraph . PHP_EOL . '' . PHP_EOL; + }, + $matches[0] ?? array() + ); + + $data['post_content'] = \rtrim( \implode( PHP_EOL, $blocks ), PHP_EOL ); + + // Add reply block if it's a reply. + if ( null !== $post->object->inReplyTo ) { + $reply_block = \sprintf( '' . PHP_EOL, \esc_url( $post->object->inReplyTo ) ); + $data['post_content'] = $reply_block . $data['post_content']; + } + + return $data; + } } diff --git a/wp-content/plugins/activitypub/includes/class-cli.php b/wp-content/plugins/activitypub/includes/class-cli.php index 64b4b8ab..b5c9d224 100644 --- a/wp-content/plugins/activitypub/includes/class-cli.php +++ b/wp-content/plugins/activitypub/includes/class-cli.php @@ -7,83 +7,14 @@ namespace Activitypub; -use WP_CLI; -use WP_CLI_Command; +use Activitypub\Collection\Outbox; /** * WP-CLI commands. * * @package Activitypub */ -class Cli extends WP_CLI_Command { - /** - * Check the Plugins Meta-Information. - * - * ## OPTIONS - * - * [--Name] - * The Plugin Name. - * - * [--PluginURI] - * The Plugin URI. - * - * [--Version] - * The Plugin Version. - * - * [--Description] - * The Plugin Description. - * - * [--Author] - * The Plugin Author. - * - * [--AuthorURI] - * The Plugin Author URI. - * - * [--TextDomain] - * The Plugin Text Domain. - * - * [--DomainPath] - * The Plugin Domain Path. - * - * [--Network] - * The Plugin Network. - * - * [--RequiresWP] - * The Plugin Requires at least. - * - * [--RequiresPHP] - * The Plugin Requires PHP. - * - * [--UpdateURI] - * The Plugin Update URI. - * - * See: https://developer.wordpress.org/reference/functions/get_plugin_data/#return - * - * ## EXAMPLES - * - * $ wp webmention meta - * - * $ wp webmention meta --Version - * Version: 1.0.0 - * - * @param array|null $args The arguments. - * @param array|null $assoc_args The associative arguments. - * - * @return void - */ - public function meta( $args, $assoc_args ) { - $plugin_data = get_plugin_meta(); - - if ( $assoc_args ) { - $plugin_data = array_intersect_key( $plugin_data, $assoc_args ); - } else { - WP_CLI::line( __( "ActivityPub Plugin Meta:\n", 'activitypub' ) ); - } - - foreach ( $plugin_data as $key => $value ) { - WP_CLI::line( $key . ': ' . $value ); - } - } +class Cli extends \WP_CLI_Command { /** * Remove the entire blog from the Fediverse. @@ -98,7 +29,7 @@ class Cli extends WP_CLI_Command { * @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' ) ); + \WP_CLI::warning( 'Self-Destructing is not implemented yet.' ); } /** @@ -123,28 +54,27 @@ class Cli extends WP_CLI_Command { * * @synopsis%s
', - \__( 'Your author URL is accessible and supports the required "Accept" header.', 'activitypub' ) - ), - 'actions' => '', - 'test' => 'test_author_url', - ); - - $check = self::is_author_url_accessible(); - - if ( true === $check ) { - return $result; - } - - $result['status'] = 'critical'; - $result['label'] = \__( 'Author URL is not accessible', 'activitypub' ); - $result['badge']['color'] = 'red'; - $result['description'] = \sprintf( - '%s
', - $check->get_error_message() - ); - - return $result; - } - - /** - * System Cron tests. - * - * @return array - */ - public static function test_system_cron() { - $result = array( - 'label' => \__( 'System Task Scheduler configured', 'activitypub' ), - 'status' => 'good', - 'badge' => array( - 'label' => \__( 'ActivityPub', 'activitypub' ), - 'color' => 'green', - ), - 'description' => \sprintf( - '%s
', - \esc_html__( 'You seem to use the System Task Scheduler to process WP_Cron tasks.', 'activitypub' ) - ), - 'actions' => '', - 'test' => 'test_system_cron', - ); - - if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) { - return $result; - } - - $result['status'] = 'recommended'; - $result['label'] = \__( 'System Task Scheduler not configured', 'activitypub' ); - $result['badge']['color'] = 'orange'; - $result['description'] = \sprintf( - '%s
', - \__( 'Enhance your WordPress site’s performance and mitigate potential heavy loads caused by plugins like ActivityPub by setting up a system cron job to run WP Cron. This ensures scheduled tasks are executed consistently and reduces the reliance on website traffic for trigger events.', 'activitypub' ) - ); - $result['actions'] .= sprintf( - '', - esc_url( __( 'https://developer.wordpress.org/plugins/cron/hooking-wp-cron-into-the-system-task-scheduler/', 'activitypub' ) ), - __( 'Learn how to hook the WP-Cron into the System Task Scheduler.', 'activitypub' ), - /* translators: Hidden accessibility text. */ - __( '(opens in a new tab)', 'activitypub' ) - ); - - return $result; - } - - /** - * WebFinger tests. - * - * @return array - */ - public static function test_webfinger() { - $result = array( - 'label' => \__( 'WebFinger endpoint', 'activitypub' ), - 'status' => 'good', - 'badge' => array( - 'label' => \__( 'ActivityPub', 'activitypub' ), - 'color' => 'green', - ), - 'description' => \sprintf( - '%s
', - \__( 'Your WebFinger endpoint is accessible and returns the correct information.', 'activitypub' ) - ), - 'actions' => '', - 'test' => 'test_webfinger', - ); - - $check = self::is_webfinger_endpoint_accessible(); - - if ( true === $check ) { - return $result; - } - - $result['status'] = 'critical'; - $result['label'] = \__( 'WebFinger endpoint is not accessible', 'activitypub' ); - $result['badge']['color'] = 'red'; - $result['description'] = \sprintf( - '%s
', - $check->get_error_message() - ); - - return $result; - } - - /** - * Check if `author_posts_url` is accessible and that request returns correct JSON. - * - * @return bool|WP_Error True if the author URL is accessible, WP_Error otherwise. - */ - public static function is_author_url_accessible() { - $user = \wp_get_current_user(); - $author_url = \get_author_posts_url( $user->ID ); - $reference_author_url = self::get_author_posts_url( $user->ID, $user->user_nicename ); - - // Check for "author" in URL. - if ( $author_url !== $reference_author_url ) { - return new WP_Error( - 'author_url_not_accessible', - \sprintf( - // translators: %s: Author URL. - \__( - 'Your author URL%s
was replaced, this is often done by plugins.',
- 'activitypub'
- ),
- $author_url
- )
- );
- }
-
- // Try to access author URL.
- $response = \wp_remote_get(
- $author_url,
- array(
- 'headers' => array( 'Accept' => 'application/activity+json' ),
- 'redirection' => 0,
- )
- );
-
- if ( \is_wp_error( $response ) ) {
- return new WP_Error(
- 'author_url_not_accessible',
- \sprintf(
- // 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'
- ),
- $author_url
- )
- );
- }
-
- $response_code = \wp_remote_retrieve_response_code( $response );
-
- // 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.
- \__(
- 'Your author URL %s
is redirecting to another page, this is often done by SEO plugins like "Yoast SEO".',
- 'activitypub'
- ),
- $author_url
- )
- );
- }
-
- // 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.
- \__(
- 'Your author URL %s
does not return valid JSON for application/activity+json
. Please check if your hosting supports alternate Accept
headers.',
- 'activitypub'
- ),
- $author_url
- )
- );
- }
-
- return true;
- }
-
- /**
- * Check if WebFinger endpoint is accessible and profile request returns correct JSON
- *
- * @return boolean|WP_Error
- */
- public static function is_webfinger_endpoint_accessible() {
- $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.
- \__(
- 'Your WebFinger endpoint %s
is not accessible. Please check your WordPress setup or permalink structure.',
- 'activitypub'
- ),
- $allowed
- );
- $invalid_response = wp_kses(
- // translators: %s: Author URL.
- \__(
- 'Your WebFinger endpoint %s
does not return valid JSON for application/jrd+json
.',
- 'activitypub'
- ),
- $allowed
- );
-
- $health_messages = array(
- 'webfinger_url_not_accessible' => \sprintf(
- $not_accessible,
- $url->get_error_data()['data']
- ),
- 'webfinger_url_invalid_response' => \sprintf(
- // translators: %s: Author URL.
- $invalid_response,
- $url->get_error_data()['data']
- ),
- );
- $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,
- $url->get_error_data()
- );
- }
-
- return true;
- }
-
- /**
- * Retrieve the URL to the author page for the user with the ID provided.
- *
- * @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.
- *
- * @return string The URL to the author's page.
- */
- 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();
-
- if ( empty( $link ) ) {
- $file = home_url( '/' );
- $link = $file . '?author=' . $auth_id;
- } else {
- if ( '' === $author_nicename ) {
- $user = get_userdata( $author_id );
- if ( ! empty( $user->user_nicename ) ) {
- $author_nicename = $user->user_nicename;
- }
- }
- $link = str_replace( '%author%', $author_nicename, $link );
- $link = home_url( user_trailingslashit( $link ) );
- }
-
- return $link;
- }
-
- /**
- * Static function for generating site debug data when required.
- *
- * @param array $info The debug information to be added to the core information page.
- * @return array The filtered information
- */
- public static function debug_information( $info ) {
- $info['activitypub'] = array(
- 'label' => __( 'ActivityPub', 'activitypub' ),
- 'fields' => array(
- 'webfinger' => array(
- 'label' => __( 'WebFinger Resource', 'activitypub' ),
- 'value' => Webfinger::get_user_resource( wp_get_current_user()->ID ),
- 'private' => true,
- ),
- 'author_url' => array(
- 'label' => __( 'Author URL', 'activitypub' ),
- 'value' => get_author_posts_url( wp_get_current_user()->ID ),
- 'private' => true,
- ),
- 'plugin_version' => array(
- 'label' => __( 'Plugin Version', 'activitypub' ),
- 'value' => get_plugin_version(),
- 'private' => true,
- ),
- ),
- );
-
- return $info;
- }
-}
diff --git a/wp-content/plugins/activitypub/includes/class-http.php b/wp-content/plugins/activitypub/includes/class-http.php
index 133a3469..9f9a8dd0 100644
--- a/wp-content/plugins/activitypub/includes/class-http.php
+++ b/wp-content/plugins/activitypub/includes/class-http.php
@@ -8,7 +8,7 @@
namespace Activitypub;
use WP_Error;
-use Activitypub\Collection\Users;
+use Activitypub\Collection\Actors;
/**
* ActivityPub HTTP Class
@@ -26,6 +26,13 @@ class Http {
* @return array|WP_Error The POST Response or an WP_Error.
*/
public static function post( $url, $body, $user_id ) {
+ /**
+ * Fires before an HTTP POST request is made.
+ *
+ * @param string $url The URL endpoint.
+ * @param string $body The POST body.
+ * @param int $user_id The WordPress User ID.
+ */
\do_action( 'activitypub_pre_http_post', $url, $body, $user_id );
$date = \gmdate( 'D, d M Y H:i:s T' );
@@ -35,7 +42,7 @@ class Http {
$wp_version = get_masked_wp_version();
/**
- * Filter the HTTP headers user agent.
+ * Filters the HTTP headers user agent string.
*
* @param string $user_agent The user agent string.
*/
@@ -59,7 +66,14 @@ class Http {
$code = \wp_remote_retrieve_response_code( $response );
if ( $code >= 400 ) {
- $response = new WP_Error( $code, __( 'Failed HTTP Request', 'activitypub' ), array( 'status' => $code ) );
+ $response = new WP_Error(
+ $code,
+ __( 'Failed HTTP Request', 'activitypub' ),
+ array(
+ 'status' => $code,
+ 'response' => $response,
+ )
+ );
}
/**
@@ -84,6 +98,11 @@ class Http {
* @return array|WP_Error The GET Response or a WP_Error.
*/
public static function get( $url, $cached = false ) {
+ /**
+ * Fires before an HTTP GET request is made.
+ *
+ * @param string $url The URL endpoint.
+ */
\do_action( 'activitypub_pre_http_get', $url );
if ( $cached ) {
@@ -105,19 +124,29 @@ class Http {
}
$date = \gmdate( 'D, d M Y H:i:s T' );
- $signature = Signature::generate_signature( Users::APPLICATION_USER_ID, 'get', $url, $date );
+ $signature = Signature::generate_signature( Actors::APPLICATION_USER_ID, 'get', $url, $date );
$wp_version = get_masked_wp_version();
/**
- * Filter the HTTP headers user agent.
+ * Filters the HTTP headers user agent string.
+ *
+ * This filter allows developers to modify the user agent string that is
+ * sent with HTTP requests.
*
* @param string $user_agent The user agent string.
*/
$user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . \get_bloginfo( 'url' ) );
+ /**
+ * Filters the timeout duration for remote GET requests in ActivityPub.
+ *
+ * @param int $timeout The timeout value in seconds. Default 100 seconds.
+ */
+ $timeout = \apply_filters( 'activitypub_remote_get_timeout', 100 );
+
$args = array(
- 'timeout' => apply_filters( 'activitypub_remote_get_timeout', 100 ),
+ 'timeout' => $timeout,
'limit_response_size' => 1048576,
'redirection' => 3,
'user-agent' => "$user_agent; ActivityPub",
@@ -164,19 +193,25 @@ class Http {
*/
public static function is_tombstone( $url ) {
/**
- * Action before checking if the URL is a tombstone.
+ * Fires 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 );
+ $response = \wp_safe_remote_get( $url, array( 'headers' => array( 'Accept' => 'application/activity+json' ) ) );
$code = \wp_remote_retrieve_response_code( $response );
if ( in_array( (int) $code, array( 404, 410 ), true ) ) {
return true;
}
+ $data = \wp_remote_retrieve_body( $response );
+ $data = \json_decode( $data, true );
+ if ( $data && isset( $data['type'] ) && 'Tombstone' === $data['type'] ) {
+ return true;
+ }
+
return false;
}
@@ -200,24 +235,7 @@ class Http {
* @return array|WP_Error The Object data as array or WP_Error on failure.
*/
public static function get_remote_object( $url_or_object, $cached = true ) {
- if ( is_array( $url_or_object ) ) {
- if ( array_key_exists( 'id', $url_or_object ) ) {
- $url = $url_or_object['id'];
- } elseif ( array_key_exists( 'url', $url_or_object ) ) {
- $url = $url_or_object['url'];
- } else {
- return new WP_Error(
- 'activitypub_no_valid_actor_identifier',
- \__( 'The "actor" identifier is not valid', 'activitypub' ),
- array(
- 'status' => 404,
- 'object' => $url_or_object,
- )
- );
- }
- } else {
- $url = $url_or_object;
- }
+ $url = object_to_uri( $url_or_object );
if ( preg_match( '/^@?' . ACTIVITYPUB_USERNAME_REGEXP . '$/i', $url ) ) {
$url = Webfinger::resolve( $url );
diff --git a/wp-content/plugins/activitypub/includes/class-link.php b/wp-content/plugins/activitypub/includes/class-link.php
index 3c53f3bb..783f1fec 100644
--- a/wp-content/plugins/activitypub/includes/class-link.php
+++ b/wp-content/plugins/activitypub/includes/class-link.php
@@ -29,11 +29,11 @@ class Link {
*/
public static function filter_activity_object( $activity ) {
/* phpcs:ignore Squiz.PHP.CommentedOutCode.Found
- Removed until this is merged: https://github.com/mastodon/mastodon/pull/28629
- if ( ! empty( $activity['summary'] ) ) {
+ Only changed it for Person and Group as long is not merged: https://github.com/mastodon/mastodon/pull/28629
+ */
+ if ( ! empty( $activity['summary'] ) && in_array( $activity['type'], array( 'Person', 'Group' ), true ) ) {
$activity['summary'] = self::the_content( $activity['summary'] );
}
- */
if ( ! empty( $activity['content'] ) ) {
$activity['content'] = self::the_content( $activity['content'] );
@@ -112,6 +112,11 @@ class Link {
$display_class .= 'ellipsis';
}
+ /**
+ * Filters the rel attribute for ActivityPub links.
+ *
+ * @param string $rel The rel attribute string. Default 'nofollow noopener noreferrer'.
+ */
$rel = apply_filters( 'activitypub_link_rel', 'nofollow noopener noreferrer' );
return \sprintf(
diff --git a/wp-content/plugins/activitypub/includes/class-mailer.php b/wp-content/plugins/activitypub/includes/class-mailer.php
new file mode 100644
index 00000000..095d8396
--- /dev/null
+++ b/wp-content/plugins/activitypub/includes/class-mailer.php
@@ -0,0 +1,337 @@
+comment_ID, 'protocol', true );
+
+ if ( 'activitypub' !== $type ) {
+ return $subject;
+ }
+
+ $singular = Comment::get_comment_type_attr( $comment->comment_type, 'singular' );
+
+ if ( ! $singular ) {
+ return $subject;
+ }
+
+ $post = \get_post( $comment->comment_post_ID );
+
+ /* translators: 1: Blog name, 2: Like or Repost, 3: Post title */
+ return \sprintf( \esc_html__( '[%1$s] %2$s: %3$s', 'activitypub' ), \esc_html( get_option( 'blogname' ) ), \esc_html( $singular ), \esc_html( $post->post_title ) );
+ }
+
+ /**
+ * Filter the notification text for Like and Announce notifications.
+ *
+ * @param string $message The default notification text.
+ * @param int|string $comment_id The comment ID.
+ *
+ * @return string The filtered notification text.
+ */
+ public static function comment_notification_text( $message, $comment_id ) {
+ $comment = \get_comment( $comment_id );
+
+ if ( ! $comment ) {
+ return $message;
+ }
+
+ $type = \get_comment_meta( $comment->comment_ID, 'protocol', true );
+
+ if ( 'activitypub' !== $type ) {
+ return $message;
+ }
+
+ $comment_type = Comment::get_comment_type( $comment->comment_type );
+
+ if ( ! $comment_type ) {
+ return $message;
+ }
+
+ $post = \get_post( $comment->comment_post_ID );
+ $comment_author_domain = \gethostbyaddr( $comment->comment_author_IP );
+
+ /* translators: 1: Comment type, 2: Post title */
+ $notify_message = \sprintf( html_entity_decode( esc_html__( 'New %1$s on your post “%2$s”.', 'activitypub' ) ), \esc_html( $comment_type['singular'] ), \esc_html( $post->post_title ) ) . "\r\n\r\n";
+ /* translators: 1: Website name, 2: Website IP address, 3: Website hostname. */
+ $notify_message .= \sprintf( \esc_html__( 'From: %1$s (IP address: %2$s, %3$s)', 'activitypub' ), \esc_html( $comment->comment_author ), \esc_html( $comment->comment_author_IP ), \esc_html( $comment_author_domain ) ) . "\r\n";
+ /* translators: Reaction author URL. */
+ $notify_message .= \sprintf( \esc_html__( 'URL: %s', 'activitypub' ), \esc_url( $comment->comment_author_url ) ) . "\r\n\r\n";
+ /* translators: Comment type label */
+ $notify_message .= \sprintf( \esc_html__( 'You can see all %s on this post here:', 'activitypub' ), \esc_html( $comment_type['label'] ) ) . "\r\n";
+ $notify_message .= \get_permalink( $comment->comment_post_ID ) . '#' . \esc_attr( $comment_type['type'] ) . "\r\n\r\n";
+
+ return $notify_message;
+ }
+
+ /**
+ * Send a notification email for every new follower.
+ *
+ * @param array $activity The activity object.
+ * @param int $user_id The id of the local blog-user.
+ */
+ public static function new_follower( $activity, $user_id ) {
+ if ( $user_id > Actors::BLOG_USER_ID ) {
+ if ( ! \get_user_option( 'activitypub_mailer_new_follower', $user_id ) ) {
+ return;
+ }
+
+ $email = \get_userdata( $user_id )->user_email;
+ $admin_url = '/users.php?page=activitypub-followers-list';
+ } else {
+ if ( '1' !== \get_option( 'activitypub_blog_user_mailer_new_follower', '1' ) ) {
+ return;
+ }
+
+ $email = \get_option( 'admin_email' );
+ $admin_url = '/options-general.php?page=activitypub&tab=followers';
+ }
+
+ $actor = get_remote_metadata_by_actor( $activity['actor'] );
+ if ( ! $actor || \is_wp_error( $actor ) ) {
+ return;
+ }
+
+ if ( empty( $actor['webfinger'] ) ) {
+ $actor['webfinger'] = '@' . ( $actor['preferredUsername'] ?? $actor['name'] ) . '@' . \wp_parse_url( $actor['url'], PHP_URL_HOST );
+ }
+
+ $template_args = array_merge(
+ $actor,
+ array(
+ 'admin_url' => $admin_url,
+ 'user_id' => $user_id,
+ 'stats' => array(
+ 'outbox' => null,
+ 'followers' => null,
+ 'following' => null,
+ ),
+ )
+ );
+
+ foreach ( $template_args['stats'] as $field => $value ) {
+ if ( empty( $actor[ $field ] ) ) {
+ continue;
+ }
+
+ $result = Http::get( $actor[ $field ], true );
+ if ( 200 === \wp_remote_retrieve_response_code( $result ) ) {
+ $body = \json_decode( \wp_remote_retrieve_body( $result ), true );
+ if ( isset( $body['totalItems'] ) ) {
+ $template_args['stats'][ $field ] = $body['totalItems'];
+ }
+ }
+ }
+
+ /* translators: 1: Blog name, 2: Follower name */
+ $subject = \sprintf( \__( '[%1$s] New Follower: %2$s', 'activitypub' ), \get_option( 'blogname' ), $actor['name'] );
+
+ \ob_start();
+ \load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/emails/new-follower.php', false, $template_args );
+ $html_message = \ob_get_clean();
+
+ $alt_function = function ( $mailer ) use ( $actor, $admin_url ) {
+ /* translators: 1: Follower name */
+ $message = \sprintf( \__( 'New Follower: %1$s.', 'activitypub' ), $actor['name'] ) . "\r\n\r\n";
+ /* translators: Follower URL */
+ $message .= \sprintf( \__( 'URL: %s', 'activitypub' ), \esc_url( $actor['url'] ) ) . "\r\n\r\n";
+ $message .= \__( 'You can see all followers here:', 'activitypub' ) . "\r\n";
+ $message .= \esc_url( \admin_url( $admin_url ) ) . "\r\n\r\n";
+ $mailer->{'AltBody'} = $message;
+ };
+ \add_action( 'phpmailer_init', $alt_function );
+
+ \wp_mail( $email, $subject, $html_message, array( 'Content-type: text/html' ) );
+
+ \remove_action( 'phpmailer_init', $alt_function );
+ }
+
+ /**
+ * Send a direct message.
+ *
+ * @param array $activity The activity object.
+ * @param int $user_id The id of the local blog-user.
+ */
+ public static function direct_message( $activity, $user_id ) {
+ if (
+ is_activity_public( $activity ) ||
+ // Only accept messages that have the user in the "to" field.
+ empty( $activity['to'] ) ||
+ ! in_array( Actors::get_by_id( $user_id )->get_id(), (array) $activity['to'], true )
+ ) {
+ return;
+ }
+
+ if ( $user_id > Actors::BLOG_USER_ID ) {
+ if ( ! \get_user_option( 'activitypub_mailer_new_dm', $user_id ) ) {
+ return;
+ }
+
+ $email = \get_userdata( $user_id )->user_email;
+ } else {
+ if ( '1' !== \get_option( 'activitypub_blog_user_mailer_new_dm', '1' ) ) {
+ return;
+ }
+
+ $email = \get_option( 'admin_email' );
+ }
+
+ $actor = get_remote_metadata_by_actor( $activity['actor'] );
+
+ if ( ! $actor || \is_wp_error( $actor ) || empty( $activity['object']['content'] ) ) {
+ return;
+ }
+
+ if ( empty( $actor['webfinger'] ) ) {
+ $actor['webfinger'] = '@' . ( $actor['preferredUsername'] ?? $actor['name'] ) . '@' . \wp_parse_url( $actor['url'], PHP_URL_HOST );
+ }
+
+ $template_args = array(
+ 'activity' => $activity,
+ 'actor' => $actor,
+ 'user_id' => $user_id,
+ );
+
+ /* translators: 1: Blog name, 2 Actor name */
+ $subject = \sprintf( \esc_html__( '[%1$s] Direct Message from: %2$s', 'activitypub' ), \esc_html( \get_option( 'blogname' ) ), \esc_html( $actor['name'] ) );
+
+ \ob_start();
+ \load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/emails/new-dm.php', false, $template_args );
+ $html_message = \ob_get_clean();
+
+ $alt_function = function ( $mailer ) use ( $actor, $activity ) {
+ $content = \html_entity_decode(
+ \wp_strip_all_tags(
+ str_replace( '', PHP_EOL . PHP_EOL, $activity['object']['content'] )
+ ),
+ ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401
+ );
+
+ /* translators: Actor name */
+ $message = \sprintf( \esc_html__( 'New Direct Message: %s', 'activitypub' ), $content ) . "\r\n\r\n";
+ /* translators: Actor name */
+ $message .= \sprintf( \esc_html__( 'From: %s', 'activitypub' ), \esc_html( $actor['name'] ) ) . "\r\n";
+ /* translators: Message URL */
+ $message .= \sprintf( \esc_html__( 'URL: %s', 'activitypub' ), \esc_url( $activity['object']['id'] ) ) . "\r\n\r\n";
+
+ $mailer->{'AltBody'} = $message;
+ };
+ \add_action( 'phpmailer_init', $alt_function );
+
+ \wp_mail( $email, $subject, $html_message, array( 'Content-type: text/html' ) );
+
+ \remove_action( 'phpmailer_init', $alt_function );
+ }
+
+ /**
+ * Send a mention notification.
+ *
+ * @param array $activity The activity object.
+ * @param int $user_id The id of the local blog-user.
+ */
+ public static function mention( $activity, $user_id ) {
+ if (
+ // Only accept messages that have the user in the "cc" field.
+ empty( $activity['cc'] ) ||
+ ! in_array( Actors::get_by_id( $user_id )->get_id(), (array) $activity['cc'], true )
+ ) {
+ return;
+ }
+
+ if ( $user_id > Actors::BLOG_USER_ID ) {
+ if ( ! \get_user_option( 'activitypub_mailer_new_mention', $user_id ) ) {
+ return;
+ }
+
+ $email = \get_userdata( $user_id )->user_email;
+ } else {
+ if ( '1' !== \get_option( 'activitypub_blog_user_mailer_new_mention', '1' ) ) {
+ return;
+ }
+
+ $email = \get_option( 'admin_email' );
+ }
+
+ $actor = get_remote_metadata_by_actor( $activity['actor'] );
+ if ( \is_wp_error( $actor ) ) {
+ return;
+ }
+
+ if ( empty( $actor['webfinger'] ) ) {
+ $actor['webfinger'] = '@' . ( $actor['preferredUsername'] ?? $actor['name'] ) . '@' . \wp_parse_url( $actor['url'], PHP_URL_HOST );
+ }
+
+ $template_args = array(
+ 'activity' => $activity,
+ 'actor' => $actor,
+ 'user_id' => $user_id,
+ );
+
+ /* translators: 1: Blog name, 2 Actor name */
+ $subject = \sprintf( \esc_html__( '[%1$s] Mention from: %2$s', 'activitypub' ), \esc_html( \get_option( 'blogname' ) ), \esc_html( $actor['name'] ) );
+
+ \ob_start();
+ \load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/emails/new-mention.php', false, $template_args );
+ $html_message = \ob_get_clean();
+
+ $alt_function = function ( $mailer ) use ( $actor, $activity ) {
+ $content = \html_entity_decode(
+ \wp_strip_all_tags(
+ str_replace( '', PHP_EOL . PHP_EOL, $activity['object']['content'] )
+ ),
+ ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401
+ );
+
+ /* translators: Message content */
+ $message = \sprintf( \esc_html__( 'New Mention: %s', 'activitypub' ), $content ) . "\r\n\r\n";
+ /* translators: Actor name */
+ $message .= \sprintf( \esc_html__( 'From: %s', 'activitypub' ), \esc_html( $actor['name'] ) ) . "\r\n";
+ /* translators: Message URL */
+ $message .= \sprintf( \esc_html__( 'URL: %s', 'activitypub' ), \esc_url( $activity['object']['id'] ) ) . "\r\n\r\n";
+
+ $mailer->{'AltBody'} = $message;
+ };
+ \add_action( 'phpmailer_init', $alt_function );
+
+ \wp_mail( $email, $subject, $html_message, array( 'Content-type: text/html' ) );
+
+ \remove_action( 'phpmailer_init', $alt_function );
+ }
+}
diff --git a/wp-content/plugins/activitypub/includes/class-mention.php b/wp-content/plugins/activitypub/includes/class-mention.php
index 73f1010c..93ada0bf 100644
--- a/wp-content/plugins/activitypub/includes/class-mention.php
+++ b/wp-content/plugins/activitypub/includes/class-mention.php
@@ -82,7 +82,7 @@ class Mention {
$url = isset( $metadata['url'] ) ? object_to_uri( $metadata['url'] ) : object_to_uri( $metadata['id'] );
- return \sprintf( '@%s', esc_url( $url ), esc_html( $username ) );
+ return \sprintf( '@%2$s', esc_url( $url ), esc_html( $username ) );
}
return $result[0];
diff --git a/wp-content/plugins/activitypub/includes/class-migration.php b/wp-content/plugins/activitypub/includes/class-migration.php
index ca4353fd..9132f6b6 100644
--- a/wp-content/plugins/activitypub/includes/class-migration.php
+++ b/wp-content/plugins/activitypub/includes/class-migration.php
@@ -7,7 +7,11 @@
namespace Activitypub;
+use Activitypub\Collection\Actors;
+use Activitypub\Collection\Extra_Fields;
use Activitypub\Collection\Followers;
+use Activitypub\Collection\Outbox;
+use Activitypub\Transformer\Factory;
/**
* ActivityPub Migration Class
@@ -20,6 +24,8 @@ class Migration {
*/
public static function init() {
\add_action( 'activitypub_migrate', array( self::class, 'async_migration' ) );
+ \add_action( 'activitypub_upgrade', array( self::class, 'async_upgrade' ), 10, 99 );
+ \add_action( 'activitypub_update_comment_counts', array( self::class, 'update_comment_counts' ), 10, 2 );
self::maybe_migrate();
}
@@ -30,10 +36,14 @@ class Migration {
* This is the version that the database structure will be updated to.
* It is the same as the plugin version.
*
+ * @deprecated 4.2.0 Use constant ACTIVITYPUB_PLUGIN_VERSION directly.
+ *
* @return string The target version.
*/
public static function get_target_version() {
- return get_plugin_version();
+ _deprecated_function( __FUNCTION__, '4.2.0', 'ACTIVITYPUB_PLUGIN_VERSION' );
+
+ return ACTIVITYPUB_PLUGIN_VERSION;
}
/**
@@ -47,9 +57,20 @@ class Migration {
/**
* Locks the database migration process to prevent simultaneous migrations.
+ *
+ * @return bool|int True if the lock was successful, timestamp of existing lock otherwise.
*/
public static function lock() {
- \update_option( 'activitypub_migration_lock', \time() );
+ global $wpdb;
+
+ // Try to lock.
+ $lock_result = (bool) $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO `$wpdb->options` ( `option_name`, `option_value`, `autoload` ) VALUES (%s, %s, 'no') /* LOCK */", 'activitypub_migration_lock', \time() ) ); // phpcs:ignore WordPress.DB
+
+ if ( ! $lock_result ) {
+ $lock_result = \get_option( 'activitypub_migration_lock' );
+ }
+
+ return $lock_result;
}
/**
@@ -87,9 +108,9 @@ class Migration {
* @return bool True if the database structure is up to date, false otherwise.
*/
public static function is_latest_version() {
- return (bool) version_compare(
+ return (bool) \version_compare(
self::get_version(),
- self::get_target_version(),
+ ACTIVITYPUB_PLUGIN_VERSION,
'=='
);
}
@@ -110,33 +131,91 @@ class Migration {
$version_from_db = self::get_version();
- // Check for inital migration.
+ // Check for initial migration.
if ( ! $version_from_db ) {
self::add_default_settings();
- $version_from_db = self::get_target_version();
+ $version_from_db = ACTIVITYPUB_PLUGIN_VERSION;
}
// Schedule the async migration.
if ( ! \wp_next_scheduled( 'activitypub_migrate', $version_from_db ) ) {
\wp_schedule_single_event( \time(), 'activitypub_migrate', array( $version_from_db ) );
}
- if ( version_compare( $version_from_db, '0.17.0', '<' ) ) {
+ if ( \version_compare( $version_from_db, '0.17.0', '<' ) ) {
self::migrate_from_0_16();
}
- if ( version_compare( $version_from_db, '1.3.0', '<' ) ) {
+ if ( \version_compare( $version_from_db, '1.3.0', '<' ) ) {
self::migrate_from_1_2_0();
}
- if ( version_compare( $version_from_db, '2.1.0', '<' ) ) {
+ if ( \version_compare( $version_from_db, '2.1.0', '<' ) ) {
self::migrate_from_2_0_0();
}
- if ( version_compare( $version_from_db, '2.3.0', '<' ) ) {
+ if ( \version_compare( $version_from_db, '2.3.0', '<' ) ) {
self::migrate_from_2_2_0();
}
- if ( version_compare( $version_from_db, '3.0.0', '<' ) ) {
+ if ( \version_compare( $version_from_db, '3.0.0', '<' ) ) {
self::migrate_from_2_6_0();
}
+ if ( \version_compare( $version_from_db, '4.0.0', '<' ) ) {
+ self::migrate_to_4_0_0();
+ }
+ if ( \version_compare( $version_from_db, '4.1.0', '<' ) ) {
+ self::migrate_to_4_1_0();
+ }
+ if ( \version_compare( $version_from_db, '4.5.0', '<' ) ) {
+ \wp_schedule_single_event( \time() + MINUTE_IN_SECONDS, 'activitypub_update_comment_counts' );
+ }
+ if ( \version_compare( $version_from_db, '4.7.1', '<' ) ) {
+ self::migrate_to_4_7_1();
+ }
+ if ( \version_compare( $version_from_db, '4.7.2', '<' ) ) {
+ self::migrate_to_4_7_2();
+ }
+ if ( \version_compare( $version_from_db, '4.7.3', '<' ) ) {
+ add_action( 'init', 'flush_rewrite_rules', 20 );
+ }
+ if ( \version_compare( $version_from_db, '5.0.0', '<' ) ) {
+ Scheduler::register_schedules();
+ \wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'create_post_outbox_items' ) );
+ \wp_schedule_single_event( \time() + 15, 'activitypub_upgrade', array( 'create_comment_outbox_items' ) );
+ add_action( 'init', 'flush_rewrite_rules', 20 );
+ }
+ if ( \version_compare( $version_from_db, '5.2.0', '<' ) ) {
+ Scheduler::register_schedules();
+ }
+ if ( \version_compare( $version_from_db, '5.4.0', '<' ) ) {
+ \wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'update_actor_json_slashing' ) );
+ \wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'update_comment_author_emails' ) );
+ \add_action( 'init', 'flush_rewrite_rules', 20 );
+ }
+ if ( \version_compare( $version_from_db, '5.7.0', '<' ) ) {
+ self::delete_mastodon_api_orphaned_extra_fields();
+ }
+ if ( \version_compare( $version_from_db, '5.8.0', '<' ) ) {
+ self::update_notification_options();
+ }
- update_option( 'activitypub_db_version', self::get_target_version() );
+ /*
+ * Add new update routines above this comment. ^
+ *
+ * Use 'unreleased' as the version number for new migrations and add tests for the callback directly.
+ * The release script will automatically replace it with the actual version number.
+ * Example:
+ *
+ * if ( \version_compare( $version_from_db, 'unreleased', '<' ) ) {
+ * // Update routine.
+ * }
+ */
+
+ /**
+ * Fires when the system has to be migrated.
+ *
+ * @param string $version_from_db The version from which to migrate.
+ * @param string $target_version The target version to migrate to.
+ */
+ \do_action( 'activitypub_migrate', $version_from_db, ACTIVITYPUB_PLUGIN_VERSION );
+
+ \update_option( 'activitypub_db_version', ACTIVITYPUB_PLUGIN_VERSION );
self::unlock();
}
@@ -147,11 +226,43 @@ class Migration {
* @param string $version_from_db The version from which to migrate.
*/
public static function async_migration( $version_from_db ) {
- if ( version_compare( $version_from_db, '1.0.0', '<' ) ) {
+ if ( \version_compare( $version_from_db, '1.0.0', '<' ) ) {
self::migrate_from_0_17();
}
}
+ /**
+ * Asynchronously runs upgrade routines.
+ *
+ * @param callable $callback Callable upgrade routine. Must be a method of this class.
+ * @params mixed ...$args Optional. Parameters that get passed to the callback.
+ */
+ public static function async_upgrade( $callback ) {
+ $args = \func_get_args();
+
+ // Bail if the existing lock is still valid.
+ if ( self::is_locked() ) {
+ \wp_schedule_single_event( time() + MINUTE_IN_SECONDS, 'activitypub_upgrade', $args );
+ return;
+ }
+
+ self::lock();
+
+ $callback = array_shift( $args ); // Remove $callback from arguments.
+ $next = \call_user_func_array( array( self::class, $callback ), $args );
+
+ self::unlock();
+
+ if ( ! empty( $next ) ) {
+ // Schedule the next run, adding the result to the arguments.
+ \wp_schedule_single_event(
+ \time() + 30,
+ 'activitypub_upgrade',
+ \array_merge( array( $callback ), \array_values( $next ) )
+ );
+ }
+ }
+
/**
* Updates the custom template to use shortcodes instead of the deprecated templates.
*/
@@ -262,6 +373,369 @@ class Migration {
self::update_options_key( 'activitypub_blog_user_identifier', 'activitypub_blog_identifier' );
}
+ /**
+ * * Update actor-mode settings.
+ * * Get the ID of the latest blog post and save it to the options table.
+ */
+ private static function migrate_to_4_0_0() {
+ $latest_post_id = 0;
+
+ // Get the ID of the latest blog post and save it to the options table.
+ $latest_post = get_posts(
+ array(
+ 'numberposts' => 1,
+ 'orderby' => 'ID',
+ 'order' => 'DESC',
+ 'post_type' => 'any',
+ 'post_status' => 'publish',
+ )
+ );
+
+ if ( $latest_post ) {
+ $latest_post_id = $latest_post[0]->ID;
+ }
+
+ \update_option( 'activitypub_last_post_with_permalink_as_id', $latest_post_id );
+
+ $users = \get_users(
+ array(
+ 'capability__in' => array( 'activitypub' ),
+ )
+ );
+
+ foreach ( $users as $user ) {
+ $followers = Followers::get_followers( $user->ID );
+
+ if ( $followers ) {
+ \update_user_option( $user->ID, 'activitypub_use_permalink_as_id', '1' );
+ }
+ }
+
+ $followers = Followers::get_followers( Actors::BLOG_USER_ID );
+
+ if ( $followers ) {
+ \update_option( 'activitypub_use_permalink_as_id_for_blog', '1' );
+ }
+
+ self::migrate_actor_mode();
+ }
+
+ /**
+ * Upate to 4.1.0
+ *
+ * * Migrate the `activitypub_post_content_type` to only use `activitypub_custom_post_content`.
+ */
+ public static function migrate_to_4_1_0() {
+ $content_type = \get_option( 'activitypub_post_content_type' );
+
+ switch ( $content_type ) {
+ case 'excerpt':
+ $template = "[ap_excerpt]\n\n[ap_permalink type=\"html\"]";
+ break;
+ case 'title':
+ $template = "[ap_title type=\"html\"]\n\n[ap_permalink type=\"html\"]";
+ break;
+ case 'content':
+ $template = "[ap_content]\n\n[ap_permalink type=\"html\"]\n\n[ap_hashtags]";
+ break;
+ case 'custom':
+ $template = \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT );
+ break;
+ default:
+ $template = ACTIVITYPUB_CUSTOM_POST_CONTENT;
+ break;
+ }
+
+ \update_option( 'activitypub_custom_post_content', $template );
+
+ \delete_option( 'activitypub_post_content_type' );
+
+ $object_type = \get_option( 'activitypub_object_type', false );
+ if ( ! $object_type ) {
+ \update_option( 'activitypub_object_type', 'note' );
+ }
+
+ // Clean up empty visibility meta.
+ global $wpdb;
+ $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
+ "DELETE FROM $wpdb->postmeta
+ WHERE meta_key = 'activitypub_content_visibility'
+ AND (meta_value IS NULL OR meta_value = '')"
+ );
+ }
+
+ /**
+ * Updates post meta keys to be prefixed with an underscore.
+ */
+ public static function migrate_to_4_7_1() {
+ global $wpdb;
+
+ $meta_keys = array(
+ 'activitypub_actor_json',
+ 'activitypub_canonical_url',
+ 'activitypub_errors',
+ 'activitypub_inbox',
+ 'activitypub_user_id',
+ );
+
+ foreach ( $meta_keys as $meta_key ) {
+ // phpcs:ignore WordPress.DB
+ $wpdb->update( $wpdb->postmeta, array( 'meta_key' => '_' . $meta_key ), array( 'meta_key' => $meta_key ) );
+ }
+ }
+
+ /**
+ * Clears the post cache for Followers, we should have done this in 4.7.1 when we renamed those keys.
+ */
+ public static function migrate_to_4_7_2() {
+ global $wpdb;
+ // phpcs:ignore WordPress.DB
+ $followers = $wpdb->get_col(
+ $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_type = %s", Followers::POST_TYPE )
+ );
+ foreach ( $followers as $id ) {
+ clean_post_cache( $id );
+ }
+ }
+
+ /**
+ * Update comment counts for posts in batches.
+ *
+ * @see Comment::pre_wp_update_comment_count_now()
+ * @param int $batch_size Optional. Number of posts to process per batch. Default 100.
+ * @param int $offset Optional. Number of posts to skip. Default 0.
+ */
+ public static function update_comment_counts( $batch_size = 100, $offset = 0 ) {
+ global $wpdb;
+
+ // Bail if the existing lock is still valid.
+ if ( self::is_locked() ) {
+ \wp_schedule_single_event(
+ time() + ( 5 * MINUTE_IN_SECONDS ),
+ 'activitypub_update_comment_counts',
+ array(
+ 'batch_size' => $batch_size,
+ 'offset' => $offset,
+ )
+ );
+ return;
+ }
+
+ self::lock();
+
+ Comment::register_comment_types();
+ $comment_types = Comment::get_comment_type_slugs();
+ $type_inclusion = "AND comment_type IN ('" . implode( "','", $comment_types ) . "')";
+
+ // Get and process this batch.
+ $post_ids = $wpdb->get_col( // phpcs:ignore WordPress.DB
+ $wpdb->prepare(
+ // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+ "SELECT DISTINCT comment_post_ID FROM {$wpdb->comments} WHERE comment_approved = '1' {$type_inclusion} ORDER BY comment_post_ID LIMIT %d OFFSET %d",
+ $batch_size,
+ $offset
+ )
+ );
+
+ foreach ( $post_ids as $post_id ) {
+ \wp_update_comment_count_now( $post_id );
+ }
+
+ if ( count( $post_ids ) === $batch_size ) {
+ // Schedule next batch.
+ \wp_schedule_single_event(
+ time() + MINUTE_IN_SECONDS,
+ 'activitypub_update_comment_counts',
+ array(
+ 'batch_size' => $batch_size,
+ 'offset' => $offset + $batch_size,
+ )
+ );
+ }
+
+ self::unlock();
+ }
+
+ /**
+ * Create outbox items for posts in batches.
+ *
+ * @param int $batch_size Optional. Number of posts to process per batch. Default 50.
+ * @param int $offset Optional. Number of posts to skip. Default 0.
+ * @return array|null Array with batch size and offset if there are more posts to process, null otherwise.
+ */
+ public static function create_post_outbox_items( $batch_size = 50, $offset = 0 ) {
+ $posts = \get_posts(
+ array(
+ // our own `ap_outbox` will be excluded from `any` by virtue of its `exclude_from_search` arg.
+ 'post_type' => 'any',
+ 'posts_per_page' => $batch_size,
+ 'offset' => $offset,
+ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
+ 'meta_query' => array(
+ array(
+ 'key' => 'activitypub_status',
+ 'value' => 'federated',
+ ),
+ ),
+ )
+ );
+
+ // Avoid multiple queries for post meta.
+ \update_postmeta_cache( \wp_list_pluck( $posts, 'ID' ) );
+
+ foreach ( $posts as $post ) {
+ $visibility = \get_post_meta( $post->ID, 'activitypub_content_visibility', true );
+
+ self::add_to_outbox( $post, 'Create', $post->post_author, $visibility );
+
+ // Add Update activity when the post has been modified.
+ if ( $post->post_modified !== $post->post_date ) {
+ self::add_to_outbox( $post, 'Update', $post->post_author, $visibility );
+ }
+ }
+
+ if ( count( $posts ) === $batch_size ) {
+ return array(
+ 'batch_size' => $batch_size,
+ 'offset' => $offset + $batch_size,
+ );
+ }
+
+ return null;
+ }
+
+ /**
+ * Create outbox items for comments in batches.
+ *
+ * @param int $batch_size Optional. Number of posts to process per batch. Default 50.
+ * @param int $offset Optional. Number of posts to skip. Default 0.
+ * @return array|null Array with batch size and offset if there are more posts to process, null otherwise.
+ */
+ public static function create_comment_outbox_items( $batch_size = 50, $offset = 0 ) {
+ $comments = \get_comments(
+ array(
+ 'author__not_in' => array( 0 ), // Limit to comments by registered users.
+ 'number' => $batch_size,
+ 'offset' => $offset,
+ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
+ 'meta_query' => array(
+ array(
+ 'key' => 'activitypub_status',
+ 'value' => 'federated',
+ ),
+ ),
+ )
+ );
+
+ foreach ( $comments as $comment ) {
+ self::add_to_outbox( $comment, 'Create', $comment->user_id );
+ }
+
+ if ( count( $comments ) === $batch_size ) {
+ return array(
+ 'batch_size' => $batch_size,
+ 'offset' => $offset + $batch_size,
+ );
+ }
+
+ return null;
+ }
+
+ /**
+ * Update _activitypub_actor_json meta values to ensure they are properly slashed.
+ *
+ * @param int $batch_size Optional. Number of meta values to process per batch. Default 100.
+ * @param int $offset Optional. Number of meta values to skip. Default 0.
+ * @return array|null Array with batch size and offset if there are more meta values to process, null otherwise.
+ */
+ public static function update_actor_json_slashing( $batch_size = 100, $offset = 0 ) {
+ global $wpdb;
+
+ // phpcs:ignore WordPress.DB.DirectDatabaseQuery
+ $meta_values = $wpdb->get_results(
+ $wpdb->prepare(
+ "SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = '_activitypub_actor_json' LIMIT %d OFFSET %d",
+ $batch_size,
+ $offset
+ )
+ );
+
+ foreach ( $meta_values as $meta ) {
+ $json = \json_decode( $meta->meta_value, true );
+
+ // If json_decode fails, try adding slashes.
+ if ( null === $json && \json_last_error() !== JSON_ERROR_NONE ) {
+ $escaped_value = \preg_replace( '#\\\\(?!["\\\\/bfnrtu])#', '\\\\\\\\', $meta->meta_value );
+ $json = \json_decode( $escaped_value, true );
+
+ // Update the meta if json_decode succeeds with slashes.
+ if ( null !== $json && \json_last_error() === JSON_ERROR_NONE ) {
+ \update_post_meta( $meta->post_id, '_activitypub_actor_json', \wp_slash( $escaped_value ) );
+ }
+ }
+ }
+
+ if ( \count( $meta_values ) === $batch_size ) {
+ return array(
+ 'batch_size' => $batch_size,
+ 'offset' => $offset + $batch_size,
+ );
+ }
+
+ return null;
+ }
+
+ /**
+ * Update comment author emails with webfinger addresses for ActivityPub comments.
+ *
+ * @param int $batch_size Optional. Number of comments to process per batch. Default 50.
+ * @param int $offset Optional. Number of comments to skip. Default 0.
+ * @return array|null Array with batch size and offset if there are more comments to process, null otherwise.
+ */
+ public static function update_comment_author_emails( $batch_size = 50, $offset = 0 ) {
+ $comments = \get_comments(
+ array(
+ 'number' => $batch_size,
+ 'offset' => $offset,
+ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
+ 'meta_query' => array(
+ array(
+ 'key' => 'protocol',
+ 'value' => 'activitypub',
+ ),
+ ),
+ )
+ );
+
+ foreach ( $comments as $comment ) {
+ $comment_author_url = $comment->comment_author_url;
+ if ( empty( $comment_author_url ) ) {
+ continue;
+ }
+
+ $webfinger = Webfinger::uri_to_acct( $comment_author_url );
+ if ( \is_wp_error( $webfinger ) ) {
+ continue;
+ }
+
+ \wp_update_comment(
+ array(
+ 'comment_ID' => $comment->comment_ID,
+ 'comment_author_email' => \str_replace( 'acct:', '', $webfinger ),
+ )
+ );
+ }
+
+ if ( count( $comments ) === $batch_size ) {
+ return array(
+ 'batch_size' => $batch_size,
+ 'offset' => $offset + $batch_size,
+ );
+ }
+
+ return null;
+ }
+
/**
* Set the defaults needed for the plugin to work.
*
@@ -269,6 +743,41 @@ class Migration {
*/
public static function add_default_settings() {
self::add_activitypub_capability();
+ self::add_default_extra_field();
+ }
+
+ /**
+ * Add an activity to the outbox without federating it.
+ *
+ * @param \WP_Post|\WP_Comment $comment The comment or post object.
+ * @param string $activity_type The type of activity.
+ * @param int $user_id The user ID.
+ * @param string $visibility Optional. The visibility of the content. Default 'public'.
+ */
+ private static function add_to_outbox( $comment, $activity_type, $user_id, $visibility = ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC ) {
+ $transformer = Factory::get_transformer( $comment );
+ if ( ! $transformer || \is_wp_error( $transformer ) ) {
+ return;
+ }
+
+ $activity = $transformer->to_activity( $activity_type );
+ if ( ! $activity || \is_wp_error( $activity ) ) {
+ return;
+ }
+
+ // If the user is disabled, fall back to the blog user when available.
+ if ( ! user_can_activitypub( $user_id ) ) {
+ if ( user_can_activitypub( Actors::BLOG_USER_ID ) ) {
+ $user_id = Actors::BLOG_USER_ID;
+ } else {
+ return;
+ }
+ }
+
+ $post_id = Outbox::add( $activity, $user_id, $visibility );
+
+ // Immediately set to publish, no federation needed.
+ \wp_publish_post( $post_id );
}
/**
@@ -288,6 +797,43 @@ class Migration {
}
}
+ /**
+ * Add a default extra field for the user.
+ */
+ private static function add_default_extra_field() {
+ $users = \get_users(
+ array(
+ 'capability__in' => array( 'activitypub' ),
+ )
+ );
+
+ $title = \__( 'Powered by', 'activitypub' );
+ $content = 'WordPress';
+
+ // Add a default extra field for each user.
+ foreach ( $users as $user ) {
+ \wp_insert_post(
+ array(
+ 'post_type' => Extra_Fields::USER_POST_TYPE,
+ 'post_author' => $user->ID,
+ 'post_status' => 'publish',
+ 'post_title' => $title,
+ 'post_content' => $content,
+ )
+ );
+ }
+
+ \wp_insert_post(
+ array(
+ 'post_type' => Extra_Fields::BLOG_POST_TYPE,
+ 'post_author' => 0,
+ 'post_status' => 'publish',
+ 'post_title' => $title,
+ 'post_content' => $content,
+ )
+ );
+ }
+
/**
* Rename meta keys.
*
@@ -323,4 +869,76 @@ class Migration {
array( '%s' )
);
}
+
+ /**
+ * Migrate the actor mode settings.
+ */
+ public static function migrate_actor_mode() {
+ $blog_profile = \get_option( 'activitypub_enable_blog_user', '0' );
+ $author_profiles = \get_option( 'activitypub_enable_users', '1' );
+
+ if (
+ '1' === $blog_profile &&
+ '1' === $author_profiles
+ ) {
+ \update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_AND_BLOG_MODE );
+ } elseif (
+ '1' === $blog_profile &&
+ '1' !== $author_profiles
+ ) {
+ \update_option( 'activitypub_actor_mode', ACTIVITYPUB_BLOG_MODE );
+ } elseif (
+ '1' !== $blog_profile &&
+ '1' === $author_profiles
+ ) {
+ \update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE );
+ }
+ }
+
+ /**
+ * Deletes user extra fields where the author is the blog user.
+ *
+ * These extra fields were created when the Enable Mastodon Apps integration passed
+ * an author_url instead of a user_id to the mastodon_api_account filter. This caused
+ * Extra_Fields::default_actor_extra_fields() to run but fail to cache the fact it ran
+ * for non-existent users. The result is a number of user extra fields with no author.
+ *
+ * @ticket https://github.com/Automattic/wordpress-activitypub/pull/1554
+ */
+ public static function delete_mastodon_api_orphaned_extra_fields() {
+ global $wpdb;
+
+ // phpcs:ignore WordPress.DB.DirectDatabaseQuery
+ $wpdb->delete(
+ $wpdb->posts,
+ array(
+ 'post_type' => Extra_Fields::USER_POST_TYPE,
+ 'post_author' => Actors::BLOG_USER_ID,
+ )
+ );
+ }
+
+ /**
+ * Update notification options.
+ */
+ public static function update_notification_options() {
+ $new_dm = \get_option( 'activitypub_mailer_new_dm', '1' );
+ $new_follower = \get_option( 'activitypub_mailer_new_follower', '1' );
+
+ // Add the blog user notification options.
+ \add_option( 'activitypub_blog_user_mailer_new_dm', $new_dm );
+ \add_option( 'activitypub_blog_user_mailer_new_follower', $new_follower );
+ \add_option( 'activitypub_blog_user_mailer_new_mention', '1' );
+
+ // Add the actor notification options.
+ foreach ( Actors::get_collection() as $actor ) {
+ \update_user_option( $actor->get__id(), 'activitypub_mailer_new_dm', $new_dm );
+ \update_user_option( $actor->get__id(), 'activitypub_mailer_new_follower', $new_follower );
+ \update_user_option( $actor->get__id(), 'activitypub_mailer_new_mention', '1' );
+ }
+
+ // Delete the old notification options.
+ \delete_option( 'activitypub_mailer_new_dm' );
+ \delete_option( 'activitypub_mailer_new_follower' );
+ }
}
diff --git a/wp-content/plugins/activitypub/includes/class-move.php b/wp-content/plugins/activitypub/includes/class-move.php
new file mode 100644
index 00000000..18e54ad4
--- /dev/null
+++ b/wp-content/plugins/activitypub/includes/class-move.php
@@ -0,0 +1,313 @@
+get__id() > 0 ) {
+ \update_user_option( $user->get__id(), 'activitypub_moved_to', $to );
+ } else {
+ \update_option( 'activitypub_blog_user_moved_to', $to );
+ }
+
+ $response = Http::get_remote_object( $to );
+
+ if ( \is_wp_error( $response ) ) {
+ return $response;
+ }
+
+ $target_actor = new Actor();
+ $target_actor->from_array( $response );
+
+ // Check if the `Move` Activity is valid.
+ $also_known_as = $target_actor->get_also_known_as() ?? array();
+ if ( ! in_array( $from, $also_known_as, true ) ) {
+ return new \WP_Error( 'invalid_target', __( 'Invalid target', 'activitypub' ) );
+ }
+
+ $activity = new Activity();
+ $activity->set_type( 'Move' );
+ $activity->set_actor( $user->get_id() );
+ $activity->set_origin( $user->get_id() );
+ $activity->set_object( $user->get_id() );
+ $activity->set_target( $target_actor->get_id() );
+
+ // Add to outbox.
+ return add_to_outbox( $activity, null, $user->get__id(), ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC );
+ }
+
+ /**
+ * Internal Move.
+ *
+ * Move an ActivityPub Actor from one location (internal) to another (internal).
+ *
+ * This helps migrating abandoned profiles to `Move` to other profiles:
+ *
+ * `Move::internally( 'https://example.com/?author=123', 'https://example.com/?author=321' );`
+ *
+ * ... or to change Actor-IDs like:
+ *
+ * `Move::internally( 'https://example.com/author/foo', 'https://example.com/?author=123' );`
+ *
+ * @param string $from The current account URL.
+ * @param string $to The new account URL.
+ *
+ * @return int|bool|\WP_Error The ID of the outbox item or false or WP_Error on failure.
+ */
+ public static function internally( $from, $to ) {
+ $user = Actors::get_by_various( $from );
+
+ if ( \is_wp_error( $user ) ) {
+ return $user;
+ }
+
+ // Add the old account URL to alsoKnownAs.
+ if ( $user->get__id() > 0 ) {
+ self::update_user_also_known_as( $user->get__id(), $from );
+ \update_user_option( $user->get__id(), 'activitypub_moved_to', $to );
+ } else {
+ self::update_blog_also_known_as( $from );
+ \update_option( 'activitypub_blog_user_moved_to', $to );
+ }
+
+ // check if `$from` is a URL or an ID.
+ if ( \filter_var( $from, FILTER_VALIDATE_URL ) ) {
+ $actor = $from;
+ } else {
+ $actor = $user->get_id();
+ }
+
+ $activity = new Activity();
+ $activity->set_type( 'Move' );
+ $activity->set_actor( $actor );
+ $activity->set_origin( $actor );
+ $activity->set_object( $actor );
+ $activity->set_target( $to );
+
+ return add_to_outbox( $activity, null, $user->get__id(), ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC );
+ }
+
+ /**
+ * Update the alsoKnownAs property of a user.
+ *
+ * @param int $user_id The user ID.
+ * @param string $from The current account URL.
+ */
+ private static function update_user_also_known_as( $user_id, $from ) {
+ // phpcs:ignore Universal.Operators.DisallowShortTernary.Found
+ $also_known_as = \get_user_option( 'activitypub_also_known_as', $user_id ) ?: array();
+ $also_known_as[] = $from;
+
+ \update_user_option( $user_id, 'activitypub_also_known_as', $also_known_as );
+ }
+
+ /**
+ * Update the alsoKnownAs property of the blog.
+ *
+ * @param string $from The current account URL.
+ */
+ private static function update_blog_also_known_as( $from ) {
+ $also_known_as = \get_option( 'activitypub_blog_user_also_known_as', array() );
+ $also_known_as[] = $from;
+
+ \update_option( 'activitypub_blog_user_also_known_as', $also_known_as );
+ }
+
+ /**
+ * Change domain for all ActivityPub Actors.
+ *
+ * This method handles domain migration according to the ActivityPub Data Portability spec.
+ * It stores the old host and calls Move::internally for each available profile.
+ * It also caches the JSON representation of the old Actor for future lookups.
+ *
+ * @param string $from The old domain.
+ * @param string $to The new domain.
+ *
+ * @return array Array of results from Move::internally calls.
+ */
+ public static function change_domain( $from, $to ) {
+ // Get all actors that need to be migrated.
+ $actors = Actors::get_all();
+
+ $results = array();
+ $to_host = \wp_parse_url( $to, \PHP_URL_HOST );
+ $from_host = \wp_parse_url( $from, \PHP_URL_HOST );
+
+ // Store the old host for future reference.
+ \update_option( 'activitypub_old_host', $from_host );
+
+ // Process each actor.
+ foreach ( $actors as $actor ) {
+ $actor_id = $actor->get_id();
+
+ // Replace the new host with the old host in the actor ID.
+ $old_actor_id = str_replace( $to_host, $from_host, $actor_id );
+
+ // Call Move::internally for this actor.
+ $result = self::internally( $old_actor_id, $actor_id );
+
+ if ( \is_wp_error( $result ) ) {
+ // Log the error and continue with the next actor.
+ Debug::write_log( 'Error moving actor: ' . $actor_id . ' - ' . $result->get_error_message() );
+ continue;
+ }
+
+ $json = str_replace( $to_host, $from_host, $actor->to_json() );
+
+ // Save the current actor data after migration.
+ if ( $actor instanceof Blog ) {
+ \update_option( 'activitypub_blog_user_old_host_data', $json, false );
+ } else {
+ \update_user_option( $actor->get__id(), 'activitypub_old_host_data', $json, false );
+ }
+
+ $results[] = array(
+ 'actor' => $actor_id,
+ 'result' => $result,
+ );
+ }
+
+ return $results;
+ }
+
+ /**
+ * Maybe initiate old user.
+ *
+ * This method checks if the current request domain matches the old host.
+ * If it does, it retrieves the cached data for the user and populates the instance.
+ *
+ * @param Blog|User $instance The Blog or User instance to populate.
+ */
+ public static function maybe_initiate_old_user( $instance ) {
+ if ( ! Query::get_instance()->is_old_host_request() ) {
+ return;
+ }
+
+ if ( $instance instanceof Blog ) {
+ $cached_data = \get_option( 'activitypub_blog_user_old_host_data' );
+ } elseif ( $instance instanceof User ) {
+ $cached_data = \get_user_option( 'activitypub_old_host_data', $instance->get__id() );
+ }
+
+ if ( ! empty( $cached_data ) ) {
+ $instance->from_json( $cached_data );
+ }
+ }
+
+ /**
+ * Pre-send to inboxes.
+ *
+ * @param string $json The ActivityPub Activity JSON.
+ */
+ public static function pre_send_to_inboxes( $json ) {
+ $json = json_decode( $json, true );
+
+ if ( 'Move' !== $json['type'] ) {
+ return;
+ }
+
+ if ( is_same_domain( $json['object'] ) ) {
+ return;
+ }
+
+ Query::get_instance()->set_old_host_request();
+ }
+
+ /**
+ * Filter to return the old blog username.
+ *
+ * @param null $pre The pre-existing value.
+ * @param string $username The username to check.
+ *
+ * @return Blog|null The old blog instance or null.
+ */
+ public static function old_blog_username( $pre, $username ) {
+ $old_host = \get_option( 'activitypub_old_host' );
+
+ // Special case for Blog Actor on old host.
+ if ( $old_host === $username && Query::get_instance()->is_old_host_request() ) {
+ // Return a new Blog instance which will load the cached data in its constructor.
+ $pre = new Blog();
+ }
+
+ return $pre;
+ }
+}
diff --git a/wp-content/plugins/activitypub/includes/class-notification.php b/wp-content/plugins/activitypub/includes/class-notification.php
index 1dd65732..68283133 100644
--- a/wp-content/plugins/activitypub/includes/class-notification.php
+++ b/wp-content/plugins/activitypub/includes/class-notification.php
@@ -60,7 +60,18 @@ class Notification {
public function send() {
$type = \strtolower( $this->type );
+ /**
+ * Action to send ActivityPub notifications.
+ *
+ * @param Notification $instance The notification object.
+ */
do_action( 'activitypub_notification', $this );
+
+ /**
+ * Type-specific action to send ActivityPub notifications.
+ *
+ * @param Notification $instance The notification object.
+ */
do_action( "activitypub_notification_{$type}", $this );
}
}
diff --git a/wp-content/plugins/activitypub/includes/class-options.php b/wp-content/plugins/activitypub/includes/class-options.php
new file mode 100644
index 00000000..d30ef9ad
--- /dev/null
+++ b/wp-content/plugins/activitypub/includes/class-options.php
@@ -0,0 +1,124 @@
+activitypub_object ) {
+ return $this->activitypub_object;
+ }
+
+ if ( $this->prepare_activitypub_data() ) {
+ return $this->activitypub_object;
+ }
+
+ $queried_object = $this->get_queried_object();
+ $transformer = Factory::get_transformer( $queried_object );
+
+ if ( $transformer && ! \is_wp_error( $transformer ) ) {
+ $this->activitypub_object = $transformer->to_object();
+ }
+
+ return $this->activitypub_object;
+ }
+
+ /**
+ * Get the ActivityPub object ID.
+ *
+ * @return string The ActivityPub object ID.
+ */
+ public function get_activitypub_object_id() {
+ if ( $this->activitypub_object_id ) {
+ return $this->activitypub_object_id;
+ }
+
+ if ( $this->prepare_activitypub_data() ) {
+ return $this->activitypub_object_id;
+ }
+
+ $queried_object = $this->get_queried_object();
+ $transformer = Factory::get_transformer( $queried_object );
+
+ if ( $transformer && ! \is_wp_error( $transformer ) ) {
+ $this->activitypub_object_id = $transformer->to_id();
+ }
+
+ return $this->activitypub_object_id;
+ }
+
+ /**
+ * Prepare and set both ActivityPub object and ID for Outbox activities and virtual objects.
+ *
+ * @return bool True if an object was found and set, false otherwise.
+ */
+ private function prepare_activitypub_data() {
+ $queried_object = $this->get_queried_object();
+
+ // Check for Outbox Activity.
+ if (
+ $queried_object instanceof \WP_Post &&
+ Outbox::POST_TYPE === $queried_object->post_type
+ ) {
+ $activitypub_object = Outbox::maybe_get_activity( $queried_object );
+
+ // Check if the Outbox Activity is public.
+ if ( ! \is_wp_error( $activitypub_object ) ) {
+ $this->activitypub_object = $activitypub_object;
+ $this->activitypub_object_id = $this->activitypub_object->get_id();
+ return true;
+ }
+ }
+
+ if ( ! $queried_object ) {
+ // If the object is not a valid ActivityPub object, try to get a virtual object.
+ $activitypub_object = $this->maybe_get_virtual_object();
+
+ if ( $activitypub_object ) {
+ $this->activitypub_object = $activitypub_object;
+ $this->activitypub_object_id = $this->activitypub_object->get_id();
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Get the queried object.
+ *
+ * This adds support for Comments by `?c=123` IDs and Users by `?author=123` and `@username` IDs.
+ *
+ * @return \WP_Term|\WP_Post_Type|\WP_Post|\WP_User|\WP_Comment|null The queried object.
+ */
+ public function get_queried_object() {
+ $queried_object = \get_queried_object();
+
+ // Check Comment by ID.
+ if ( ! $queried_object ) {
+ $comment_id = \get_query_var( 'c' );
+ if ( $comment_id ) {
+ $queried_object = \get_comment( $comment_id );
+ }
+ }
+
+ // Check Post by ID (works for custom post types).
+ if ( ! $queried_object ) {
+ $post_id = \get_query_var( 'p' );
+ if ( $post_id ) {
+ $queried_object = \get_post( $post_id );
+ }
+ }
+
+ // Try to get Author by ID.
+ if ( ! $queried_object ) {
+ $url = $this->get_request_url();
+ $author_id = url_to_authorid( $url );
+ if ( $author_id ) {
+ $queried_object = \get_user_by( 'id', $author_id );
+ }
+ }
+
+ /**
+ * Filters the queried object.
+ *
+ * @param \WP_Term|\WP_Post_Type|\WP_Post|\WP_User|\WP_Comment|null $queried_object The queried object.
+ */
+ return apply_filters( 'activitypub_queried_object', $queried_object );
+ }
+
+ /**
+ * Get the virtual object.
+ *
+ * Virtual objects are objects that are not stored in the database, but are created on the fly.
+ * The plugins currently supports two virtual objects: The Blog-Actor and the Application-Actor.
+ *
+ * @see \Activitypub\Model\Blog
+ * @see \Activitypub\Model\Application
+ *
+ * @return object|null The virtual object.
+ */
+ protected function maybe_get_virtual_object() {
+ $url = $this->get_request_url();
+
+ if ( ! $url ) {
+ return null;
+ }
+
+ $author_id = url_to_authorid( $url );
+
+ if ( ! is_numeric( $author_id ) ) {
+ $author_id = $url;
+ }
+
+ $user = Actors::get_by_various( $author_id );
+
+ if ( \is_wp_error( $user ) || ! $user ) {
+ return null;
+ }
+
+ return $user;
+ }
+
+ /**
+ * Get the request URL.
+ *
+ * @return string|null The request URL.
+ */
+ protected function get_request_url() {
+ if ( ! isset( $_SERVER['REQUEST_URI'] ) ) {
+ return null;
+ }
+
+ // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+ $url = \wp_unslash( $_SERVER['REQUEST_URI'] );
+ $url = \WP_Http::make_absolute_url( $url, \home_url() );
+ $url = \sanitize_url( $url );
+
+ return $url;
+ }
+
+ /**
+ * Check if the current request is an ActivityPub request.
+ *
+ * @return bool True if the request is an ActivityPub request, false otherwise.
+ */
+ public function is_activitypub_request() {
+ if ( isset( $this->is_activitypub_request ) ) {
+ return $this->is_activitypub_request;
+ }
+
+ global $wp_query;
+
+ // One can trigger an ActivityPub request by adding `?activitypub` to the URL.
+ if (
+ isset( $wp_query->query_vars['activitypub'] ) ||
+ // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ isset( $_GET['activitypub'] )
+ ) {
+ \defined( 'ACTIVITYPUB_REQUEST' ) || \define( 'ACTIVITYPUB_REQUEST', true );
+ $this->is_activitypub_request = true;
+
+ return true;
+ }
+
+ /*
+ * The other (more common) option to make an ActivityPub request
+ * is to send an Accept header.
+ */
+ if ( isset( $_SERVER['HTTP_ACCEPT'] ) ) {
+ $accept = \sanitize_text_field( \wp_unslash( $_SERVER['HTTP_ACCEPT'] ) );
+
+ /*
+ * $accept can be a single value, or a comma separated list of values.
+ * We want to support both scenarios,
+ * and return true when the header includes at least one of the following:
+ * - application/activity+json
+ * - application/ld+json
+ * - application/json
+ */
+ if ( \preg_match( '/(application\/(ld\+json|activity\+json|json))/i', $accept ) ) {
+ \defined( 'ACTIVITYPUB_REQUEST' ) || \define( 'ACTIVITYPUB_REQUEST', true );
+ $this->is_activitypub_request = true;
+
+ return true;
+ }
+ }
+
+ $this->is_activitypub_request = false;
+
+ return false;
+ }
+
+ /**
+ * Check if the current request is from the old host.
+ *
+ * @return bool True if the request is from the old host, false otherwise.
+ */
+ public function is_old_host_request() {
+ if ( isset( $this->is_old_host_request ) ) {
+ return $this->is_old_host_request;
+ }
+
+ $old_host = \get_option( 'activitypub_old_host' );
+
+ if ( ! $old_host ) {
+ $this->is_old_host_request = false;
+ return false;
+ }
+
+ $request_host = isset( $_SERVER['HTTP_HOST'] ) ? \sanitize_text_field( \wp_unslash( $_SERVER['HTTP_HOST'] ) ) : '';
+ $referer_host = isset( $_SERVER['HTTP_REFERER'] ) ? \wp_parse_url( \sanitize_text_field( \wp_unslash( $_SERVER['HTTP_REFERER'] ) ), PHP_URL_HOST ) : '';
+
+ // Check if the domain matches either the request domain or referer.
+ $check = $old_host === $request_host || $old_host === $referer_host;
+ $this->is_old_host_request = $check;
+
+ return $check;
+ }
+
+ /**
+ * Fake an old host request.
+ *
+ * @param bool $state Optional. The state to set. Default true.
+ */
+ public function set_old_host_request( $state = true ) {
+ $this->is_old_host_request = $state;
+ }
+}
diff --git a/wp-content/plugins/activitypub/includes/class-sanitize.php b/wp-content/plugins/activitypub/includes/class-sanitize.php
new file mode 100644
index 00000000..42d32491
--- /dev/null
+++ b/wp-content/plugins/activitypub/includes/class-sanitize.php
@@ -0,0 +1,122 @@
+ $sanitized,
+ 'search_columns' => array( 'user_login', 'user_nicename' ),
+ 'number' => 1,
+ 'hide_empty' => true,
+ 'fields' => 'ID',
+ )
+ );
+
+ if ( $user->get_results() ) {
+ \add_settings_error(
+ 'activitypub_blog_identifier',
+ 'activitypub_blog_identifier',
+ \esc_html__( 'You cannot use an existing author’s name for the blog profile ID.', 'activitypub' )
+ );
+
+ return Blog::get_default_username();
+ }
+
+ return $sanitized;
+ }
+
+ /**
+ * Get the sanitized value of a constant.
+ *
+ * @param mixed $value The constant value.
+ *
+ * @return string The sanitized value.
+ */
+ public static function constant_value( $value ) {
+ if ( is_bool( $value ) ) {
+ return $value ? 'true' : 'false';
+ }
+
+ if ( is_string( $value ) ) {
+ return esc_attr( $value );
+ }
+
+ if ( is_array( $value ) ) {
+ // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
+ return print_r( $value, true );
+ }
+
+ return $value;
+ }
+}
diff --git a/wp-content/plugins/activitypub/includes/class-scheduler.php b/wp-content/plugins/activitypub/includes/class-scheduler.php
index 0774f296..2e2ca070 100644
--- a/wp-content/plugins/activitypub/includes/class-scheduler.php
+++ b/wp-content/plugins/activitypub/includes/class-scheduler.php
@@ -7,7 +7,15 @@
namespace Activitypub;
+use Activitypub\Activity\Activity;
+use Activitypub\Activity\Base_Object;
+use Activitypub\Scheduler\Post;
+use Activitypub\Scheduler\Actor;
+use Activitypub\Scheduler\Comment;
+use Activitypub\Collection\Actors;
+use Activitypub\Collection\Outbox;
use Activitypub\Collection\Followers;
+use Activitypub\Transformer\Factory;
/**
* Scheduler class.
@@ -16,67 +24,53 @@ use Activitypub\Collection\Followers;
*/
class Scheduler {
+ /**
+ * Allowed batch callbacks.
+ *
+ * @var array
+ */
+ private static $batch_callbacks = array();
+
/**
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
- // Post transitions.
- \add_action( 'transition_post_status', array( self::class, 'schedule_post_activity' ), 33, 3 );
- \add_action(
- 'edit_attachment',
- function ( $post_id ) {
- self::schedule_post_activity( 'publish', 'publish', $post_id );
- }
- );
- \add_action(
- 'add_attachment',
- function ( $post_id ) {
- self::schedule_post_activity( 'publish', '', $post_id );
- }
- );
- \add_action(
- 'delete_attachment',
- function ( $post_id ) {
- self::schedule_post_activity( 'trash', '', $post_id );
- }
- );
+ self::register_schedulers();
- if ( ! ACTIVITYPUB_DISABLE_OUTGOING_INTERACTIONS ) {
- // Comment transitions.
- \add_action( 'transition_comment_status', array( self::class, 'schedule_comment_activity' ), 20, 3 );
- \add_action(
- 'edit_comment',
- function ( $comment_id ) {
- self::schedule_comment_activity( 'approved', 'approved', $comment_id );
- }
- );
- \add_action(
- 'wp_insert_comment',
- function ( $comment_id ) {
- self::schedule_comment_activity( 'approved', '', $comment_id );
- }
- );
- }
+ self::$batch_callbacks = array(
+ Dispatcher::$callback,
+ array( Dispatcher::class, 'retry_send_to_followers' ),
+ );
// Follower Cleanups.
\add_action( 'activitypub_update_followers', array( self::class, 'update_followers' ) );
\add_action( 'activitypub_cleanup_followers', array( self::class, 'cleanup_followers' ) );
- // Profile updates for blog options.
- if ( ! is_user_type_disabled( 'blog' ) ) {
- \add_action( 'update_option_site_icon', array( self::class, 'blog_user_update' ) );
- \add_action( 'update_option_blogdescription', array( self::class, 'blog_user_update' ) );
- \add_action( 'update_option_blogname', array( self::class, 'blog_user_update' ) );
- \add_filter( 'pre_set_theme_mod_custom_logo', array( self::class, 'blog_user_update' ) );
- \add_filter( 'pre_set_theme_mod_header_image', array( self::class, 'blog_user_update' ) );
- }
+ // Event callbacks.
+ \add_action( 'activitypub_async_batch', array( self::class, 'async_batch' ), 10, 99 );
+ \add_action( 'activitypub_reprocess_outbox', array( self::class, 'reprocess_outbox' ) );
+ \add_action( 'activitypub_outbox_purge', array( self::class, 'purge_outbox' ) );
- // Profile updates for user options.
- if ( ! is_user_type_disabled( 'user' ) ) {
- \add_action( 'wp_update_user', array( self::class, 'user_update' ) );
- \add_action( 'updated_user_meta', array( self::class, 'user_meta_update' ), 10, 3 );
- // @todo figure out a feasible way of updating the header image since it's not unique to any user.
- }
+ \add_action( 'post_activitypub_add_to_outbox', array( self::class, 'schedule_outbox_activity_for_federation' ) );
+ \add_action( 'post_activitypub_add_to_outbox', array( self::class, 'schedule_announce_activity' ), 10, 4 );
+
+ \add_action( 'update_option_activitypub_outbox_purge_days', array( self::class, 'handle_outbox_purge_days_update' ), 10, 2 );
+ }
+
+ /**
+ * Register handlers.
+ */
+ public static function register_schedulers() {
+ Post::init();
+ Actor::init();
+ Comment::init();
+
+ /**
+ * Register additional schedulers.
+ *
+ * @since 5.0.0
+ */
+ do_action( 'activitypub_register_schedulers' );
}
/**
@@ -90,6 +84,14 @@ class Scheduler {
if ( ! \wp_next_scheduled( 'activitypub_cleanup_followers' ) ) {
\wp_schedule_event( time(), 'daily', 'activitypub_cleanup_followers' );
}
+
+ if ( ! \wp_next_scheduled( 'activitypub_reprocess_outbox' ) ) {
+ \wp_schedule_event( time(), 'hourly', 'activitypub_reprocess_outbox' );
+ }
+
+ if ( ! wp_next_scheduled( 'activitypub_outbox_purge' ) ) {
+ wp_schedule_event( time(), 'daily', 'activitypub_outbox_purge' );
+ }
}
/**
@@ -100,125 +102,8 @@ class Scheduler {
public static function deregister_schedules() {
wp_unschedule_hook( 'activitypub_update_followers' );
wp_unschedule_hook( 'activitypub_cleanup_followers' );
- }
-
-
- /**
- * Schedule Activities.
- *
- * @param string $new_status New post status.
- * @param string $old_status Old post status.
- * @param \WP_Post $post Post object.
- */
- public static function schedule_post_activity( $new_status, $old_status, $post ) {
- $post = get_post( $post );
-
- if ( ! $post ) {
- return;
- }
-
- if ( 'ap_extrafield' === $post->post_type ) {
- self::schedule_profile_update( $post->post_author );
- return;
- }
-
- if ( 'ap_extrafield_blog' === $post->post_type ) {
- self::schedule_profile_update( 0 );
- return;
- }
-
- // Do not send activities if post is password protected.
- if ( \post_password_required( $post ) ) {
- return;
- }
-
- // Check if post-type supports ActivityPub.
- $post_types = \get_post_types_by_support( 'activitypub' );
- if ( ! \in_array( $post->post_type, $post_types, true ) ) {
- return;
- }
-
- $type = false;
-
- if (
- 'publish' === $new_status &&
- 'publish' !== $old_status
- ) {
- $type = 'Create';
- } elseif (
- 'publish' === $new_status ||
- // We want to send updates for posts that are published and then moved to draft.
- ( 'draft' === $new_status &&
- 'publish' === $old_status )
- ) {
- $type = 'Update';
- } elseif ( 'trash' === $new_status ) {
- $type = 'Delete';
- }
-
- if ( empty( $type ) ) {
- return;
- }
-
- $hook = 'activitypub_send_post';
- $args = array( $post->ID, $type );
-
- if ( false === wp_next_scheduled( $hook, $args ) ) {
- set_wp_object_state( $post, 'federate' );
- \wp_schedule_single_event( \time(), $hook, $args );
- }
- }
-
- /**
- * Schedule Comment Activities.
- *
- * @see transition_comment_status()
- *
- * @param string $new_status New comment status.
- * @param string $old_status Old comment status.
- * @param \WP_Comment $comment Comment object.
- */
- public static function schedule_comment_activity( $new_status, $old_status, $comment ) {
- $comment = get_comment( $comment );
-
- // Federate only comments that are written by a registered user.
- if ( ! $comment || ! $comment->user_id ) {
- return;
- }
-
- $type = false;
-
- if (
- 'approved' === $new_status &&
- 'approved' !== $old_status
- ) {
- $type = 'Create';
- } elseif ( 'approved' === $new_status ) {
- $type = 'Update';
- \update_comment_meta( $comment->comment_ID, 'activitypub_comment_modified', time(), true );
- } elseif (
- 'trash' === $new_status ||
- 'spam' === $new_status
- ) {
- $type = 'Delete';
- }
-
- if ( empty( $type ) ) {
- return;
- }
-
- // Check if comment should be federated or not.
- if ( ! should_comment_be_federated( $comment ) ) {
- return;
- }
-
- $hook = 'activitypub_send_comment';
- $args = array( $comment->comment_ID, $type );
-
- if ( false === wp_next_scheduled( $hook, $args ) ) {
- set_wp_object_state( $comment, 'federate' );
- \wp_schedule_single_event( \time(), $hook, $args );
- }
+ wp_unschedule_hook( 'activitypub_reprocess_outbox' );
+ wp_unschedule_hook( 'activitypub_outbox_purge' );
}
/**
@@ -292,67 +177,268 @@ class Scheduler {
}
/**
- * Send a profile update when relevant user meta is updated.
+ * Schedule the outbox item for federation.
*
- * @param int $meta_id Meta ID being updated.
- * @param int $user_id User ID being updated.
- * @param string $meta_key Meta key being updated.
+ * @param int $id The ID of the outbox item.
+ * @param int $offset The offset to add to the scheduled time.
*/
- public static function user_meta_update( $meta_id, $user_id, $meta_key ) {
- // Don't bother if the user can't publish.
- if ( ! \user_can( $user_id, 'activitypub' ) ) {
+ public static function schedule_outbox_activity_for_federation( $id, $offset = 0 ) {
+ $hook = 'activitypub_process_outbox';
+ $args = array( $id );
+
+ if ( false === wp_next_scheduled( $hook, $args ) ) {
+ \wp_schedule_single_event(
+ \time() + $offset,
+ $hook,
+ $args
+ );
+ }
+ }
+
+ /**
+ * Reprocess the outbox.
+ */
+ public static function reprocess_outbox() {
+ // Bail if there is a pending batch.
+ if ( self::next_scheduled_hook( 'activitypub_async_batch' ) ) {
return;
}
- // The user meta fields that affect a profile.
- $fields = array(
- 'activitypub_description',
- 'activitypub_header_image',
- 'description',
- 'user_url',
- 'display_name',
- );
- if ( in_array( $meta_key, $fields, true ) ) {
- self::schedule_profile_update( $user_id );
- }
- }
-
- /**
- * Send a profile update when a user is updated.
- *
- * @param int $user_id User ID being updated.
- */
- public static function user_update( $user_id ) {
- // Don't bother if the user can't publish.
- if ( ! \user_can( $user_id, 'activitypub' ) ) {
+ // Bail if there is a batch in progress.
+ $key = \md5( \serialize( Dispatcher::$callback ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
+ if ( self::is_locked( $key ) ) {
return;
}
- self::schedule_profile_update( $user_id );
- }
-
- /**
- * Theme mods only have a dynamic filter so we fudge it like this.
- *
- * @param mixed $value Optional. The value to be updated. Default null.
- *
- * @return mixed
- */
- public static function blog_user_update( $value = null ) {
- self::schedule_profile_update( 0 );
- return $value;
- }
-
- /**
- * Send a profile update to all followers. Gets hooked into all relevant options/meta etc.
- *
- * @param int $user_id The user ID to update (Could be 0 for Blog-User).
- */
- public static function schedule_profile_update( $user_id ) {
- \wp_schedule_single_event(
- \time(),
- 'activitypub_send_update_profile_activity',
- array( $user_id )
+ $ids = \get_posts(
+ array(
+ 'post_type' => Outbox::POST_TYPE,
+ 'post_status' => 'pending',
+ 'posts_per_page' => 10,
+ 'fields' => 'ids',
+ )
);
+
+ foreach ( $ids as $id ) {
+ self::schedule_outbox_activity_for_federation( $id );
+ }
+ }
+
+ /**
+ * Purge outbox items based on a schedule.
+ */
+ public static function purge_outbox() {
+ $total_posts = (int) wp_count_posts( Outbox::POST_TYPE )->publish;
+ if ( $total_posts <= 20 ) {
+ return;
+ }
+
+ $days = (int) get_option( 'activitypub_outbox_purge_days', 180 );
+ $timezone = new \DateTimeZone( 'UTC' );
+ $date = new \DateTime( 'now', $timezone );
+
+ $date->sub( \DateInterval::createFromDateString( "$days days" ) );
+
+ $post_ids = get_posts(
+ array(
+ 'post_type' => Outbox::POST_TYPE,
+ 'post_status' => 'any',
+ 'fields' => 'ids',
+ 'numberposts' => -1,
+ 'date_query' => array(
+ array(
+ 'before' => $date->format( 'Y-m-d' ),
+ ),
+ ),
+ )
+ );
+
+ foreach ( $post_ids as $post_id ) {
+ \wp_delete_post( $post_id, true );
+ }
+ }
+
+ /**
+ * Update schedules when outbox purge days settings change.
+ *
+ * @param int $old_value The old value.
+ * @param int $value The new value.
+ */
+ public static function handle_outbox_purge_days_update( $old_value, $value ) {
+ if ( 0 === (int) $value ) {
+ wp_clear_scheduled_hook( 'activitypub_outbox_purge' );
+ } elseif ( ! wp_next_scheduled( 'activitypub_outbox_purge' ) ) {
+ wp_schedule_event( time(), 'daily', 'activitypub_outbox_purge' );
+ }
+ }
+
+ /**
+ * Asynchronously runs batch processing routines.
+ *
+ * The batching part is optional and only comes into play if the callback returns anything.
+ * Beyond that it's a helper to run a callback asynchronously with locking to prevent simultaneous processing.
+ *
+ * @param callable $callback Callable processing routine.
+ * @params mixed ...$args Optional. Parameters that get passed to the callback.
+ */
+ public static function async_batch( $callback ) {
+ if ( ! in_array( $callback, self::$batch_callbacks, true ) || ! \is_callable( $callback ) ) {
+ _doing_it_wrong( __METHOD__, 'The first argument must be a valid callback.', '5.2.0' );
+ return;
+ }
+
+ $args = \func_get_args(); // phpcs:ignore PHPCompatibility.FunctionUse.ArgumentFunctionsReportCurrentValue
+ $key = \md5( \serialize( $callback ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
+
+ // Bail if the existing lock is still valid.
+ if ( self::is_locked( $key ) ) {
+ \wp_schedule_single_event( time() + MINUTE_IN_SECONDS, 'activitypub_async_batch', $args );
+ return;
+ }
+
+ self::lock( $key );
+
+ $callback = array_shift( $args ); // Remove $callback from arguments.
+ $next = \call_user_func_array( $callback, $args );
+
+ self::unlock( $key );
+
+ if ( ! empty( $next ) ) {
+ // Schedule the next run, adding the result to the arguments.
+ \wp_schedule_single_event(
+ \time() + 30,
+ 'activitypub_async_batch',
+ \array_merge( array( $callback ), \array_values( $next ) )
+ );
+ }
+ }
+
+
+ /**
+ * Locks the async batch process for individual callbacks to prevent simultaneous processing.
+ *
+ * @param string $key Serialized callback name.
+ * @return bool|int True if the lock was successful, timestamp of existing lock otherwise.
+ */
+ public static function lock( $key ) {
+ global $wpdb;
+
+ // Try to lock.
+ $lock_result = (bool) $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO `$wpdb->options` ( `option_name`, `option_value`, `autoload` ) VALUES (%s, %s, 'no') /* LOCK */", 'activitypub_async_batch_' . $key, \time() ) ); // phpcs:ignore WordPress.DB
+
+ if ( ! $lock_result ) {
+ $lock_result = \get_option( 'activitypub_async_batch_' . $key );
+ }
+
+ return $lock_result;
+ }
+
+ /**
+ * Unlocks processing for the async batch callback.
+ *
+ * @param string $key Serialized callback name.
+ */
+ public static function unlock( $key ) {
+ \delete_option( 'activitypub_async_batch_' . $key );
+ }
+
+ /**
+ * Whether the async batch callback is locked.
+ *
+ * @param string $key Serialized callback name.
+ * @return boolean
+ */
+ public static function is_locked( $key ) {
+ $lock = \get_option( 'activitypub_async_batch_' . $key );
+
+ if ( ! $lock ) {
+ return false;
+ }
+
+ $lock = (int) $lock;
+
+ if ( $lock < \time() - 1800 ) {
+ self::unlock( $key );
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Get the next scheduled hook.
+ *
+ * @param string $hook The hook name.
+ * @return int|bool The timestamp of the next scheduled hook, or false if none found.
+ */
+ private static function next_scheduled_hook( $hook ) {
+ $crons = _get_cron_array();
+ if ( empty( $crons ) ) {
+ return false;
+ }
+
+ // Get next event.
+ $next = false;
+ foreach ( $crons as $timestamp => $cron ) {
+ if ( isset( $cron[ $hook ] ) ) {
+ $next = $timestamp;
+ break;
+ }
+ }
+
+ return $next;
+ }
+
+ /**
+ * Send announces.
+ *
+ * @param int $outbox_activity_id The outbox activity ID.
+ * @param \Activitypub\Activity\Activity $activity The activity object.
+ * @param int $actor_id The actor ID.
+ * @param int $content_visibility The content visibility.
+ */
+ public static function schedule_announce_activity( $outbox_activity_id, $activity, $actor_id, $content_visibility ) {
+ // Only if we're in both Blog and User modes.
+ if ( ACTIVITYPUB_ACTOR_AND_BLOG_MODE !== \get_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE ) ) {
+ return;
+ }
+
+ // Only if this isn't the Blog Actor.
+ if ( Actors::BLOG_USER_ID === $actor_id ) {
+ return;
+ }
+
+ // Only if the content is public or quiet public.
+ if ( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC !== $content_visibility ) {
+ return;
+ }
+
+ // Only if the activity is a Create.
+ if ( 'Create' !== $activity->get_type() ) {
+ return;
+ }
+
+ if ( ! is_object( $activity->get_object() ) ) {
+ return;
+ }
+
+ // Check if the object is an article, image, audio, video, event, or document and ignore profile updates and other activities.
+ if ( ! in_array( $activity->get_object()->get_type(), Base_Object::TYPES, true ) ) {
+ return;
+ }
+
+ $announce = new Activity();
+ $announce->set_type( 'Announce' );
+ $announce->set_actor( Actors::get_by_id( Actors::BLOG_USER_ID )->get_id() );
+ $announce->set_object( $activity );
+
+ $outbox_activity_id = Outbox::add( $announce, Actors::BLOG_USER_ID );
+
+ if ( ! $outbox_activity_id ) {
+ return;
+ }
+
+ // Schedule the outbox item for federation.
+ self::schedule_outbox_activity_for_federation( $outbox_activity_id, 120 );
}
}
diff --git a/wp-content/plugins/activitypub/includes/class-shortcodes.php b/wp-content/plugins/activitypub/includes/class-shortcodes.php
index eb9c5135..63d7e6de 100644
--- a/wp-content/plugins/activitypub/includes/class-shortcodes.php
+++ b/wp-content/plugins/activitypub/includes/class-shortcodes.php
@@ -54,6 +54,11 @@ class Shortcodes {
$hash_tags = array();
foreach ( $tags as $tag ) {
+ // Tag can be empty.
+ if ( ! $tag ) {
+ continue;
+ }
+
$hash_tags[] = \sprintf(
'%s',
\esc_url( \get_tag_link( $tag ) ),
@@ -67,16 +72,36 @@ class Shortcodes {
/**
* Generates output for the 'ap_title' Shortcode
*
+ * @param array $atts The Shortcode attributes.
+ * @param string $content The ActivityPub post-content.
+ * @param string $tag The tag/name of the Shortcode.
+ *
* @return string The post title.
*/
- public static function title() {
+ public static function title( $atts, $content, $tag ) {
$item = self::get_item();
if ( ! $item ) {
return '';
}
- return \wp_strip_all_tags( \get_the_title( $item->ID ), true );
+ $title = \wp_strip_all_tags( \get_the_title( $item->ID ), true );
+
+ if ( ! $title ) {
+ return '';
+ }
+
+ $atts = shortcode_atts(
+ array( 'type' => 'plain' ),
+ $atts,
+ $tag
+ );
+
+ if ( 'html' !== $atts['type'] ) {
+ return $title;
+ }
+
+ return sprintf( '' . $content . '
'; } + /** + * Add the 'me' rel to the link. + * + * @param string $rel The rel attribute. + * @return string The modified rel attribute. + */ + public static function add_rel_me( $rel ) { + return $rel . ' me'; + } + /** * Checks if the user is the blog user. * @@ -278,6 +295,6 @@ class Extra_Fields { * @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; + return Actors::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 76611ff9..24be3507 100644 --- a/wp-content/plugins/activitypub/includes/collection/class-followers.php +++ b/wp-content/plugins/activitypub/includes/collection/class-followers.php @@ -7,9 +7,9 @@ namespace Activitypub\Collection; +use Activitypub\Model\Follower; use WP_Error; use WP_Query; -use Activitypub\Model\Follower; use function Activitypub\is_tombstone; use function Activitypub\get_remote_metadata_by_actor; @@ -52,11 +52,11 @@ class Followers { return $id; } - $post_meta = get_post_meta( $id, 'activitypub_user_id', false ); + $post_meta = get_post_meta( $id, '_activitypub_user_id', false ); // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict if ( is_array( $post_meta ) && ! in_array( $user_id, $post_meta ) ) { - add_post_meta( $id, 'activitypub_user_id', $user_id ); + add_post_meta( $id, '_activitypub_user_id', $user_id ); wp_cache_delete( sprintf( self::CACHE_KEY_INBOXES, $user_id ), 'activitypub' ); } @@ -80,7 +80,16 @@ class Followers { return false; } - return delete_post_meta( $follower->get__id(), 'activitypub_user_id', $user_id ); + /** + * Fires before a Follower is removed. + * + * @param Follower $follower The Follower object. + * @param int $user_id The ID of the WordPress User. + * @param string $actor The Actor URL. + */ + do_action( 'activitypub_followers_pre_remove_follower', $follower, $user_id, $actor ); + + return delete_post_meta( $follower->get__id(), '_activitypub_user_id', $user_id ); } /** @@ -89,7 +98,7 @@ class Followers { * @param int $user_id The ID of the WordPress User. * @param string $actor The Actor URL. * - * @return Follower|null The Follower object or null + * @return Follower|false|null The Follower object or null */ public static function get_follower( $user_id, $actor ) { global $wpdb; @@ -97,7 +106,7 @@ class Followers { // phpcs:ignore WordPress.DB.DirectDatabaseQuery $post_id = $wpdb->get_var( $wpdb->prepare( - "SELECT DISTINCT p.ID FROM $wpdb->posts p INNER JOIN $wpdb->postmeta pm ON p.ID = pm.post_id WHERE p.post_type = %s AND pm.meta_key = 'activitypub_user_id' AND pm.meta_value = %d AND p.guid = %s", + "SELECT DISTINCT p.ID FROM $wpdb->posts p INNER JOIN $wpdb->postmeta pm ON p.ID = pm.post_id WHERE p.post_type = %s AND pm.meta_key = '_activitypub_user_id' AND pm.meta_value = %d AND p.guid = %s", array( esc_sql( self::POST_TYPE ), esc_sql( $user_id ), @@ -119,7 +128,7 @@ class Followers { * * @param string $actor The Actor URL. * - * @return \Activitypub\Activity\Base_Object|WP_Error|null + * @return Follower|false|null The Follower object or false on failure. */ public static function get_follower_by_actor( $actor ) { global $wpdb; @@ -147,7 +156,7 @@ class Followers { * @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. + * @return Follower[] List of `Follower` objects. */ public static function get_followers( $user_id, $number = -1, $page = null, $args = array() ) { $data = self::get_followers_with_count( $user_id, $number, $page, $args ); @@ -165,8 +174,8 @@ class Followers { * @return array { * Data about the followers. * - * @type array $followers List of `Follower` objects. - * @type int $total Total number of followers. + * @type Follower[] $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() ) { @@ -179,7 +188,7 @@ class Followers { // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'meta_query' => array( array( - 'key' => 'activitypub_user_id', + 'key' => '_activitypub_user_id', 'value' => $user_id, ), ), @@ -188,12 +197,8 @@ class Followers { $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() - ); + $followers = array_map( array( Follower::class, 'init_from_cpt' ), $query->get_posts() ); + $followers = array_filter( $followers ); return compact( 'followers', 'total' ); } @@ -201,7 +206,7 @@ class Followers { /** * Get all Followers. * - * @return array The Term list of Followers. + * @return Follower[] The Term list of Followers. */ public static function get_all_followers() { $args = array( @@ -210,11 +215,11 @@ class Followers { 'meta_query' => array( 'relation' => 'AND', array( - 'key' => 'activitypub_inbox', + 'key' => '_activitypub_inbox', 'compare' => 'EXISTS', ), array( - 'key' => 'activitypub_actor_json', + 'key' => '_activitypub_actor_json', 'compare' => 'EXISTS', ), ), @@ -238,15 +243,15 @@ class Followers { 'meta_query' => array( 'relation' => 'AND', array( - 'key' => 'activitypub_user_id', + 'key' => '_activitypub_user_id', 'value' => $user_id, ), array( - 'key' => 'activitypub_inbox', + 'key' => '_activitypub_inbox', 'compare' => 'EXISTS', ), array( - 'key' => 'activitypub_actor_json', + 'key' => '_activitypub_actor_json', 'compare' => 'EXISTS', ), ), @@ -257,7 +262,7 @@ class Followers { } /** - * Returns all Inboxes for a Users Followers. + * Returns all Inboxes for an Actor's Followers. * * @param int $user_id The ID of the WordPress User. * @@ -271,7 +276,7 @@ class Followers { return $inboxes; } - // Get all Followers of a ID of the WordPress User. + // Get all Followers of an ID of the WordPress User. $posts = new WP_Query( array( 'nopaging' => true, @@ -281,15 +286,15 @@ class Followers { 'meta_query' => array( 'relation' => 'AND', array( - 'key' => 'activitypub_inbox', + 'key' => '_activitypub_inbox', 'compare' => 'EXISTS', ), array( - 'key' => 'activitypub_user_id', + 'key' => '_activitypub_user_id', 'value' => $user_id, ), array( - 'key' => 'activitypub_inbox', + 'key' => '_activitypub_inbox', 'value' => '', 'compare' => '!=', ), @@ -309,7 +314,7 @@ class Followers { $wpdb->prepare( "SELECT DISTINCT meta_value FROM {$wpdb->postmeta} WHERE post_id IN (" . implode( ', ', array_fill( 0, count( $posts ), '%d' ) ) . ") - AND meta_key = 'activitypub_inbox' + AND meta_key = '_activitypub_inbox' AND meta_value IS NOT NULL", $posts ) @@ -321,13 +326,63 @@ class Followers { return $inboxes; } + /** + * Get all Inboxes for a given Activity. + * + * @param string $json The ActivityPub Activity JSON. + * @param int $actor_id The WordPress Actor ID. + * @param int $batch_size Optional. The batch size. Default 50. + * @param int $offset Optional. The offset. Default 0. + * + * @return array The list of Inboxes. + */ + public static function get_inboxes_for_activity( $json, $actor_id, $batch_size = 50, $offset = 0 ) { + $inboxes = self::get_inboxes( $actor_id ); + + if ( self::maybe_add_inboxes_of_blog_user( $json, $actor_id ) ) { + $inboxes = array_fill_keys( $inboxes, 1 ); + foreach ( self::get_inboxes( Actors::BLOG_USER_ID ) as $inbox ) { + $inboxes[ $inbox ] = 1; + } + $inboxes = array_keys( $inboxes ); + } + + return array_slice( $inboxes, $offset, $batch_size ); + } + + /** + * Maybe add Inboxes of the Blog User. + * + * @param string $json The ActivityPub Activity JSON. + * @param int $actor_id The WordPress Actor ID. + * @return bool True if the Inboxes of the Blog User should be added, false otherwise. + */ + public static function maybe_add_inboxes_of_blog_user( $json, $actor_id ) { + // Only if we're in both Blog and User modes. + if ( ACTIVITYPUB_ACTOR_AND_BLOG_MODE !== \get_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE ) ) { + return false; + } + // Only if this isn't the Blog Actor. + if ( Actors::BLOG_USER_ID === $actor_id ) { + return false; + } + + $activity = json_decode( $json, true ); + // Only if this is an Update or Delete. Create handles its own "Announce" in dual user mode. + if ( ! in_array( $activity['type'] ?? null, array( 'Update', 'Delete' ), true ) ) { + return false; + } + + return true; + } + /** * Get all Followers that have not been updated for a given time. * * @param int $number Optional. Limits the result. Default 50. * @param int $older_than Optional. The time in seconds. Default 86400 (1 day). * - * @return array The Term list of Followers. + * @return Follower[] The Term list of Followers. */ public static function get_outdated_followers( $number = 50, $older_than = 86400 ) { $args = array( @@ -345,13 +400,9 @@ class Followers { ); $posts = new WP_Query( $args ); - $items = array(); + $items = array_map( array( Follower::class, 'init_from_cpt' ), $posts->get_posts() ); - foreach ( $posts->get_posts() as $follower ) { - $items[] = Follower::init_from_cpt( $follower ); - } - - return $items; + return array_filter( $items ); } /** @@ -359,7 +410,7 @@ class Followers { * * @param int $number Optional. The number of Followers to return. Default 20. * - * @return array The Term list of Followers. + * @return Follower[] The Term list of Followers. */ public static function get_faulty_followers( $number = 20 ) { $args = array( @@ -369,24 +420,24 @@ class Followers { 'meta_query' => array( 'relation' => 'OR', array( - 'key' => 'activitypub_errors', + 'key' => '_activitypub_errors', 'compare' => 'EXISTS', ), array( - 'key' => 'activitypub_inbox', + 'key' => '_activitypub_inbox', 'compare' => 'NOT EXISTS', ), array( - 'key' => 'activitypub_actor_json', + 'key' => '_activitypub_actor_json', 'compare' => 'NOT EXISTS', ), array( - 'key' => 'activitypub_inbox', + 'key' => '_activitypub_inbox', 'value' => '', 'compare' => '=', ), array( - 'key' => 'activitypub_actor_json', + 'key' => '_activitypub_actor_json', 'value' => '', 'compare' => '=', ), @@ -394,13 +445,9 @@ class Followers { ); $posts = new WP_Query( $args ); - $items = array(); + $items = array_map( array( Follower::class, 'init_from_cpt' ), $posts->get_posts() ); - foreach ( $posts->get_posts() as $follower ) { - $items[] = Follower::init_from_cpt( $follower ); - } - - return $items; + return array_filter( $items ); } /** @@ -428,7 +475,7 @@ class Followers { return add_post_meta( $post_id, - 'activitypub_errors', + '_activitypub_errors', $error_message ); } diff --git a/wp-content/plugins/activitypub/includes/collection/class-interactions.php b/wp-content/plugins/activitypub/includes/collection/class-interactions.php index 36262e86..dc1f9417 100644 --- a/wp-content/plugins/activitypub/includes/collection/class-interactions.php +++ b/wp-content/plugins/activitypub/includes/collection/class-interactions.php @@ -7,10 +7,12 @@ namespace Activitypub\Collection; +use Activitypub\Webfinger; use WP_Comment_Query; use Activitypub\Comment; use function Activitypub\object_to_uri; +use function Activitypub\is_post_disabled; use function Activitypub\url_to_commentid; use function Activitypub\object_id_to_comment; use function Activitypub\get_remote_metadata_by_actor; @@ -27,7 +29,7 @@ class Interactions { * * @param array $activity The activity-object. * - * @return array|false The comment data or false on failure. + * @return int|false|\WP_Error The comment ID or false or WP_Error on failure. */ public static function add_comment( $activity ) { $commentdata = self::activity_to_comment( $activity ); @@ -36,7 +38,8 @@ class Interactions { return false; } - $in_reply_to = \esc_url_raw( $activity['object']['inReplyTo'] ); + $in_reply_to = object_to_uri( $activity['object']['inReplyTo'] ); + $in_reply_to = \esc_url_raw( $in_reply_to ); $comment_post_id = \url_to_postid( $in_reply_to ); $parent_comment_id = url_to_commentid( $in_reply_to ); @@ -46,8 +49,7 @@ class Interactions { $comment_post_id = $parent_comment->comment_post_ID; } - // Not a reply to a post or comment. - if ( ! $comment_post_id ) { + if ( is_post_disabled( $comment_post_id ) ) { return false; } @@ -97,27 +99,26 @@ class Interactions { } $url = object_to_uri( $activity['object'] ); - $comment_post_id = url_to_postid( $url ); + $comment_post_id = \url_to_postid( $url ); $parent_comment_id = url_to_commentid( $url ); if ( ! $comment_post_id && $parent_comment_id ) { - $parent_comment = get_comment( $parent_comment_id ); + $parent_comment = \get_comment( $parent_comment_id ); $comment_post_id = $parent_comment->comment_post_ID; } - if ( ! $comment_post_id ) { + if ( ! $comment_post_id || is_post_disabled( $comment_post_id ) ) { // Not a reply to a post or comment. return false; } - $type = $activity['type']; + $comment_type = Comment::get_comment_type_by_activity_type( $activity['type'] ); - if ( ! Comment::is_registered_comment_type( $type ) ) { + if ( ! $comment_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; @@ -178,20 +179,19 @@ class Interactions { $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', - 'compare' => '=', + 'key' => 'protocol', + 'value' => 'activitypub', ), ), ); - $comment_query = new WP_Comment_Query( $args ); - return $comment_query->comments; + + return get_comments( $args ); } /** @@ -229,7 +229,7 @@ class Interactions { */ public static function activity_to_comment( $activity ) { $comment_content = null; - $actor = object_to_uri( $activity['actor'] ); + $actor = object_to_uri( $activity['actor'] ?? null ); $actor = get_remote_metadata_by_actor( $actor ); // Check Actor-Meta. @@ -246,22 +246,29 @@ class Interactions { return false; } - $url = object_to_uri( $actor['url'] ); + $url = object_to_uri( $actor['url'] ?? $actor['id'] ); if ( ! $url ) { - object_to_uri( $actor['id'] ); + $url = object_to_uri( $actor['id'] ); } if ( isset( $activity['object']['content'] ) ) { $comment_content = \addslashes( $activity['object']['content'] ); } + $webfinger = Webfinger::uri_to_acct( $url ); + if ( is_wp_error( $webfinger ) ) { + $webfinger = ''; + } else { + $webfinger = str_replace( 'acct:', '', $webfinger ); + } + $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_author_email' => $webfinger, 'comment_meta' => array( 'source_id' => \esc_url_raw( object_to_uri( $activity['object'] ) ), 'protocol' => 'activitypub', @@ -289,7 +296,7 @@ class Interactions { */ public static function persist( $commentdata, $action = self::INSERT ) { // Disable flood control. - \remove_action( 'check_comment_flood', 'check_comment_flood_db', 10 ); + \remove_action( 'check_comment_flood', 'check_comment_flood_db' ); // Do not require email for AP entries. \add_filter( 'pre_option_require_name_email', '__return_false' ); // No nonce possible for this submission route. @@ -307,7 +314,7 @@ class Interactions { $state = \wp_update_comment( $commentdata, true ); } - \remove_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ), 10 ); + \remove_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ) ); \remove_filter( 'pre_option_require_name_email', '__return_false' ); // Restore flood control. \add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 ); @@ -318,4 +325,25 @@ class Interactions { return $state; // Either WP_Comment, false, a WP_Error, 0, or 1! } } + + /** + * Get the total number of interactions by type for a given ID. + * + * @param int $post_id The post ID. + * @param string $type The type of interaction to count. + * + * @return int The total number of interactions. + */ + public static function count_by_type( $post_id, $type ) { + return \get_comments( + array( + 'post_id' => $post_id, + 'status' => 'approve', + 'type' => $type, + 'count' => true, + 'paging' => false, + 'fields' => 'ids', + ) + ); + } } diff --git a/wp-content/plugins/activitypub/includes/collection/class-outbox.php b/wp-content/plugins/activitypub/includes/collection/class-outbox.php new file mode 100644 index 00000000..cf1dff89 --- /dev/null +++ b/wp-content/plugins/activitypub/includes/collection/class-outbox.php @@ -0,0 +1,351 @@ +get_object() ); + + if ( ! $activity->get_actor() ) { + $activity->set_actor( Actors::get_by_id( $user_id )->get_id() ); + } + + $outbox_item = array( + 'post_type' => self::POST_TYPE, + 'post_title' => sprintf( + /* translators: 1. Activity type, 2. Object Title or Excerpt */ + __( '[%1$s] %2$s', 'activitypub' ), + $activity->get_type(), + \wp_trim_words( $title, 5 ) + ), + 'post_content' => wp_slash( $activity->to_json() ), + // ensure that user ID is not below 0. + 'post_author' => \max( $user_id, 0 ), + 'post_status' => 'pending', + 'meta_input' => array( + '_activitypub_object_id' => $object_id, + '_activitypub_activity_type' => $activity->get_type(), + '_activitypub_activity_actor' => $actor_type, + 'activitypub_content_visibility' => $visibility, + ), + ); + + $has_kses = false !== \has_filter( 'content_save_pre', 'wp_filter_post_kses' ); + if ( $has_kses ) { + // Prevent KSES from corrupting JSON in post_content. + \kses_remove_filters(); + } + + $id = \wp_insert_post( $outbox_item, true ); + + // Update the activity ID if the post was inserted successfully. + if ( $id && ! \is_wp_error( $id ) ) { + $activity->set_id( \get_the_guid( $id ) ); + + \wp_update_post( + array( + 'ID' => $id, + 'post_content' => \wp_slash( $activity->to_json() ), + ) + ); + } + + if ( $has_kses ) { + \kses_init_filters(); + } + + if ( \is_wp_error( $id ) ) { + return $id; + } + + if ( ! $id ) { + return false; + } + + self::invalidate_existing_items( $object_id, $activity->get_type(), $id ); + + return $id; + } + + /** + * Invalidate existing outbox items with the same activity type and object ID + * by setting their status to 'publish'. + * + * @param string $object_id The ID of the activity object. + * @param string $activity_type The type of the activity. + * @param int $current_id The ID of the current outbox item to exclude. + * + * @return void + */ + private static function invalidate_existing_items( $object_id, $activity_type, $current_id ) { + // Do not invalidate items for Announce activities. + if ( 'Announce' === $activity_type ) { + return; + } + + $meta_query = array( + array( + 'key' => '_activitypub_object_id', + 'value' => $object_id, + ), + ); + + // For non-Delete activities, only invalidate items of the same type. + if ( 'Delete' !== $activity_type ) { + $meta_query[] = array( + 'key' => '_activitypub_activity_type', + 'value' => $activity_type, + ); + } + + $existing_items = get_posts( + array( + 'post_type' => self::POST_TYPE, + 'post_status' => 'pending', + 'exclude' => array( $current_id ), + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + 'meta_query' => $meta_query, + 'fields' => 'ids', + ) + ); + + foreach ( $existing_items as $existing_item_id ) { + $event_args = array( + Dispatcher::$callback, + $existing_item_id, + Dispatcher::$batch_size, + \get_post_meta( $existing_item_id, '_activitypub_outbox_offset', true ) ?: 0, // phpcs:ignore + ); + + $timestamp = \wp_next_scheduled( 'activitypub_async_batch', $event_args ); + \wp_unschedule_event( $timestamp, 'activitypub_async_batch', $event_args ); + + $timestamp = \wp_next_scheduled( 'activitypub_process_outbox', array( $existing_item_id ) ); + \wp_unschedule_event( $timestamp, 'activitypub_process_outbox', array( $existing_item_id ) ); + + \wp_publish_post( $existing_item_id ); + \delete_post_meta( $existing_item_id, '_activitypub_outbox_offset' ); + } + } + + /** + * Creates an Undo activity. + * + * @param int|\WP_Post $outbox_item The Outbox post or post ID. + * + * @return int|bool The ID of the outbox item or false on failure. + */ + public static function undo( $outbox_item ) { + $outbox_item = get_post( $outbox_item ); + $activity = self::get_activity( $outbox_item ); + + $type = 'Undo'; + if ( 'Create' === $activity->get_type() ) { + $type = 'Delete'; + } elseif ( 'Add' === $activity->get_type() ) { + $type = 'Remove'; + } + + return add_to_outbox( $activity, $type, $outbox_item->post_author ); + } + + /** + * Reschedule an activity. + * + * @param int|\WP_Post $outbox_item The Outbox post or post ID. + * + * @return bool True if the activity was rescheduled, false otherwise. + */ + public static function reschedule( $outbox_item ) { + $outbox_item = get_post( $outbox_item ); + + $outbox_item->post_status = 'pending'; + $outbox_item->post_date = current_time( 'mysql' ); + + wp_update_post( $outbox_item ); + + Scheduler::schedule_outbox_activity_for_federation( $outbox_item->ID ); + + return true; + } + + /** + * Get the Activity object from the Outbox item. + * + * @param int|\WP_Post $outbox_item The Outbox post or post ID. + * @return Activity|\WP_Error The Activity object or WP_Error. + */ + public static function get_activity( $outbox_item ) { + $outbox_item = get_post( $outbox_item ); + $actor = self::get_actor( $outbox_item ); + if ( is_wp_error( $actor ) ) { + return $actor; + } + + $activity_object = \json_decode( $outbox_item->post_content, true ); + $type = \get_post_meta( $outbox_item->ID, '_activitypub_activity_type', true ); + + if ( $activity_object['type'] === $type ) { + $activity = Activity::init_from_array( $activity_object ); + if ( ! $activity->get_actor() ) { + $activity->set_actor( $actor->get_id() ); + } + } else { + $activity = new Activity(); + $activity->set_type( $type ); + $activity->set_id( $outbox_item->guid ); + $activity->set_actor( $actor->get_id() ); + // Pre-fill the Activity with data (for example cc and to). + $activity->set_object( $activity_object ); + } + + if ( 'Update' === $type ) { + $activity->set_updated( gmdate( ACTIVITYPUB_DATE_TIME_RFC3339, strtotime( $outbox_item->post_modified ) ) ); + } + + /** + * Filters the Activity object before it is returned. + * + * @param Activity $activity The Activity object. + * @param \WP_Post $outbox_item The outbox item post object. + */ + return apply_filters( 'activitypub_get_outbox_activity', $activity, $outbox_item ); + } + + /** + * Get the Actor object from the Outbox item. + * + * @param \WP_Post $outbox_item The Outbox post. + * + * @return \Activitypub\Model\User|\Activitypub\Model\Blog|\WP_Error The Actor object or WP_Error. + */ + public static function get_actor( $outbox_item ) { + $actor_type = \get_post_meta( $outbox_item->ID, '_activitypub_activity_actor', true ); + + switch ( $actor_type ) { + case 'blog': + $actor_id = Actors::BLOG_USER_ID; + break; + case 'application': + $actor_id = Actors::APPLICATION_USER_ID; + break; + case 'user': + default: + $actor_id = $outbox_item->post_author; + break; + } + + return Actors::get_by_id( $actor_id ); + } + + /** + * Get the Activity object from the Outbox item. + * + * @param \WP_Post $outbox_item The Outbox post. + * + * @return Activity|\WP_Error The Activity object or WP_Error. + */ + public static function maybe_get_activity( $outbox_item ) { + if ( ! $outbox_item instanceof \WP_Post ) { + return new \WP_Error( 'invalid_outbox_item', 'Invalid Outbox item.' ); + } + + if ( 'ap_outbox' !== $outbox_item->post_type ) { + return new \WP_Error( 'invalid_outbox_item', 'Invalid Outbox item.' ); + } + + // Check if Outbox Activity is public. + $visibility = \get_post_meta( $outbox_item->ID, 'activitypub_content_visibility', true ); + + if ( ! in_array( $visibility, array( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC ), true ) ) { + return new \WP_Error( 'private_outbox_item', 'Not a public Outbox item.' ); + } + + $activity_types = \apply_filters( 'rest_activitypub_outbox_activity_types', array( 'Announce', 'Create', 'Like', 'Update' ) ); + $activity_type = \get_post_meta( $outbox_item->ID, '_activitypub_activity_type', true ); + + if ( ! in_array( $activity_type, $activity_types, true ) ) { + return new \WP_Error( 'private_outbox_item', 'Not public Outbox item type.' ); + } + + return self::get_activity( $outbox_item ); + } + + /** + * Get the object ID of an activity. + * + * @param Activity|Base_Object|string $data The activity object. + * + * @return string The object ID. + */ + private static function get_object_id( $data ) { + $object = $data->get_object(); + + if ( is_object( $object ) ) { + return self::get_object_id( $object ); + } + + if ( is_string( $object ) ) { + return $object; + } + + return $data->get_id() ?? $data->get_actor(); + } + + /** + * Get the title of an activity recursively. + * + * @param Base_Object $activity_object The activity object. + * + * @return string The title. + */ + private static function get_object_title( $activity_object ) { + if ( ! $activity_object ) { + return ''; + } + + if ( is_string( $activity_object ) ) { + $post_id = url_to_postid( $activity_object ); + + return $post_id ? get_the_title( $post_id ) : ''; + } + + $title = $activity_object->get_name() ?? $activity_object->get_content(); + + if ( ! $title && $activity_object->get_object() instanceof Base_Object ) { + $title = $activity_object->get_object()->get_name() ?? $activity_object->get_object()->get_content(); + } + + return $title; + } +} diff --git a/wp-content/plugins/activitypub/includes/collection/class-replies.php b/wp-content/plugins/activitypub/includes/collection/class-replies.php index 34a5aa02..25b47d21 100644 --- a/wp-content/plugins/activitypub/includes/collection/class-replies.php +++ b/wp-content/plugins/activitypub/includes/collection/class-replies.php @@ -12,9 +12,14 @@ use WP_Comment; use WP_Error; use Activitypub\Comment; +use Activitypub\Model\Blog; +use Activitypub\Transformer\Post as PostTransformer; +use Activitypub\Transformer\Comment as CommentTransformer; +use function Activitypub\is_post_disabled; use function Activitypub\is_local_comment; use function Activitypub\get_rest_url_by_path; +use function Activitypub\is_user_type_disabled; /** * Class containing code for getting replies Collections and CollectionPages of posts and comments. @@ -23,13 +28,14 @@ class Replies { /** * Build base arguments for fetching the comments of either a WordPress post or comment. * - * @param WP_Post|WP_Comment $wp_object The post or comment to fetch replies for. + * @param WP_Post|WP_Comment|WP_Error $wp_object The post or comment to fetch replies for on success. */ private static function build_args( $wp_object ) { $args = array( 'status' => 'approve', 'orderby' => 'comment_date_gmt', 'order' => 'ASC', + 'type' => 'comment', ); if ( $wp_object instanceof WP_Post ) { @@ -44,23 +50,6 @@ class Replies { 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. * @@ -74,22 +63,22 @@ class Replies { } 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(); + return new WP_Error( 'unsupported_object', 'The object is not a post or comment.' ); } } /** - * Get the replies collection. + * 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. + * @return array|\WP_Error|null An associative array containing the replies collection without JSON-LD context on success. */ public static function get_collection( $wp_object ) { $id = self::get_id( $wp_object ); - if ( ! $id ) { - return null; + if ( is_wp_error( $id ) ) { + return \wp_is_serving_rest_request() ? $id : null; } $replies = array( @@ -97,38 +86,11 @@ class Replies { 'type' => 'Collection', ); - $replies['first'] = self::get_collection_page( $wp_object, 0, $replies['id'] ); + $replies['first'] = self::get_collection_page( $wp_object, 1, $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. * @@ -136,33 +98,34 @@ class Replies { * * @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. + * @param string $part_of Optional. The collection id/url the returned CollectionPage belongs to. Default null. * - * @return array A CollectionPage as an associative array. + * @return array|WP_Error|null A CollectionPage as an associative array on success, WP_Error or null on failure. */ 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 ); + if ( is_wp_error( $args ) ) { + return \wp_is_serving_rest_request() ? $args : null; + } // 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; + if ( is_wp_error( $part_of ) ) { + return \wp_is_serving_rest_request() ? $part_of : 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' ); + // If set to zero, we get errors below. You need at least one comment per page, here. + $args['number'] = max( (int) \get_option( 'comments_per_page' ), 1 ); + $args['offset'] = intval( $page - 1 ) * $args['number']; - // 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 ); + // Get the ActivityPub ID's of the comments, without local-only comments. + $comment_ids = self::get_reply_ids( \get_comments( $args ) ); // Build the associative CollectionPage array. $collection_page = array( @@ -172,10 +135,92 @@ class Replies { 'items' => $comment_ids, ); - if ( $total_replies / $comments_per_page > $page + 1 ) { + if ( ( $total_replies / $args['number'] ) > $page ) { $collection_page['next'] = \add_query_arg( 'page', $page + 1, $part_of ); } + if ( $page > 1 ) { + $collection_page['prev'] = \add_query_arg( 'page', $page - 1, $part_of ); + } + return $collection_page; } + + /** + * Get the context collection for a post. + * + * @param int $post_id The post ID. + * + * @return array|false The context for the post or false if the post is not found or disabled. + */ + public static function get_context_collection( $post_id ) { + $post = \get_post( $post_id ); + + if ( ! $post || is_post_disabled( $post_id ) ) { + return false; + } + + $comments = \get_comments( + array( + 'post_id' => $post_id, + 'type' => 'comment', + 'status' => 'approve', + 'orderby' => 'comment_date_gmt', + 'order' => 'ASC', + ) + ); + $ids = self::get_reply_ids( $comments, true ); + $post_uri = ( new PostTransformer( $post ) )->to_id(); + \array_unshift( $ids, $post_uri ); + + $author = Actors::get_by_id( $post->post_author ); + if ( is_wp_error( $author ) ) { + if ( is_user_type_disabled( 'blog' ) ) { + return false; + } + + $author = new Blog(); + } + + return array( + 'type' => 'OrderedCollection', + 'url' => \get_permalink( $post_id ), + 'attributedTo' => $author->get_id(), + 'totalItems' => count( $ids ), + 'items' => $ids, + ); + } + + /** + * 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. + * @param boolean $include_blog_comments Optional. Include blog comments in the returned array. Default false. + * + * @return string[] A list of the ActivityPub ID's. + */ + private static function get_reply_ids( $comments, $include_blog_comments = false ) { + $comment_ids = array(); + + 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; + continue; + } + + if ( $include_blog_comments ) { + $comment_ids[] = ( new CommentTransformer( $comment ) )->to_id(); + } + } + + return \array_unique( $comment_ids ); + } } diff --git a/wp-content/plugins/activitypub/includes/collection/class-users.php b/wp-content/plugins/activitypub/includes/collection/class-users.php index 036d210a..12194903 100644 --- a/wp-content/plugins/activitypub/includes/collection/class-users.php +++ b/wp-content/plugins/activitypub/includes/collection/class-users.php @@ -7,36 +7,12 @@ namespace Activitypub\Collection; -use WP_Error; -use WP_User_Query; -use Activitypub\Model\User; -use Activitypub\Model\Blog; -use Activitypub\Model\Application; - -use function Activitypub\object_to_uri; -use function Activitypub\normalize_url; -use function Activitypub\normalize_host; -use function Activitypub\url_to_authorid; -use function Activitypub\is_user_disabled; - /** * Users collection. + * + * @deprecated version 4.2.0 */ -class Users { - /** - * The ID of the Blog User. - * - * @var int - */ - const BLOG_USER_ID = 0; - - /** - * The ID of the Application User. - * - * @var int - */ - const APPLICATION_USER_ID = -1; - +class Users extends Actors { /** * Get the User by ID. * @@ -45,31 +21,9 @@ class Users { * @return User|Blog|Application|WP_Error The User or WP_Error if user not found. */ public static function get_by_id( $user_id ) { - if ( is_string( $user_id ) || is_numeric( $user_id ) ) { - $user_id = (int) $user_id; - } + _deprecated_function( __METHOD__, '4.2.0', 'Activitypub\Collection\Actors::get_by_id' ); - if ( is_user_disabled( $user_id ) ) { - return new WP_Error( - 'activitypub_user_not_found', - \__( 'User not found', 'activitypub' ), - array( 'status' => 404 ) - ); - } - - if ( self::BLOG_USER_ID === $user_id ) { - return new Blog(); - } elseif ( self::APPLICATION_USER_ID === $user_id ) { - return new Application(); - } elseif ( $user_id > 0 ) { - return User::from_wp_user( $user_id ); - } - - return new WP_Error( - 'activitypub_user_not_found', - \__( 'User not found', 'activitypub' ), - array( 'status' => 404 ) - ); + return parent::get_by_id( $user_id ); } /** @@ -80,66 +34,9 @@ class Users { * @return User|Blog|Application|WP_Error The User or WP_Error if user not found. */ public static function get_by_username( $username ) { - // Check for blog user. - if ( Blog::get_default_username() === $username ) { - return new Blog(); - } + _deprecated_function( __METHOD__, '4.2.0', 'Activitypub\Collection\Actors::get_by_username' ); - if ( get_option( 'activitypub_blog_identifier' ) === $username ) { - return new Blog(); - } - - // Check for application user. - if ( 'application' === $username ) { - return new Application(); - } - - // Check for 'activitypub_username' meta. - $user = new WP_User_Query( - array( - 'count_total' => false, - 'number' => 1, - 'hide_empty' => true, - 'fields' => 'ID', - // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query - 'meta_query' => array( - 'relation' => 'OR', - array( - 'key' => 'activitypub_user_identifier', - 'value' => $username, - 'compare' => 'LIKE', - ), - ), - ) - ); - - if ( $user->results ) { - return self::get_by_id( $user->results[0] ); - } - - $username = str_replace( array( '*', '%' ), '', $username ); - - // Check for login or nicename. - $user = new WP_User_Query( - array( - 'count_total' => false, - 'search' => $username, - 'search_columns' => array( 'user_login', 'user_nicename' ), - 'number' => 1, - 'hide_empty' => true, - 'fields' => 'ID', - ) - ); - - if ( $user->results ) { - return self::get_by_id( $user->results[0] ); - } - - return new WP_Error( - 'activitypub_user_not_found', - \__( 'User not found', 'activitypub' ), - array( 'status' => 404 ) - ); + return parent::get_by_username( $username ); } /** @@ -150,88 +47,9 @@ class Users { * @return User|WP_Error The User or WP_Error if user not found. */ public static function get_by_resource( $uri ) { - $uri = object_to_uri( $uri ); + _deprecated_function( __METHOD__, '4.2.0', 'Activitypub\Collection\Actors::get_by_resource' ); - $scheme = 'acct'; - $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. - case 'http': - case 'https': - $resource_path = \wp_parse_url( $uri, PHP_URL_PATH ); - - if ( $resource_path ) { - $blog_path = \wp_parse_url( \home_url(), PHP_URL_PATH ); - - if ( $blog_path ) { - $resource_path = \str_replace( $blog_path, '', $resource_path ); - } - - $resource_path = \trim( $resource_path, '/' ); - - // Check for http(s)://blog.example.com/@username. - if ( str_starts_with( $resource_path, '@' ) ) { - $identifier = \str_replace( '@', '', $resource_path ); - $identifier = \trim( $identifier, '/' ); - - return self::get_by_username( $identifier ); - } - } - - // Check for http(s)://blog.example.com/author/username. - $user_id = url_to_authorid( $uri ); - - if ( $user_id ) { - return self::get_by_id( $user_id ); - } - - // Check for http(s)://blog.example.com/. - if ( - normalize_url( site_url() ) === normalize_url( $uri ) || - normalize_url( home_url() ) === normalize_url( $uri ) - ) { - return self::get_by_id( self::BLOG_USER_ID ); - } - - return new WP_Error( - 'activitypub_no_user_found', - \__( 'User not found', 'activitypub' ), - array( 'status' => 404 ) - ); - // Check for acct URIs. - case 'acct': - $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 ) { - return new WP_Error( - 'activitypub_wrong_host', - \__( 'Resource host does not match blog host', 'activitypub' ), - array( 'status' => 404 ) - ); - } - - // Prepare wildcards https://github.com/mastodon/mastodon/issues/22213. - if ( in_array( $identifier, array( '_', '*', '' ), true ) ) { - return self::get_by_id( self::BLOG_USER_ID ); - } - - return self::get_by_username( $identifier ); - default: - return new WP_Error( - 'activitypub_wrong_scheme', - \__( 'Wrong scheme', 'activitypub' ), - array( 'status' => 404 ) - ); - } + return parent::get_by_resource( $uri ); } /** @@ -242,26 +60,9 @@ class Users { * @return User|Blog|Application|WP_Error The User or WP_Error if user not found. */ public static function get_by_various( $id ) { - $user = null; + _deprecated_function( __METHOD__, '4.2.0', 'Activitypub\Collection\Actors::get_by_various' ); - if ( is_numeric( $id ) ) { - $user = self::get_by_id( $id ); - } elseif ( - // Is URL. - filter_var( $id, FILTER_VALIDATE_URL ) || - // Is acct. - str_starts_with( $id, 'acct:' ) || - // Is email. - filter_var( $id, FILTER_VALIDATE_EMAIL ) - ) { - $user = self::get_by_resource( $id ); - } - - if ( $user && ! is_wp_error( $user ) ) { - return $user; - } - - return self::get_by_username( $id ); + return parent::get_by_various( $id ); } /** @@ -270,18 +71,8 @@ class Users { * @return array The User collection. */ public static function get_collection() { - $users = \get_users( - array( - 'capability__in' => array( 'activitypub' ), - ) - ); + _deprecated_function( __METHOD__, '4.2.0', 'Activitypub\Collection\Actors::get_collection' ); - $return = array(); - - foreach ( $users as $user ) { - $return[] = User::from_wp_user( $user->ID ); - } - - return $return; + return parent::get_collection(); } } diff --git a/wp-content/plugins/activitypub/includes/compat.php b/wp-content/plugins/activitypub/includes/compat.php index fa8627b7..c5a73a1a 100644 --- a/wp-content/plugins/activitypub/includes/compat.php +++ b/wp-content/plugins/activitypub/includes/compat.php @@ -25,25 +25,6 @@ if ( ! function_exists( 'str_starts_with' ) ) { } } -if ( ! function_exists( 'get_self_link' ) ) { - /** - * Returns the link for the currently displayed feed. - * - * @return string Correct link for the atom:self element. - */ - function get_self_link() { - $host = wp_parse_url( home_url() ); - $path = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; - - /** - * Filters the self link. - * - * @param string $link The self link. - */ - return esc_url( apply_filters( 'self_link', set_url_scheme( 'http://' . $host['host'] . $path ) ) ); - } -} - if ( ! function_exists( 'is_countable' ) ) { /** * Polyfill for `is_countable()` function added in PHP 7.3. @@ -115,3 +96,16 @@ if ( ! function_exists( 'str_contains' ) ) { return false !== strpos( $haystack, $needle ); } } + +if ( ! function_exists( 'wp_is_serving_rest_request' ) ) { + /** + * Polyfill for `wp_is_serving_rest_request()` function added in WordPress 6.5. + * + * @see https://developer.wordpress.org/reference/functions/wp_is_serving_rest_request/ + * + * @return bool True if it's a WordPress REST API request, false otherwise. + */ + function wp_is_serving_rest_request() { + return defined( 'REST_REQUEST' ) && REST_REQUEST; + } +} diff --git a/wp-content/plugins/activitypub/includes/constants.php b/wp-content/plugins/activitypub/includes/constants.php new file mode 100644 index 00000000..8b6c92d0 --- /dev/null +++ b/wp-content/plugins/activitypub/includes/constants.php @@ -0,0 +1,76 @@ +)|(?<=' . __( 'The following Template Tags are available:', 'activitypub' ) . '
' . - '[ap_title]
[ap_content apply_filters="yes"]
apply_filters
you can decide if filters (apply_filters( \'the_content\', $content )
) should be applied or not (default is yes
). The values can be yes
or no
. apply_filters
attribute is optional.', 'activitypub' ), array( 'code' => array() ) ) . '[ap_excerpt length="400"]
the_excerpt
if that is set). If no excerpt is provided, will truncate at length
(optional, default = 400).', 'activitypub' ), array( 'code' => array() ) ) . '[ap_permalink type="url"]
type
can be either: url
or html
(an <a /> tag). type
attribute is optional.', 'activitypub' ), array( 'code' => array() ) ) . '[ap_shortlink type="url"]
type
can be either url
or html
(an <a /> tag). I can recommend Hum, to prettify the Shortlinks. type
attribute is optional.', 'activitypub' ), array( 'code' => array() ) ) . '[ap_hashtags]
[ap_hashcats]
[ap_image type=full]
thumbnail
, medium
, large
, full
. type
attribute is optional.', 'activitypub' ), array( 'code' => array() ) ) . '[ap_author]
[ap_authorurl]
[ap_date]
[ap_time]
[ap_datetime]
[ap_blogurl]
[ap_blogname]
[ap_blogdesc]
' . __( 'You may also use any Shortcode normally available to you on your site, however be aware that Shortcodes may significantly increase the size of your content depending on what they do.', 'activitypub' ) . '
' . - '' . __( 'Note: the old Template Tags are now deprecated and automatically converted to the new ones.', 'activitypub' ) . '
' . - '' . \wp_kses( \__( 'Let me know if you miss a Template Tag.', 'activitypub' ), 'activitypub' ) . '
', - ) -); - -\get_current_screen()->add_help_tab( - array( - 'id' => 'glossary', - 'title' => \__( 'Glossary', 'activitypub' ), - 'content' => - '' . \__( '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 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 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 information please visit nodeinfo.diaspora.software', 'activitypub' ) . '
', - ) -); - -\get_current_screen()->set_help_sidebar( - '' . \__( 'For more information:', 'activitypub' ) . '
' . - '' . \__( 'Get support', 'activitypub' ) . '
' . - '' . \__( 'Report an issue', 'activitypub' ) . '
' -); diff --git a/wp-content/plugins/activitypub/includes/model/class-application.php b/wp-content/plugins/activitypub/includes/model/class-application.php index b7d38af2..0309941e 100644 --- a/wp-content/plugins/activitypub/includes/model/class-application.php +++ b/wp-content/plugins/activitypub/includes/model/class-application.php @@ -10,12 +10,14 @@ namespace Activitypub\Model; use WP_Query; use Activitypub\Signature; use Activitypub\Activity\Actor; -use Activitypub\Collection\Users; +use Activitypub\Collection\Actors; use function Activitypub\get_rest_url_by_path; /** * Application class. + * + * @method int get__id() Gets the internal user ID for the application (always returns APPLICATION_USER_ID). */ class Application extends Actor { /** @@ -23,7 +25,7 @@ class Application extends Actor { * * @var int */ - protected $_id = Users::APPLICATION_USER_ID; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore + protected $_id = Actors::APPLICATION_USER_ID; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore /** * Whether the Application is discoverable. @@ -94,7 +96,7 @@ class Application extends Actor { * @return string The User-URL with @-Prefix for the username. */ public function get_alternate_url() { - return $this->get_url(); + return $this->get_id(); } /** @@ -118,7 +120,7 @@ class Application extends Actor { /** * Get the User-Icon. * - * @return array The User-Icon. + * @return string[] The User-Icon. */ public function get_icon() { // Try site icon first. @@ -152,7 +154,7 @@ class Application extends Actor { /** * Get the User-Header-Image. * - * @return array|null The User-Header-Image. + * @return string[]|null The User-Header-Image. */ public function get_header_image() { if ( \has_header_image() ) { @@ -185,7 +187,7 @@ class Application extends Actor { $time = \time(); } - return \gmdate( 'Y-m-d\TH:i:s\Z', $time ); + return \gmdate( ACTIVITYPUB_DATE_TIME_RFC3339, $time ); } /** @@ -218,13 +220,13 @@ class Application extends Actor { /** * Returns the public key. * - * @return array The public key. + * @return string[] The public key. */ public function get_public_key() { return array( 'id' => $this->get_id() . '#main-key', 'owner' => $this->get_id(), - 'publicKeyPem' => Signature::get_public_key_for( Users::APPLICATION_USER_ID ), + 'publicKeyPem' => Signature::get_public_key_for( Actors::APPLICATION_USER_ID ), ); } diff --git a/wp-content/plugins/activitypub/includes/model/class-blog.php b/wp-content/plugins/activitypub/includes/model/class-blog.php index 3cbd6f91..e46e5b02 100644 --- a/wp-content/plugins/activitypub/includes/model/class-blog.php +++ b/wp-content/plugins/activitypub/includes/model/class-blog.php @@ -7,20 +7,22 @@ namespace Activitypub\Model; -use WP_Query; - -use Activitypub\Signature; use Activitypub\Activity\Actor; -use Activitypub\Collection\Users; +use Activitypub\Collection\Actors; use Activitypub\Collection\Extra_Fields; +use Activitypub\Signature; +use WP_Query; use function Activitypub\esc_hashtag; use function Activitypub\is_single_user; use function Activitypub\is_blog_public; use function Activitypub\get_rest_url_by_path; +use function Activitypub\get_attribution_domains; /** * Blog class. + * + * @method int get__id() Gets the internal user ID for the blog (always returns BLOG_USER_ID). */ class Blog extends Actor { /** @@ -51,7 +53,7 @@ class Blog extends Actor { * * @var int */ - protected $_id = Users::BLOG_USER_ID; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore + protected $_id = Actors::BLOG_USER_ID; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore /** * If the User is indexable. @@ -89,6 +91,18 @@ class Blog extends Actor { */ protected $posting_restricted_to_mods; + /** + * Constructor. + */ + public function __construct() { + /** + * Fires when a model actor is constructed. + * + * @param Blog $this The Blog model. + */ + \do_action( 'activitypub_construct_model_actor', $this ); + } + /** * Whether the User manually approves followers. * @@ -113,13 +127,25 @@ class Blog extends Actor { * @return string The User ID. */ public function get_id() { - return $this->get_url(); + $id = parent::get_id(); + + if ( $id ) { + return $id; + } + + $permalink = \get_option( 'activitypub_use_permalink_as_id_for_blog', false ); + + if ( $permalink ) { + return $this->get_url(); + } + + return \add_query_arg( 'author', $this->_id, \trailingslashit( \home_url() ) ); } /** * Get the type of the object. * - * If the Blog is in "single user" mode, return "Person" insted of "Group". + * If the Blog is in "single user" mode, return "Person" instead of "Group". * * @return string The type of the object. */ @@ -197,7 +223,11 @@ class Blog extends Actor { /** * Filters the default blog username. * - * @param string $host The default username. + * This filter allows developers to modify the default username that is + * generated for the blog, which by default is the site's host name + * without the 'www.' prefix. + * + * @param string $host The default username (site's host name). */ return apply_filters( 'activitypub_default_blog_username', $host ); } @@ -220,7 +250,7 @@ class Blog extends Actor { /** * Get the User icon. * - * @return array The User icon. + * @return string[] The User icon. */ public function get_icon() { // Try site_logo, falling back to site_icon, first. @@ -254,7 +284,7 @@ class Blog extends Actor { /** * Get the User-Header-Image. * - * @return array|null The User-Header-Image. + * @return string[]|null The User-Header-Image. */ public function get_image() { $header_image = get_option( 'activitypub_header_image' ); @@ -298,7 +328,7 @@ class Blog extends Actor { $time = \time(); } - return \gmdate( 'Y-m-d\TH:i:s\Z', $time ); + return \gmdate( ACTIVITYPUB_DATE_TIME_RFC3339, $time ); } /** @@ -339,7 +369,7 @@ class Blog extends Actor { /** * Get the public key information. * - * @return array The public key. + * @return string[] The public key. */ public function get_public_key() { return array( @@ -401,12 +431,12 @@ class Blog extends Actor { /** * Returns endpoints. * - * @return array|null The endpoints. + * @return string[]|null The endpoints. */ public function get_endpoints() { $endpoints = null; - if ( ACTIVITYPUB_SHARED_INBOX_FEATURE ) { + if ( \get_option( 'activitypub_shared_inbox' ) ) { $endpoints = array( 'sharedInbox' => get_rest_url_by_path( 'inbox' ), ); @@ -497,7 +527,7 @@ class Blog extends Actor { * * @see https://docs.joinmastodon.org/spec/activitypub/#Hashtag * - * @return array The User - Hashtags. + * @return string[] The User - Hashtags. */ public function get_tag() { $hashtags = array(); @@ -530,4 +560,41 @@ class Blog extends Actor { $extra_fields = Extra_Fields::get_actor_fields( $this->_id ); return Extra_Fields::fields_to_attachments( $extra_fields ); } + + /** + * Returns the website hosts allowed to credit this blog. + * + * @return string[]|null The attribution domains or null if not found. + */ + public function get_attribution_domains() { + return get_attribution_domains(); + } + + /** + * Returns the alsoKnownAs. + * + * @return string[] The alsoKnownAs. + */ + public function get_also_known_as() { + $also_known_as = array( + \add_query_arg( 'author', $this->_id, \home_url( '/' ) ), + $this->get_url(), + $this->get_alternate_url(), + ); + + $also_known_as = array_merge( $also_known_as, \get_option( 'activitypub_blog_user_also_known_as', array() ) ); + + return array_unique( $also_known_as ); + } + + /** + * Returns the movedTo. + * + * @return string The movedTo. + */ + public function get_moved_to() { + $moved_to = \get_option( 'activitypub_blog_user_moved_to' ); + + return $moved_to && $moved_to !== $this->get_id() ? $moved_to : null; + } } diff --git a/wp-content/plugins/activitypub/includes/model/class-follower.php b/wp-content/plugins/activitypub/includes/model/class-follower.php index 45ef6157..c7d50ea2 100644 --- a/wp-content/plugins/activitypub/includes/model/class-follower.php +++ b/wp-content/plugins/activitypub/includes/model/class-follower.php @@ -21,6 +21,18 @@ use Activitypub\Collection\Followers; * @author Matthias Pfefferle * * @see https://www.w3.org/TR/activitypub/#follow-activity-inbox + * + * @method int get__id() Gets the post ID of the follower record. + * @method string[]|null get_image() Gets the follower's profile image data. + * @method string|null get_inbox() Gets the follower's ActivityPub inbox URL. + * @method string[]|null get_endpoints() Gets the follower's ActivityPub endpoints. + * + * @method Follower set__id( int $id ) Sets the post ID of the follower record. + * @method Follower set_id( string $guid ) Sets the follower's GUID. + * @method Follower set_name( string $name ) Sets the follower's display name. + * @method Follower set_summary( string $summary ) Sets the follower's bio/summary. + * @method Follower set_published( string $datetime ) Sets the follower's published datetime in ISO 8601 format. + * @method Follower set_updated( string $datetime ) Sets the follower's last updated datetime in ISO 8601 format. */ class Follower extends Actor { /** @@ -36,7 +48,7 @@ class Follower extends Actor { * @return mixed */ public function get_errors() { - return get_post_meta( $this->_id, 'activitypub_errors', false ); + return get_post_meta( $this->_id, '_activitypub_errors', false ); } /** @@ -72,7 +84,7 @@ class Follower extends Actor { * Reset (delete) all errors. */ public function reset_errors() { - delete_post_meta( $this->_id, 'activitypub_errors' ); + delete_post_meta( $this->_id, '_activitypub_errors' ); } /** @@ -216,9 +228,9 @@ class Follower extends Actor { * Update the post meta. */ protected function get_post_meta_input() { - $meta_input = array(); - $meta_input['activitypub_inbox'] = $this->get_shared_inbox(); - $meta_input['activitypub_actor_json'] = $this->to_json(); + $meta_input = array(); + $meta_input['_activitypub_inbox'] = $this->get_shared_inbox(); + $meta_input['_activitypub_actor_json'] = wp_slash( $this->to_json() ); return $meta_input; } @@ -228,7 +240,7 @@ class Follower extends Actor { * * Sets a fallback to better handle API and HTML outputs. * - * @return array The icon. + * @return string[] The icon. */ public function get_icon() { if ( isset( $this->icon['url'] ) ) { @@ -331,11 +343,18 @@ class Follower extends Actor { * Convert a Custom-Post-Type input to an Activitypub\Model\Follower. * * @param \WP_Post $post The post object. - * @return \Activitypub\Activity\Base_Object|WP_Error + * @return Follower|false The Follower object or false on failure. */ public static function init_from_cpt( $post ) { - $actor_json = get_post_meta( $post->ID, 'activitypub_actor_json', true ); - $object = self::init_from_json( $actor_json ); + $actor_json = get_post_meta( $post->ID, '_activitypub_actor_json', true ); + + /* @var Follower $object Follower object. */ + $object = self::init_from_json( $actor_json ); + + if ( is_wp_error( $object ) ) { + return false; + } + $object->set__id( $post->ID ); $object->set_id( $post->guid ); $object->set_name( $post->post_title ); diff --git a/wp-content/plugins/activitypub/includes/model/class-user.php b/wp-content/plugins/activitypub/includes/model/class-user.php index 997c5210..964c7867 100644 --- a/wp-content/plugins/activitypub/includes/model/class-user.php +++ b/wp-content/plugins/activitypub/includes/model/class-user.php @@ -7,17 +7,20 @@ namespace Activitypub\Model; -use WP_Error; -use Activitypub\Signature; use Activitypub\Activity\Actor; use Activitypub\Collection\Extra_Fields; +use Activitypub\Http; +use Activitypub\Signature; use function Activitypub\is_blog_public; -use function Activitypub\is_user_disabled; use function Activitypub\get_rest_url_by_path; +use function Activitypub\get_attribution_domains; +use function Activitypub\user_can_activitypub; /** * User class. + * + * @method int get__id() Gets the WordPress user ID. */ class User extends Actor { /** @@ -68,6 +71,24 @@ class User extends Actor { */ protected $webfinger; + /** + * Constructor. + * + * @param int $user_id Optional. The WordPress user ID. Default null. + */ + public function __construct( $user_id = null ) { + if ( $user_id ) { + $this->_id = $user_id; + + /** + * Fires when a model actor is constructed. + * + * @param User $this The User object. + */ + \do_action( 'activitypub_construct_model_actor', $this ); + } + } + /** * The type of the object. * @@ -82,21 +103,18 @@ class User extends Actor { * * @param int $user_id The user ID. * - * @return WP_Error|User The User object or WP_Error if user not found. + * @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( + if ( ! user_can_activitypub( $user_id ) ) { + return new \WP_Error( 'activitypub_user_not_found', \__( 'User not found', 'activitypub' ), array( 'status' => 404 ) ); } - $object = new static(); - $object->_id = $user_id; - - return $object; + return new static( $user_id ); } /** @@ -105,7 +123,19 @@ class User extends Actor { * @return string The user ID. */ public function get_id() { - return $this->get_url(); + $id = parent::get_id(); + + if ( $id ) { + return $id; + } + + $permalink = \get_user_option( 'activitypub_use_permalink_as_id', $this->_id ); + + if ( '1' === $permalink ) { + return $this->get_url(); + } + + return \add_query_arg( 'author', $this->_id, \trailingslashit( \home_url() ) ); } /** @@ -114,7 +144,7 @@ class User extends Actor { * @return string The Username. */ public function get_name() { - return \esc_attr( \get_the_author_meta( 'display_name', $this->_id ) ); + return \get_the_author_meta( 'display_name', $this->_id ); } /** @@ -154,17 +184,17 @@ class User extends Actor { * @return string The preferred username. */ public function get_preferred_username() { - return \esc_attr( \get_the_author_meta( 'login', $this->_id ) ); + return \get_the_author_meta( 'login', $this->_id ); } /** * Get the User icon. * - * @return array The User icon. + * @return string[] The User icon. */ public function get_icon() { $icon = \get_user_option( 'activitypub_icon', $this->_id ); - if ( wp_attachment_is_image( $icon ) ) { + if ( false !== $icon && wp_attachment_is_image( $icon ) ) { return array( 'type' => 'Image', 'url' => esc_url( wp_get_attachment_url( $icon ) ), @@ -187,7 +217,7 @@ class User extends Actor { /** * Returns the header image. * - * @return array|null The header image. + * @return string[]|null The header image. */ public function get_image() { $header_image = get_user_option( 'activitypub_header_image', $this->_id ); @@ -217,13 +247,13 @@ class User extends Actor { * @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 ) ) ); + return \gmdate( ACTIVITYPUB_DATE_TIME_RFC3339, \strtotime( \get_the_author_meta( 'registered', $this->_id ) ) ); } /** * Returns the public key. * - * @return array The public key. + * @return string[] The public key. */ public function get_public_key() { return array( @@ -281,12 +311,12 @@ class User extends Actor { /** * Returns the endpoints. * - * @return array|null The endpoints. + * @return string[]|null The endpoints. */ public function get_endpoints() { $endpoints = null; - if ( ACTIVITYPUB_SHARED_INBOX_FEATURE ) { + if ( \get_option( 'activitypub_shared_inbox' ) ) { $endpoints = array( 'sharedInbox' => get_rest_url_by_path( 'inbox' ), ); @@ -358,7 +388,7 @@ class User extends Actor { * Update the username. * * @param string $value The new value. - * @return int|WP_Error The updated user ID or WP_Error on failure. + * @return int|\WP_Error The updated user ID or \WP_Error on failure. */ public function update_name( $value ) { $userdata = array( @@ -403,4 +433,42 @@ class User extends Actor { } return \update_user_option( $this->_id, 'activitypub_header_image', $value ); } + + /** + * Returns the website hosts allowed to credit this blog. + * + * @return string[]|null The attribution domains or null if not found. + */ + public function get_attribution_domains() { + return get_attribution_domains(); + } + + /** + * Returns the alsoKnownAs. + * + * @return string[] The alsoKnownAs. + */ + public function get_also_known_as() { + $also_known_as = array( + \add_query_arg( 'author', $this->_id, \home_url( '/' ) ), + $this->get_url(), + $this->get_alternate_url(), + ); + + // phpcs:ignore Universal.Operators.DisallowShortTernary.Found + $also_known_as = array_merge( $also_known_as, \get_user_option( 'activitypub_also_known_as', $this->_id ) ?: array() ); + + return array_unique( $also_known_as ); + } + + /** + * Returns the movedTo. + * + * @return string The movedTo. + */ + public function get_moved_to() { + $moved_to = \get_user_option( 'activitypub_moved_to', $this->_id ); + + return $moved_to && $moved_to !== $this->get_id() ? $moved_to : null; + } } diff --git a/wp-content/plugins/activitypub/includes/rest/class-actors-controller.php b/wp-content/plugins/activitypub/includes/rest/class-actors-controller.php new file mode 100644 index 00000000..15cd0c5c --- /dev/null +++ b/wp-content/plugins/activitypub/includes/rest/class-actors-controller.php @@ -0,0 +1,359 @@ +[\w\-\.]+)'; + + /** + * Register routes. + */ + public function register_routes() { + \register_rest_route( + $this->namespace, + '/' . $this->rest_base, + array( + 'args' => array( + 'user_id' => array( + 'description' => 'The ID or username of the actor.', + 'type' => 'string', + 'required' => true, + 'pattern' => '[\w\-\.]+', + ), + ), + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( 'Activitypub\Rest\Server', 'verify_signature' ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + \register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/remote-follow', + array( + 'args' => array( + 'user_id' => array( + 'description' => 'The ID or username of the actor.', + 'type' => 'string', + 'required' => true, + 'pattern' => '[\w\-\.]+', + ), + ), + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_remote_follow_item' ), + 'permission_callback' => '__return_true', + 'args' => array( + 'resource' => array( + 'description' => 'The resource to follow.', + 'type' => 'string', + 'required' => true, + ), + ), + ), + ) + ); + } + + /** + * Retrieves a single actor. + * + * @param \WP_REST_Request $request Full details about the request. + * @return \WP_REST_Response|\WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_item( $request ) { + $user_id = $request->get_param( 'user_id' ); + $user = Actor_Collection::get_by_various( $user_id ); + + if ( \is_wp_error( $user ) ) { + return $user; + } + + /** + * Action triggered prior to the ActivityPub profile being created and sent to the client. + */ + \do_action( 'activitypub_rest_users_pre' ); + + $data = $user->to_array(); + + $response = \rest_ensure_response( $data ); + $response->header( 'Content-Type', 'application/activity+json; charset=' . \get_option( 'blog_charset' ) ); + $response->header( 'Link', \sprintf( '<%1$s>; rel="alternate"; type="application/activity+json"', $user->get_id() ) ); + + return $response; + } + + /** + * Retrieves the remote follow endpoint. + * + * @param \WP_REST_Request $request Full details about the request. + * @return \WP_REST_Response|\WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_remote_follow_item( $request ) { + $resource = $request->get_param( 'resource' ); + $user_id = $request->get_param( 'user_id' ); + $user = Actor_Collection::get_by_various( $user_id ); + + if ( \is_wp_error( $user ) ) { + return $user; + } + + $template = Webfinger::get_remote_follow_endpoint( $resource ); + + if ( \is_wp_error( $template ) ) { + return $template; + } + + $resource = $user->get_webfinger(); + $url = \str_replace( '{uri}', $resource, $template ); + + return \rest_ensure_response( + array( + 'url' => $url, + 'template' => $template, + ) + ); + } + + /** + * Retrieves the actor schema, conforming to JSON Schema. + * + * @return array Item schema data. + */ + public function get_item_schema() { + if ( $this->schema ) { + return $this->add_additional_fields_schema( $this->schema ); + } + + $this->schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'actor', + 'type' => 'object', + 'properties' => array( + '@context' => array( + 'description' => 'The JSON-LD context for the response.', + 'type' => array( 'array', 'object' ), + 'readonly' => true, + ), + 'id' => array( + 'description' => 'The unique identifier for the actor.', + 'type' => 'string', + 'format' => 'uri', + 'readonly' => true, + ), + 'type' => array( + 'description' => 'The type of the actor.', + 'type' => 'string', + 'enum' => array( 'Person', 'Service', 'Organization', 'Application', 'Group' ), + 'readonly' => true, + ), + 'attachment' => array( + 'description' => 'Additional information attached to the actor.', + 'type' => 'array', + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'type' => array( + 'type' => 'string', + 'enum' => array( 'PropertyValue', 'Link' ), + ), + 'name' => array( + 'type' => 'string', + ), + 'value' => array( + 'type' => 'string', + ), + 'href' => array( + 'type' => 'string', + 'format' => 'uri', + ), + 'rel' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + ), + ), + ), + 'readonly' => true, + ), + 'name' => array( + 'description' => 'The display name of the actor.', + 'type' => 'string', + 'readonly' => true, + ), + 'icon' => array( + 'description' => 'The icon/avatar of the actor.', + 'type' => 'object', + 'properties' => array( + 'type' => array( + 'type' => 'string', + ), + 'url' => array( + 'type' => 'string', + 'format' => 'uri', + ), + ), + 'readonly' => true, + ), + 'published' => array( + 'description' => 'The date the actor was published.', + 'type' => 'string', + 'format' => 'date-time', + 'readonly' => true, + ), + 'summary' => array( + 'description' => 'A summary about the actor.', + 'type' => 'string', + 'readonly' => true, + ), + 'tag' => array( + 'description' => 'Tags associated with the actor.', + 'type' => 'array', + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'type' => array( + 'type' => 'string', + ), + 'href' => array( + 'type' => 'string', + 'format' => 'uri', + ), + 'name' => array( + 'type' => 'string', + ), + ), + ), + 'readonly' => true, + ), + 'url' => array( + 'description' => 'The URL to the actor\'s profile page.', + 'type' => 'string', + 'format' => 'uri', + 'readonly' => true, + ), + 'inbox' => array( + 'description' => 'The inbox endpoint for the actor.', + 'type' => 'string', + 'format' => 'uri', + 'readonly' => true, + ), + 'outbox' => array( + 'description' => 'The outbox endpoint for the actor.', + 'type' => 'string', + 'format' => 'uri', + 'readonly' => true, + ), + 'following' => array( + 'description' => 'The following endpoint for the actor.', + 'type' => 'string', + 'format' => 'uri', + 'readonly' => true, + ), + 'followers' => array( + 'description' => 'The followers endpoint for the actor.', + 'type' => 'string', + 'format' => 'uri', + 'readonly' => true, + ), + 'streams' => array( + 'description' => 'The streams associated with the actor.', + 'type' => 'array', + 'readonly' => true, + ), + 'preferredUsername' => array( + 'description' => 'The preferred username of the actor.', + 'type' => 'string', + 'readonly' => true, + ), + 'publicKey' => array( + 'description' => 'The public key information for the actor.', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'type' => 'string', + 'format' => 'uri', + ), + 'owner' => array( + 'type' => 'string', + 'format' => 'uri', + ), + 'publicKeyPem' => array( + 'type' => 'string', + ), + ), + 'readonly' => true, + ), + 'manuallyApprovesFollowers' => array( + 'description' => 'Whether the actor manually approves followers.', + 'type' => 'boolean', + 'readonly' => true, + ), + 'attributionDomains' => array( + 'description' => 'The attribution domains for the actor.', + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + 'readonly' => true, + ), + 'featured' => array( + 'description' => 'The featured collection endpoint for the actor.', + 'type' => 'string', + 'format' => 'uri', + 'readonly' => true, + ), + 'indexable' => array( + 'description' => 'Whether the actor is indexable.', + 'type' => 'boolean', + 'readonly' => true, + ), + 'webfinger' => array( + 'description' => 'The webfinger identifier for the actor.', + 'type' => 'string', + 'readonly' => true, + ), + 'discoverable' => array( + 'description' => 'Whether the actor is discoverable.', + 'type' => 'boolean', + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $this->schema ); + } +} diff --git a/wp-content/plugins/activitypub/includes/rest/class-actors-inbox-controller.php b/wp-content/plugins/activitypub/includes/rest/class-actors-inbox-controller.php new file mode 100644 index 00000000..99fb5b9a --- /dev/null +++ b/wp-content/plugins/activitypub/includes/rest/class-actors-inbox-controller.php @@ -0,0 +1,238 @@ +namespace, + '/' . $this->rest_base . '/inbox', + array( + 'args' => array( + 'user_id' => array( + 'description' => 'The ID or username of the actor.', + 'type' => 'string', + 'required' => true, + 'pattern' => '[\w\-\.]+', + ), + ), + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => '__return_true', + 'args' => array( + 'page' => array( + 'description' => 'Current page of the collection.', + 'type' => 'integer', + 'minimum' => 1, + // No default so we can differentiate between Collection and CollectionPage requests. + ), + 'per_page' => array( + 'description' => 'Maximum number of items to be returned in result set.', + 'type' => 'integer', + 'default' => 20, + 'minimum' => 1, + ), + ), + 'schema' => array( $this, 'get_collection_schema' ), + ), + array( + 'methods' => \WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( 'Activitypub\Rest\Server', 'verify_signature' ), + 'args' => array( + 'id' => array( + 'description' => 'The unique identifier for the activity.', + 'type' => 'string', + 'format' => 'uri', + 'required' => true, + ), + 'actor' => array( + 'description' => 'The actor performing the activity.', + 'type' => 'string', + 'required' => true, + 'sanitize_callback' => '\Activitypub\object_to_uri', + ), + 'type' => array( + 'description' => 'The type of the activity.', + 'type' => 'string', + 'required' => true, + ), + 'object' => array( + 'description' => 'The object of the activity.', + 'required' => true, + 'validate_callback' => function ( $param, $request, $key ) { + /** + * Filter the ActivityPub object validation. + * + * @param bool $validate The validation result. + * @param array $param The object data. + * @param object $request The request object. + * @param string $key The key. + */ + return \apply_filters( 'activitypub_validate_object', true, $param, $request, $key ); + }, + ), + ), + ), + 'schema' => array( $this, 'get_item_schema' ), + ) + ); + } + + /** + * Renders the user-inbox. + * + * @param \WP_REST_Request $request The request object. + * @return \WP_REST_Response|\WP_Error Response object or WP_Error. + */ + public function get_items( $request ) { + $user_id = $request->get_param( 'user_id' ); + $user = Actors::get_by_various( $user_id ); + + if ( \is_wp_error( $user ) ) { + return $user; + } + + /** + * Fires before the ActivityPub inbox is created and sent to the client. + */ + \do_action( 'activitypub_rest_inbox_pre' ); + + $response = array( + '@context' => get_context(), + 'id' => get_rest_url_by_path( \sprintf( 'actors/%d/inbox', $user->get__id() ) ), + 'generator' => 'https://wordpress.org/?v=' . get_masked_wp_version(), + 'type' => 'OrderedCollection', + 'totalItems' => 0, + 'orderedItems' => array(), + ); + + /** + * Filters the ActivityPub inbox data before it is sent to the client. + * + * @param array $response The ActivityPub inbox array. + */ + $response = \apply_filters( 'activitypub_rest_inbox_array', $response ); + + $response = $this->prepare_collection_response( $response, $request ); + if ( \is_wp_error( $response ) ) { + return $response; + } + + /** + * Fires after the ActivityPub inbox has been created and sent to the client. + */ + \do_action( 'activitypub_inbox_post' ); + + $response = \rest_ensure_response( $response ); + $response->header( 'Content-Type', 'application/activity+json; charset=' . \get_option( 'blog_charset' ) ); + + return $response; + } + + /** + * Handles user-inbox requests. + * + * @param \WP_REST_Request $request The request object. + * + * @return \WP_REST_Response|\WP_Error Response object or WP_Error. + */ + public function create_item( $request ) { + $user_id = $request->get_param( 'user_id' ); + $user = Actors::get_by_various( $user_id ); + + if ( \is_wp_error( $user ) ) { + return $user; + } + + $data = $request->get_json_params(); + $activity = Activity::init_from_array( $data ); + $type = $request->get_param( 'type' ); + $type = \strtolower( $type ); + + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput + if ( \wp_check_comment_disallowed_list( $activity->to_json( false ), '', '', '', $_SERVER['REMOTE_ADDR'], $_SERVER['HTTP_USER_AGENT'] ?? '' ) ) { + Debug::write_log( 'Blocked activity from: ' . $activity->get_actor() ); + } else { + /** + * ActivityPub inbox action. + * + * @param array $data The data array. + * @param int|null $user_id The user ID. + * @param string $type The type of the activity. + * @param Activity|\WP_Error $activity The Activity object. + */ + \do_action( 'activitypub_inbox', $data, $user->get__id(), $type, $activity ); + + /** + * ActivityPub inbox action for specific activity types. + * + * @param array $data The data array. + * @param int|null $user_id The user ID. + * @param Activity|\WP_Error $activity The Activity object. + */ + \do_action( 'activitypub_inbox_' . $type, $data, $user->get__id(), $activity ); + } + + $response = \rest_ensure_response( array() ); + $response->set_status( 202 ); + $response->header( 'Content-Type', 'application/activity+json; charset=' . \get_option( 'blog_charset' ) ); + + return $response; + } + + /** + * Retrieves the schema for the inbox collection, conforming to JSON Schema. + * + * @return array Collection schema data. + */ + public function get_item_schema() { + if ( $this->schema ) { + return $this->add_additional_fields_schema( $this->schema ); + } + + $item_schema = array( + 'type' => 'object', + ); + + $schema = $this->get_collection_schema( $item_schema ); + + // Add inbox-specific properties. + $schema['title'] = 'inbox'; + $schema['properties']['generator'] = array( + 'description' => 'The software used to generate the collection.', + 'type' => 'string', + 'format' => 'uri', + ); + + $this->schema = $schema; + + return $this->add_additional_fields_schema( $this->schema ); + } +} diff --git a/wp-content/plugins/activitypub/includes/rest/class-actors.php b/wp-content/plugins/activitypub/includes/rest/class-actors.php deleted file mode 100644 index 60f03d29..00000000 --- a/wp-content/plugins/activitypub/includes/rest/class-actors.php +++ /dev/null @@ -1,161 +0,0 @@ -[\w\-\.]+)', - array( - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( self::class, 'get' ), - 'args' => self::request_parameters(), - 'permission_callback' => '__return_true', - ), - ) - ); - - \register_rest_route( - ACTIVITYPUB_REST_NAMESPACE, - '/(users|actors)/(?P' . $block['attrs']['url'] . '
'; + } + + /** + * Check if the post is a preview. + * + * @return boolean True if the post is a preview, false otherwise. + */ + private function is_preview() { + return defined( 'ACTIVITYPUB_PREVIEW' ) && ACTIVITYPUB_PREVIEW; } /** @@ -232,8 +710,8 @@ class Post extends Base { * * @return array The media array extended with enclosures. */ - public function get_enclosures( $media ) { - $enclosures = get_enclosures( $this->wp_object->ID ); + protected function get_enclosures( $media ) { + $enclosures = get_enclosures( $this->item->ID ); if ( ! $enclosures ) { return $media; @@ -242,14 +720,16 @@ class Post extends Base { foreach ( $enclosures as $enclosure ) { // Check if URL is an attachment. $attachment_id = \attachment_url_to_postid( $enclosure['url'] ); + if ( $attachment_id ) { $enclosure['id'] = $attachment_id; $enclosure['url'] = \wp_get_attachment_url( $attachment_id ); $enclosure['mediaType'] = \get_post_mime_type( $attachment_id ); } - $mime_type = $enclosure['mediaType']; - $mime_type_parts = \explode( '/', $mime_type ); + $mime_type = $enclosure['mediaType']; + $mime_type_parts = \explode( '/', $mime_type ); + $enclosure['type'] = \ucfirst( $mime_type_parts[0] ); switch ( $mime_type_parts[0] ) { case 'image': @@ -281,9 +761,9 @@ class Post extends Base { return array(); } - $blocks = \parse_blocks( $this->wp_object->post_content ); + $blocks = \parse_blocks( $this->item->post_content ); - return self::get_media_from_blocks( $blocks, $media ); + return $this->get_media_from_blocks( $blocks, $media ); } /** @@ -294,11 +774,11 @@ class Post extends Base { * * @return array The image IDs. */ - protected static function get_media_from_blocks( $blocks, $media ) { + protected function get_media_from_blocks( $blocks, $media ) { foreach ( $blocks as $block ) { // Recurse into inner blocks. if ( ! empty( $block['innerBlocks'] ) ) { - $media = self::get_media_from_blocks( $block['innerBlocks'], $media ); + $media = $this->get_media_from_blocks( $block['innerBlocks'], $media ); } switch ( $block['blockName'] ) { @@ -312,10 +792,21 @@ class Post extends Base { $alt = $match[2]; } - $media['image'][] = array( - 'id' => $block['attrs']['id'], - 'alt' => $alt, - ); + $found = false; + foreach ( $media['image'] as $i => $image ) { + if ( isset( $image['id'] ) && $image['id'] === $block['attrs']['id'] ) { + $media['image'][ $i ]['alt'] = $alt; + $found = true; + break; + } + } + + if ( ! $found ) { + $media['image'][] = array( + 'id' => $block['attrs']['id'], + 'alt' => $alt, + ); + } } break; case 'core/audio': @@ -358,58 +849,41 @@ class Post extends Base { } /** - * Get post images from the classic editor. - * Note that audio/video attachments are only supported in the block editor. + * Get image embeds from the classic editor by parsing HTML. * * @param array $media The media array grouped by type. * @param int $max_images The maximum number of images to return. * * @return array The attachments. */ - protected function get_classic_editor_images( $media, $max_images ) { - // Max images can't be negative or zero. - if ( $max_images <= 0 ) { - return array(); - } - - if ( \count( $media['image'] ) <= $max_images ) { - if ( \class_exists( '\WP_HTML_Tag_Processor' ) ) { - $media['image'] = \array_merge( $media['image'], $this->get_classic_editor_image_embeds( $max_images ) ); - } else { - $media['image'] = \array_merge( $media['image'], $this->get_classic_editor_image_attachments( $max_images ) ); - } - } - - return $media; - } - - /** - * Get image embeds from the classic editor by parsing HTML. - * - * @param int $max_images The maximum number of images to return. - * - * @return array The attachments. - */ - protected function get_classic_editor_image_embeds( $max_images ) { + protected function get_classic_editor_image_embeds( $media, $max_images ) { // If someone calls that function directly, bail. if ( ! \class_exists( '\WP_HTML_Tag_Processor' ) ) { - return array(); + return $media; } // Max images can't be negative or zero. if ( $max_images <= 0 ) { - return array(); + return $media; } $images = array(); - $base = \wp_get_upload_dir()['baseurl']; - $content = \get_post_field( 'post_content', $this->wp_object ); + $base = get_upload_baseurl(); + $content = \get_post_field( 'post_content', $this->item ); $tags = new \WP_HTML_Tag_Processor( $content ); // This linter warning is a false positive - we have to re-count each time here as we modify $images. // phpcs:ignore Squiz.PHP.DisallowSizeFunctionsInLoops.Found while ( $tags->next_tag( 'img' ) && ( \count( $images ) <= $max_images ) ) { - $src = $tags->get_attribute( 'src' ); + /** + * Filter the image source URL. + * + * This can be used to modify the image source URL before it is used to + * determine the attachment ID. + * + * @param string $src The image source URL. + */ + $src = \apply_filters( 'activitypub_image_src', $tags->get_attribute( 'src' ) ); /* * If the img source is in our uploads dir, get the @@ -424,16 +898,22 @@ class Post extends Base { if ( null !== $src && \str_starts_with( $src, $base ) ) { $img_id = \attachment_url_to_postid( $src ); + if ( 0 === $img_id ) { + $count = 0; + $src = \strtok( $src, '?' ); + $img_id = \attachment_url_to_postid( $src ); + } + if ( 0 === $img_id ) { $count = 0; - $src = preg_replace( '/-(?:\d+x\d+)(\.[a-zA-Z]+)$/', '$1', $src, 1, $count ); + $src = \preg_replace( '/-(?:\d+x\d+)(\.[a-zA-Z]+)$/', '$1', $src, 1, $count ); if ( $count > 0 ) { $img_id = \attachment_url_to_postid( $src ); } } if ( 0 === $img_id ) { - $src = preg_replace( '/(\.[a-zA-Z]+)$/', '-scaled$1', $src ); + $src = \preg_replace( '/(\.[a-zA-Z]+)$/', '-scaled$1', $src ); $img_id = \attachment_url_to_postid( $src ); } @@ -446,65 +926,32 @@ class Post extends Base { } } - return $images; - } - - /** - * Get image attachments from the classic editor. - * This is imperfect as the contained images aren't necessarily the - * same as the attachments. - * - * @param int $max_images The maximum number of images to return. - * - * @return array The attachment IDs. - */ - protected function get_classic_editor_image_attachments( $max_images ) { - // Max images can't be negative or zero. - if ( $max_images <= 0 ) { - return array(); + if ( \count( $media['image'] ) <= $max_images ) { + $media['image'] = \array_merge( $media['image'], $images ); } - $images = array(); - $query = new \WP_Query( - array( - 'post_parent' => $this->wp_object->ID, - 'post_status' => 'inherit', - 'post_type' => 'attachment', - 'post_mime_type' => 'image', - 'order' => 'ASC', - 'orderby' => 'menu_order ID', - 'posts_per_page' => $max_images, - ) - ); - - foreach ( $query->get_posts() as $attachment ) { - if ( ! \in_array( $attachment->ID, $images, true ) ) { - $images[] = array( 'id' => $attachment->ID ); - } - } - - return $images; + return $media; } /** * Filter media IDs by object type. * - * @param array $media The media array grouped by type. - * @param string $type The object type. - * @param WP_Post $wp_object The post object. + * @param array $media The media array grouped by type. + * @param string $type The object type. + * @param WP_Post $item The post object. * * @return array The filtered media IDs. */ - protected static function filter_media_by_object_type( $media, $type, $wp_object ) { + protected function filter_media_by_object_type( $media, $type, $item ) { /** * Filter the object type for media attachments. * * @param string $type The object type. - * @param WP_Post $wp_object The post object. + * @param WP_Post $item The post object. * * @return string The filtered object type. */ - $type = \apply_filters( 'filter_media_by_object_type', \strtolower( $type ), $wp_object ); + $type = \apply_filters( 'filter_media_by_object_type', \strtolower( $type ), $item ); if ( ! empty( $media[ $type ] ) ) { return $media[ $type ]; @@ -520,7 +967,7 @@ class Post extends Base { * * @return array The ActivityPub Attachment. */ - public static function wp_attachment_to_activity_attachment( $media ) { + public function wp_attachment_to_activity_attachment( $media ) { if ( ! isset( $media['id'] ) ) { return $media; } @@ -543,7 +990,7 @@ class Post extends Base { */ $thumbnail = apply_filters( 'activitypub_get_image', - self::get_wordpress_attachment( $id, $image_size ), + $this->get_wordpress_attachment( $id, $image_size ), $id, $image_size ); @@ -582,7 +1029,11 @@ class Post extends Base { $attachment['width'] = \esc_attr( $meta['width'] ); $attachment['height'] = \esc_attr( $meta['height'] ); } - // @todo: add `icon` support for audio/video attachments. Maybe use post thumbnail? + + if ( $this->get_icon() ) { + $attachment['icon'] = object_to_uri( $this->get_icon() ); + } + break; } @@ -605,7 +1056,7 @@ class Post extends Base { * * @return array|false Array of image data, or boolean false if no image is available. */ - protected static function get_wordpress_attachment( $id, $image_size = 'large' ) { + protected function get_wordpress_attachment( $id, $image_size = 'large' ) { /** * Hook into the image retrieval process. Before image retrieval. * @@ -628,225 +1079,14 @@ class Post extends Base { } /** - * Returns the ActivityStreams 2.0 Object-Type for a Post based on the - * settings and the Post-Type. + * Get the context of the post. * - * @see https://www.w3.org/TR/activitystreams-vocabulary/#activity-types + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-context * - * @return string The Object-Type. + * @return string The context of the post. */ - protected function get_type() { - $post_format_setting = \get_option( 'activitypub_object_type', ACTIVITYPUB_DEFAULT_OBJECT_TYPE ); - - if ( 'wordpress-post-format' !== $post_format_setting ) { - return \ucfirst( $post_format_setting ); - } - - $has_title = post_type_supports( $this->wp_object->post_type, 'title' ); - - if ( ! $has_title ) { - return 'Note'; - } - - // Default to Article. - $object_type = 'Article'; - $post_format = 'standard'; - - if ( \get_theme_support( 'post-formats' ) ) { - $post_format = \get_post_format( $this->wp_object ); - } - - $post_type = \get_post_type( $this->wp_object ); - switch ( $post_type ) { - case 'post': - switch ( $post_format ) { - case 'standard': - case '': - $object_type = 'Article'; - break; - default: - $object_type = 'Note'; - break; - } - break; - case 'page': - $object_type = 'Page'; - break; - default: - $object_type = 'Article'; - break; - } - - return $object_type; - } - - /** - * Returns a list of Mentions, used in the Post. - * - * @see https://docs.joinmastodon.org/spec/activitypub/#Mention - * - * @return array The list of Mentions. - */ - protected function get_cc() { - $cc = array(); - - $mentions = $this->get_mentions(); - if ( $mentions ) { - foreach ( $mentions as $url ) { - $cc[] = $url; - } - } - - return $cc; - } - - /** - * Returns the Audience for the Post. - * - * @return string|null The audience. - */ - public function get_audience() { - if ( is_single_user() ) { - return null; - } else { - $blog = new Blog(); - return $blog->get_id(); - } - } - - /** - * Returns a list of Tags, used in the Post. - * - * This includes Hash-Tags and Mentions. - * - * @return array The list of Tags. - */ - protected function get_tag() { - $tags = array(); - - $post_tags = \get_the_tags( $this->wp_object->ID ); - if ( $post_tags ) { - foreach ( $post_tags as $post_tag ) { - $tag = array( - 'type' => 'Hashtag', - 'href' => \esc_url( \get_tag_link( $post_tag->term_id ) ), - 'name' => esc_hashtag( $post_tag->name ), - ); - $tags[] = $tag; - } - } - - $mentions = $this->get_mentions(); - if ( $mentions ) { - foreach ( $mentions as $mention => $url ) { - $tag = array( - 'type' => 'Mention', - 'href' => \esc_url( $url ), - 'name' => \esc_html( $mention ), - ); - $tags[] = $tag; - } - } - - return $tags; - } - - /** - * Returns the summary for the ActivityPub Item. - * - * The summary will be generated based on the user settings and only if the - * object type is not set to `note`. - * - * @return string|null The summary or null if the object type is `note`. - */ - protected function get_summary() { - if ( 'Note' === $this->get_type() ) { - return null; - } - - // Remove Teaser from drafts. - if ( 'draft' === \get_post_status( $this->wp_object ) ) { - return \__( '(This post is being modified)', 'activitypub' ); - } - - return generate_post_summary( $this->wp_object ); - } - - /** - * Returns the title for the ActivityPub Item. - * - * The title will be generated based on the user settings and only if the - * object type is not set to `note`. - * - * @return string|null The title or null if the object type is `note`. - */ - protected function get_name() { - if ( 'Note' === $this->get_type() ) { - return null; - } - - $title = \get_the_title( $this->wp_object->ID ); - - if ( $title ) { - return \wp_strip_all_tags( - \html_entity_decode( - $title - ) - ); - } - - return null; - } - - /** - * Returns the content for the ActivityPub Item. - * - * The content will be generated based on the user settings. - * - * @return string The content. - */ - protected function get_content() { - add_filter( 'activitypub_reply_block', '__return_empty_string' ); - - // Remove Content from drafts. - if ( 'draft' === \get_post_status( $this->wp_object ) ) { - return \__( '(This post is being modified)', 'activitypub' ); - } - - global $post; - - /** - * Provides an action hook so plugins can add their own hooks/filters before AP content is generated. - * - * Example: if a plugin adds a filter to `the_content` to add a button to the end of posts, it can also remove that filter here. - * - * @param WP_Post $post The post object. - */ - do_action( 'activitypub_before_get_content', $post ); - - add_filter( 'render_block_core/embed', array( self::class, 'revert_embed_links' ), 10, 2 ); - - // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited - $post = $this->wp_object; - $content = $this->get_post_content_template(); - - // Register our shortcodes just in time. - Shortcodes::register(); - // Fill in the shortcodes. - setup_postdata( $post ); - $content = do_shortcode( $content ); - wp_reset_postdata(); - - $content = \wpautop( $content ); - $content = \preg_replace( '/[\n\r\t]/', '', $content ); - $content = \trim( $content ); - - $content = \apply_filters( 'activitypub_the_content', $content, $post ); - - // Don't need these anymore, should never appear in a post. - Shortcodes::unregister(); - - return $content; + protected function get_context() { + return get_rest_url_by_path( sprintf( 'posts/%d/context', $this->item->ID ) ); } /** @@ -855,193 +1095,83 @@ class Post extends Base { * @return string The Template. */ protected function get_post_content_template() { - $type = \get_option( 'activitypub_post_content_type', 'content' ); - - switch ( $type ) { - case 'excerpt': - $template = "[ap_excerpt]\n\n[ap_permalink type=\"html\"]"; - break; - case 'title': - $template = "' . $block['attrs']['url'] . '
'; - } } diff --git a/wp-content/plugins/activitypub/includes/transformer/class-user.php b/wp-content/plugins/activitypub/includes/transformer/class-user.php new file mode 100644 index 00000000..418913be --- /dev/null +++ b/wp-content/plugins/activitypub/includes/transformer/class-user.php @@ -0,0 +1,41 @@ +transform_object_properties( Actors::get_by_id( $this->item->ID ) ); + + if ( \is_wp_error( $activity_object ) ) { + return $activity_object; + } + + return $activity_object; + } + + /** + * Get the Actor ID. + * + * @return string The Actor ID. + */ + public function to_id() { + return Actors::get_by_id( $this->item->ID )->get_id(); + } +} diff --git a/wp-content/plugins/activitypub/integration/class-akismet.php b/wp-content/plugins/activitypub/integration/class-akismet.php new file mode 100644 index 00000000..511cef39 --- /dev/null +++ b/wp-content/plugins/activitypub/integration/class-akismet.php @@ -0,0 +1,40 @@ + $field ) { @@ -190,7 +193,7 @@ class Enable_Mastodon_Apps { if ( $acct && ! is_wp_error( $acct ) ) { $acct = \str_replace( 'acct:', '', $acct ); } else { - $acct = $item->get_url(); + $acct = $item->get_id(); } $account = new Account(); @@ -239,7 +242,7 @@ class Enable_Mastodon_Apps { return $user_data; } - $user = Users::get_by_various( $user_id ); + $user = Actors::get_by_various( $user_id ); if ( $user && ! is_wp_error( $user ) ) { return $user_data; @@ -269,7 +272,7 @@ class Enable_Mastodon_Apps { */ public static function api_account_internal( $user_data, $user_id ) { $user_id_to_use = self::maybe_map_user_to_blog( $user_id ); - $user = Users::get_by_id( $user_id_to_use ); + $user = Actors::get_by_id( $user_id_to_use ); if ( ! $user || is_wp_error( $user ) ) { return $user_data; @@ -324,6 +327,44 @@ class Enable_Mastodon_Apps { return $account; } + /** + * Use our representation of posts to power each status item. + * Includes proper referncing of 3rd party comments that arrived via federation. + * + * @param null|Status $status The status, typically null to allow later filters their shot. + * @param int $post_id The post ID. + * @return Status|null The status. + */ + public static function api_status( $status, $post_id ) { + $post = \get_post( $post_id ); + if ( ! $post ) { + return $status; + } + + return self::api_post_status( $post_id ); + } + + /** + * Transforms a WordPress post into a Mastodon-compatible status object. + * + * Takes a post ID, transforms it into an ActivityPub object, and converts + * it to a Mastodon API status format including the author's account info. + * + * @param int $post_id The WordPress post ID to transform. + * @return Status|null The Mastodon API status object, or null if the post is not found + */ + private static function api_post_status( $post_id ) { + $post = Factory::get_transformer( get_post( $post_id ) ); + if ( is_wp_error( $post ) ) { + return null; + } + + $data = $post->to_object()->to_array(); + $account = self::api_account_internal( null, get_post_field( 'post_author', $post_id ) ); + + return self::activity_to_status( $data, $account, $post_id ); + } + /** * Get account for actor. * @@ -332,7 +373,7 @@ class Enable_Mastodon_Apps { * @return Account|null The account. */ private static function get_account_for_actor( $uri ) { - if ( ! is_string( $uri ) ) { + if ( ! is_string( $uri ) || empty( $uri ) ) { return null; } $data = get_remote_metadata_by_actor( $uri ); @@ -343,6 +384,10 @@ class Enable_Mastodon_Apps { $account = new Account(); $acct = Webfinger_Util::uri_to_acct( $uri ); + if ( ! $acct || is_wp_error( $acct ) ) { + return null; + } + if ( str_starts_with( $acct, 'acct:' ) ) { $acct = substr( $acct, 5 ); } @@ -489,22 +534,23 @@ class Enable_Mastodon_Apps { * * @param array $item The activity. * @param Account $account The account. + * @param int $post_id The post ID. Optional, but will be preferred in the Status. * * @return Status|null The status. */ - private static function activity_to_status( $item, $account ) { + private static function activity_to_status( $item, $account, $post_id = null ) { if ( isset( $item['object'] ) ) { $object = $item['object']; } else { $object = $item; } - if ( ! isset( $object['type'] ) || 'Note' !== $object['type'] ) { + if ( ! isset( $object['type'] ) || 'Note' !== $object['type'] || ! $account ) { return null; } $status = new Status(); - $status->id = $object['id']; + $status->id = $post_id ?? $object['id']; $status->created_at = new DateTime( $object['published'] ); $status->content = $object['content']; $status->account = $account; @@ -624,7 +670,7 @@ class Enable_Mastodon_Apps { $posts['orderedItems'] ); $activitypub_statuses = array_merge( $activitypub_statuses, array_filter( $new_statuses ) ); - $url = $posts['next']; + $url = $posts['next'] ?? null; if ( count( $activitypub_statuses ) >= $limit ) { break; @@ -649,20 +695,37 @@ class Enable_Mastodon_Apps { return $context; } - $replies_url = $meta['replies']['first']['next']; - $replies = Http::get_remote_object( $replies_url, true ); - if ( is_wp_error( $replies ) || ! isset( $replies['items'] ) ) { + if ( ! empty( $meta['replies']['first']['items'] ) ) { + $replies = $meta['replies']['first']; + } elseif ( isset( $meta['replies']['first']['next'] ) ) { + $replies_url = $meta['replies']['first']['next']; + $replies = Http::get_remote_object( $replies_url, true ); + if ( is_wp_error( $replies ) || ! isset( $replies['items'] ) ) { + return $context; + } + } else { return $context; } - foreach ( $replies['items'] as $url ) { - $response = Http::get( $url, true ); - if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) !== 200 ) { - continue; - } - $status = json_decode( wp_remote_retrieve_body( $response ), true ); - if ( ! $status || is_wp_error( $status ) ) { - continue; + foreach ( $replies['items'] as $reply ) { + if ( isset( $reply['id'] ) && is_string( $reply['id'] ) && isset( $reply['content'] ) && is_string( $reply['content'] ) ) { + $status = $reply; + } else { + if ( is_string( $reply ) ) { + $url = $reply; + } elseif ( isset( $reply['url'] ) && is_string( $reply['url'] ) ) { + $url = $reply['url']; + } else { + continue; + } + $response = Http::get( $url, true ); + if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) !== 200 ) { + continue; + } + $status = json_decode( wp_remote_retrieve_body( $response ), true ); + if ( ! $status || is_wp_error( $status ) ) { + continue; + } } $account = self::get_account_for_actor( $status['attributedTo'] ); diff --git a/wp-content/plugins/activitypub/integration/class-jetpack.php b/wp-content/plugins/activitypub/integration/class-jetpack.php index 002de2cb..5586cf79 100644 --- a/wp-content/plugins/activitypub/integration/class-jetpack.php +++ b/wp-content/plugins/activitypub/integration/class-jetpack.php @@ -7,6 +7,8 @@ namespace Activitypub\Integration; +use Activitypub\Comment; + /** * Jetpack integration class. */ @@ -17,6 +19,8 @@ class Jetpack { */ public static function init() { \add_filter( 'jetpack_sync_post_meta_whitelist', array( self::class, 'add_sync_meta' ) ); + \add_filter( 'jetpack_json_api_comment_types', array( self::class, 'add_comment_types' ) ); + \add_filter( 'jetpack_api_include_comment_types_count', array( self::class, 'add_comment_types' ) ); } /** @@ -31,10 +35,20 @@ class Jetpack { return $allow_list; } $activitypub_meta_keys = array( - 'activitypub_user_id', - 'activitypub_inbox', - 'activitypub_actor_json', + '_activitypub_user_id', + '_activitypub_inbox', + '_activitypub_actor_json', ); return \array_merge( $allow_list, $activitypub_meta_keys ); } + + /** + * Add custom comment types to the list of comment types. + * + * @param array $comment_types Default comment types. + * @return array + */ + public static function add_comment_types( $comment_types ) { + return array_unique( \array_merge( $comment_types, Comment::get_comment_type_slugs() ) ); + } } diff --git a/wp-content/plugins/activitypub/integration/class-multisite-language-switcher.php b/wp-content/plugins/activitypub/integration/class-multisite-language-switcher.php new file mode 100644 index 00000000..53e18f96 --- /dev/null +++ b/wp-content/plugins/activitypub/integration/class-multisite-language-switcher.php @@ -0,0 +1,49 @@ +post_type ) { + \add_action( 'msls_main_save', '__return_null' ); + } + } + + /** + * Remove short-circuit for Multisite Language Switcher data. + * + * @param int $post_id The post id. + * @param WP_Post $post The post object. + */ + public static function unignore_outbox_post( $post_id, $post ) { + if ( Outbox::POST_TYPE === $post->post_type ) { + \remove_action( 'msls_main_save', '__return_null' ); + } + } +} diff --git a/wp-content/plugins/activitypub/integration/class-nodeinfo.php b/wp-content/plugins/activitypub/integration/class-nodeinfo.php index c5972fb3..dfbedcbf 100644 --- a/wp-content/plugins/activitypub/integration/class-nodeinfo.php +++ b/wp-content/plugins/activitypub/integration/class-nodeinfo.php @@ -24,7 +24,7 @@ class Nodeinfo { \add_filter( 'nodeinfo_data', array( self::class, 'add_nodeinfo_data' ), 10, 2 ); \add_filter( 'nodeinfo2_data', array( self::class, 'add_nodeinfo2_data' ) ); - \add_filter( 'wellknown_nodeinfo_data', array( self::class, 'add_wellknown_nodeinfo_data' ), 10, 2 ); + \add_filter( 'wellknown_nodeinfo_data', array( self::class, 'add_wellknown_nodeinfo_data' ) ); } /** @@ -45,8 +45,8 @@ class Nodeinfo { $nodeinfo['usage']['users'] = array( 'total' => get_total_users(), - 'activeMonth' => get_active_users( '1 month ago' ), - 'activeHalfyear' => get_active_users( '6 month ago' ), + 'activeMonth' => get_active_users(), + 'activeHalfyear' => get_active_users( 6 ), ); return $nodeinfo; @@ -64,8 +64,8 @@ class Nodeinfo { $nodeinfo['usage']['users'] = array( 'total' => get_total_users(), - 'activeMonth' => get_active_users( '1 month ago' ), - 'activeHalfyear' => get_active_users( '6 month ago' ), + 'activeMonth' => get_active_users(), + 'activeHalfyear' => get_active_users( 6 ), ); return $nodeinfo; diff --git a/wp-content/plugins/activitypub/integration/class-opengraph.php b/wp-content/plugins/activitypub/integration/class-opengraph.php index 4eb1b1e0..c22e2958 100644 --- a/wp-content/plugins/activitypub/integration/class-opengraph.php +++ b/wp-content/plugins/activitypub/integration/class-opengraph.php @@ -8,7 +8,7 @@ namespace Activitypub\Integration; use Activitypub\Model\Blog; -use Activitypub\Collection\Users; +use Activitypub\Collection\Actors; use function Activitypub\is_single_user; use function Activitypub\is_user_type_disabled; @@ -72,13 +72,13 @@ class Opengraph { $user_id = \get_post_field( 'post_author', \get_queried_object_id() ); } elseif ( ! is_user_type_disabled( 'blog' ) ) { // Use the Blog-User for any other page, if the Blog-User is not disabled. - $user_id = Users::BLOG_USER_ID; + $user_id = Actors::BLOG_USER_ID; } else { // Do not add any metadata otherwise. return $metadata; } - $user = Users::get_by_id( $user_id ); + $user = Actors::get_by_id( $user_id ); if ( ! $user || \is_wp_error( $user ) ) { return $metadata; diff --git a/wp-content/plugins/activitypub/integration/class-seriously-simple-podcasting.php b/wp-content/plugins/activitypub/integration/class-seriously-simple-podcasting.php index c678f6d3..7533db98 100644 --- a/wp-content/plugins/activitypub/integration/class-seriously-simple-podcasting.php +++ b/wp-content/plugins/activitypub/integration/class-seriously-simple-podcasting.php @@ -9,6 +9,7 @@ namespace Activitypub\Integration; use Activitypub\Transformer\Post; +use function Activitypub\object_to_uri; use function Activitypub\generate_post_summary; /** @@ -28,20 +29,23 @@ class Seriously_Simple_Podcasting extends Post { * @return array The attachments array. */ public function get_attachment() { - $post = $this->wp_object; - $attachments = parent::get_attachment(); - + $post = $this->item; $attachment = array( - 'type' => \esc_attr( \get_post_meta( $post->ID, 'episode_type', true ) ), + 'type' => \esc_attr( ucfirst( \get_post_meta( $post->ID, 'episode_type', true ) ?? 'Audio' ) ), 'url' => \esc_url( \get_post_meta( $post->ID, 'audio_file', true ) ), - 'name' => \esc_attr( \get_the_title( $post->ID ) ), - 'icon' => \esc_url( \get_post_meta( $post->ID, 'cover_image', true ) ), + 'name' => \esc_attr( \get_the_title( $post->ID ) ?? '' ), ); - $attachment = array_filter( $attachment ); - array_unshift( $attachments, $attachment ); + $icon = \get_post_meta( $post->ID, 'cover_image', true ); + if ( ! $icon ) { + $icon = $this->get_icon(); + } - return $attachments; + if ( $icon ) { + $attachment['icon'] = \esc_url( object_to_uri( $icon ) ); + } + + return array( $attachment ); } /** @@ -63,6 +67,6 @@ class Seriously_Simple_Podcasting extends Post { * @return string The content. */ public function get_content() { - return generate_post_summary( $this->wp_object ); + return generate_post_summary( $this->item ); } } diff --git a/wp-content/plugins/activitypub/integration/class-stream-connector.php b/wp-content/plugins/activitypub/integration/class-stream-connector.php index 18ffb5da..e59c6009 100644 --- a/wp-content/plugins/activitypub/integration/class-stream-connector.php +++ b/wp-content/plugins/activitypub/integration/class-stream-connector.php @@ -7,6 +7,10 @@ namespace Activitypub\Integration; +use Activitypub\Collection\Actors; +use function Activitypub\url_to_authorid; +use function Activitypub\url_to_commentid; + /** * Stream Connector for ActivityPub. * @@ -29,6 +33,9 @@ class Stream_Connector extends \WP_Stream\Connector { */ public $actions = array( 'activitypub_notification_follow', + 'activitypub_sent_to_inbox', + 'activitypub_outbox_processing_complete', + 'activitypub_outbox_processing_batch_complete', ); /** @@ -55,7 +62,49 @@ class Stream_Connector extends \WP_Stream\Connector { * @return array */ public function get_action_labels() { - return array(); + return array( + 'processed' => __( 'Processed', 'activitypub' ), + ); + } + + /** + * Add action links to Stream drop row in admin list screen + * + * @filter wp_stream_action_links_{connector} + * + * @param array $links Previous links registered. + * @param Record $record Stream record. + * + * @return array Action links + */ + public function action_links( $links, $record ) { + if ( 'processed' === $record->action ) { + $error = json_decode( $record->get_meta( 'error', true ), true ); + + if ( $error ) { + $message = sprintf( + '%2$s
%2$s
+ ' . esc_html( $args['actor']['webfinger'] ) . '
+ ' . esc_html( $args['webfinger'] ) . '' ); + ?> +
+ ++ + + +
+ ++ followers list to see all followers.', 'activitypub' ), array( 'a' => array( 'href' => array() ) ) ), + esc_url( admin_url( $args['admin_url'] ) ) + ); + ?> +
+ + + ++ ' . esc_html( $args['actor']['webfinger'] ) . '' ); + ?> +
+ + + ++ + + +
+ + + + diff --git a/wp-content/plugins/activitypub/templates/emails/parts/header.php b/wp-content/plugins/activitypub/templates/emails/parts/header.php new file mode 100644 index 00000000..6ea7e471 --- /dev/null +++ b/wp-content/plugins/activitypub/templates/emails/parts/header.php @@ -0,0 +1,60 @@ + + + +- - | -
-
- get_webfinger() ) ); ?> - |
- |||
---|---|---|---|---|
- - | -- - - | -|||
- - | -
-
-
-
-
-
-
- |
- |||
- - | -
- - - - -
|
-