=1e4&&(clearInterval(a),r("Telemetry library not loaded"))}),100);else r("Telemetry not enabled")}))},e.prototype.trackEvent=function(t,r){var n;this.isLoaded?(0!==t.indexOf(e.TRACKS_PREFIX)&&(t=e.TRACKS_PREFIX+t),this.isEventNameValid(t)?(r=this.prepareProperties(r),null===(n=this._tkq)||void 0===n||n.push(["recordEvent",t,r])):console.error("Error tracking event: Invalid event name")):console.error("Error tracking event: Telemetry not loaded")},e.prototype.isTelemetryEnabled=function(){return this.isEnabled},e.prototype.isProprietyValid=function(t){return e.PROPERTY_REGEX.test(t)},e.prototype.isEventNameValid=function(t){return e.EVENT_NAME_REGEX.test(t)},e.prototype.prepareProperties=function(e){return(e=this.sanitizeProperties(e)).parsely_version=wpParselyTracksTelemetry.version,wpParselyTracksTelemetry.user&&(e._ut=wpParselyTracksTelemetry.user.type,e._ui=wpParselyTracksTelemetry.user.id),wpParselyTracksTelemetry.vipgo_env&&(e.vipgo_env=wpParselyTracksTelemetry.vipgo_env),this.sanitizeProperties(e)},e.prototype.sanitizeProperties=function(e){var t=this,r={};return Object.keys(e).forEach((function(n){t.isProprietyValid(n)&&(r[n]=e[n])})),r},e.TRACKS_PREFIX="wpparsely_",e.EVENT_NAME_REGEX=/^(([a-z0-9]+)_){2}([a-z0-9_]+)$/,e.PROPERTY_REGEX=/^[a-z_][a-z0-9_]*$/,e}(),k=(S.trackEvent,function(e){void 0===e&&(e=null);var t="";(null==e?void 0:e.children)&&(t=e.children);var r="content-helper-error-message";return(null==e?void 0:e.className)&&(r+=" "+e.className),(0,w.jsx)("div",{className:r,"data-testid":null==e?void 0:e.testId,dangerouslySetInnerHTML:{__html:t}})}),T=(_=function(e,t){return _=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(e[r]=t[r])},_(e,t)},function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");function __(){this.constructor=e}_(e,t),e.prototype=null===t?Object.create(t):(__.prototype=t.prototype,new __)});!function(e){e.AccessToFeatureDisabled="ch_access_to_feature_disabled",e.CannotFormulateApiQuery="ch_cannot_formulate_api_query",e.FetchError="fetch_error",e.HttpRequestFailed="http_request_failed",e.ParselyAborted="ch_parsely_aborted",e[e.ParselyApiForbidden=403]="ParselyApiForbidden",e.ParselyApiResponseContainsError="ch_response_contains_error",e.ParselyApiReturnedNoData="ch_parsely_api_returned_no_data",e.ParselyApiReturnedTooManyResults="ch_parsely_api_returned_too_many_results",e.PluginCredentialsNotSetMessageDetected="parsely_credentials_not_set_message_detected",e.PluginSettingsApiSecretNotSet="parsely_api_secret_not_set",e.PluginSettingsSiteIdNotSet="parsely_site_id_not_set",e.PostIsNotPublished="ch_post_not_published",e.UnknownError="ch_unknown_error",e.ParselySuggestionsApiAuthUnavailable="AUTH_UNAVAILABLE",e.ParselySuggestionsApiNoAuthentication="NO_AUTHENTICATION",e.ParselySuggestionsApiNoAuthorization="NO_AUTHORIZATION",e.ParselySuggestionsApiNoData="NO_DATA",e.ParselySuggestionsApiOpenAiError="OPENAI_ERROR",e.ParselySuggestionsApiOpenAiSchema="OPENAI_SCHEMA",e.ParselySuggestionsApiOpenAiUnavailable="OPENAI_UNAVAILABLE",e.ParselySuggestionsApiSchemaError="SCHEMA_ERROR"}(b||(b={}));var N=function(e){function t(r,n,o){void 0===o&&(o=(0,m.__)("Error:","wp-parsely"));var a=this;r.startsWith(o)&&(o=""),(a=e.call(this,o.length>0?"".concat(o," ").concat(r):r)||this).hint=null,a.name=a.constructor.name,a.code=n;var i=[b.AccessToFeatureDisabled,b.ParselyApiForbidden,b.ParselyApiResponseContainsError,b.ParselyApiReturnedNoData,b.ParselyApiReturnedTooManyResults,b.PluginCredentialsNotSetMessageDetected,b.PluginSettingsApiSecretNotSet,b.PluginSettingsSiteIdNotSet,b.PostIsNotPublished,b.UnknownError,b.ParselySuggestionsApiAuthUnavailable,b.ParselySuggestionsApiNoAuthentication,b.ParselySuggestionsApiNoAuthorization,b.ParselySuggestionsApiNoData,b.ParselySuggestionsApiSchemaError];return a.retryFetch=!i.includes(a.code),Object.setPrototypeOf(a,t.prototype),a.code===b.AccessToFeatureDisabled?a.message=(0,m.__)("Access to this feature is disabled by the site's administration.","wp-parsely"):a.code===b.ParselySuggestionsApiNoAuthorization?a.message=(0,m.__)('This AI-powered feature is opt-in. To gain access, please submit a request here.',"wp-parsely"):a.code===b.ParselySuggestionsApiOpenAiError||a.code===b.ParselySuggestionsApiOpenAiUnavailable?a.message=(0,m.__)("The Parse.ly API returned an internal server error. Please retry with a different input, or try again later.","wp-parsely"):a.code===b.HttpRequestFailed&&a.message.includes("cURL error 28")?a.message=(0,m.__)("The Parse.ly API did not respond in a timely manner. Please try again later.","wp-parsely"):a.code===b.ParselySuggestionsApiSchemaError?a.message=(0,m.__)("The Parse.ly API returned a validation error. Please try again with different parameters.","wp-parsely"):a.code===b.ParselySuggestionsApiNoData?a.message=(0,m.__)("The Parse.ly API couldn't find any relevant data to fulfill the request. Please retry with a different input.","wp-parsely"):a.code===b.ParselySuggestionsApiOpenAiSchema?a.message=(0,m.__)("The Parse.ly API returned an incorrect response. Please try again later.","wp-parsely"):a.code===b.ParselySuggestionsApiAuthUnavailable&&(a.message=(0,m.__)("The Parse.ly API is currently unavailable. Please try again later.","wp-parsely")),a}return T(t,e),t.prototype.Message=function(e){return void 0===e&&(e=null),[b.PluginCredentialsNotSetMessageDetected,b.PluginSettingsSiteIdNotSet,b.PluginSettingsApiSecretNotSet].includes(this.code)?function(e){var t;return void 0===e&&(e=null),(0,w.jsx)(k,{className:null==e?void 0:e.className,testId:"empty-credentials-message",children:null!==(t=window.wpParselyEmptyCredentialsMessage)&&void 0!==t?t:(0,m.__)("Please ensure that the Site ID and API Secret given in the plugin's settings are correct.","wp-parsely")})}(e):(this.code===b.FetchError&&(this.hint=this.Hint((0,m.__)("This error can sometimes be caused by ad-blockers or browser tracking protections. Please add this site to any applicable allow lists and try again.","wp-parsely"))),this.code!==b.ParselyApiForbidden&&this.code!==b.ParselySuggestionsApiNoAuthentication||(this.hint=this.Hint((0,m.__)("Please ensure that the Site ID and API Secret given in the plugin's settings are correct.","wp-parsely"))),this.code===b.HttpRequestFailed&&(this.hint=this.Hint((0,m.__)("The Parse.ly API cannot be reached. Please verify that you are online.","wp-parsely"))),(0,w.jsx)(k,{className:null==e?void 0:e.className,testId:"error",children:"".concat(this.message,"
").concat(this.hint?this.hint:"")}))},t.prototype.Hint=function(e){return''.concat((0,m.__)("Hint:","wp-parsely")," ").concat(e,"
")},t.prototype.createErrorSnackbar=function(){//.test(this.message)||(0,d.dispatch)("core/notices").createNotice("error",this.message,{type:"snackbar"})},t}(Error),j=function(e){var t=e.size,r=void 0===t?24:t,n=e.className,o=void 0===n?"wp-parsely-icon":n;return(0,w.jsxs)(v.SVG,{className:o,height:r,viewBox:"0 0 60 65",width:r,xmlns:"http://www.w3.org/2000/svg",children:[(0,w.jsx)(v.Path,{fill:"#5ba745",d:"M23.72,51.53c0-.18,0-.34-.06-.52a13.11,13.11,0,0,0-2.1-5.53A14.74,14.74,0,0,0,19.12,43c-.27-.21-.5-.11-.51.22l-.24,3.42c0,.33-.38.35-.49,0l-1.5-4.8a1.4,1.4,0,0,0-.77-.78,23.91,23.91,0,0,0-3.1-.84c-1.38-.24-3.39-.39-3.39-.39-.34,0-.45.21-.25.49l2.06,3.76c.2.27,0,.54-.29.33l-4.51-3.6a3.68,3.68,0,0,0-2.86-.48c-1,.16-2.44.46-2.44.46a.68.68,0,0,0-.39.25.73.73,0,0,0-.14.45S.41,43,.54,44a3.63,3.63,0,0,0,1.25,2.62L6.48,50c.28.2.09.49-.23.37l-4.18-.94c-.32-.12-.5,0-.4.37,0,0,.69,1.89,1.31,3.16a24,24,0,0,0,1.66,2.74,1.34,1.34,0,0,0,1,.52l5,.13c.33,0,.41.38.1.48L7.51,58c-.31.1-.34.35-.07.55a14.29,14.29,0,0,0,3.05,1.66,13.09,13.09,0,0,0,5.9.5,25.13,25.13,0,0,0,4.34-1,9.55,9.55,0,0,1-.08-1.2,9.32,9.32,0,0,1,3.07-6.91"}),(0,w.jsx)(v.Path,{fill:"#5ba745",d:"M59.7,41.53a.73.73,0,0,0-.14-.45.68.68,0,0,0-.39-.25s-1.43-.3-2.44-.46a3.64,3.64,0,0,0-2.86.48l-4.51,3.6c-.26.21-.49-.06-.29-.33l2.06-3.76c.2-.28.09-.49-.25-.49,0,0-2,.15-3.39.39a23.91,23.91,0,0,0-3.1.84,1.4,1.4,0,0,0-.77.78l-1.5,4.8c-.11.32-.48.3-.49,0l-.24-3.42c0-.33-.24-.43-.51-.22a14.74,14.74,0,0,0-2.44,2.47A13.11,13.11,0,0,0,36.34,51c0,.18,0,.34-.06.52a9.26,9.26,0,0,1,3,8.1,24.1,24.1,0,0,0,4.34,1,13.09,13.09,0,0,0,5.9-.5,14.29,14.29,0,0,0,3.05-1.66c.27-.2.24-.45-.07-.55l-3.22-1.17c-.31-.1-.23-.47.1-.48l5-.13a1.38,1.38,0,0,0,1-.52A24.6,24.6,0,0,0,57,52.92c.61-1.27,1.31-3.16,1.31-3.16.1-.33-.08-.49-.4-.37l-4.18.94c-.32.12-.51-.17-.23-.37l4.69-3.34A3.63,3.63,0,0,0,59.46,44c.13-1,.24-2.47.24-2.47"}),(0,w.jsx)(v.Path,{fill:"#5ba745",d:"M46.5,25.61c0-.53-.35-.72-.8-.43l-4.86,2.66c-.45.28-.56-.27-.23-.69l4.66-6.23a2,2,0,0,0,.28-1.68,36.51,36.51,0,0,0-2.19-4.89,34,34,0,0,0-2.81-3.94c-.33-.41-.74-.35-.91.16l-2.28,5.68c-.16.5-.6.48-.59-.05l.28-8.93a2.54,2.54,0,0,0-.66-1.64S35,4.27,33.88,3.27,30.78.69,30.78.69a1.29,1.29,0,0,0-1.54,0s-1.88,1.49-3.12,2.59-2.48,2.35-2.48,2.35A2.5,2.5,0,0,0,23,7.27l.27,8.93c0,.53-.41.55-.58.05l-2.29-5.69c-.17-.5-.57-.56-.91-.14a35.77,35.77,0,0,0-3,4.2,35.55,35.55,0,0,0-2,4.62,2,2,0,0,0,.27,1.67l4.67,6.24c.33.42.23,1-.22.69l-4.87-2.66c-.45-.29-.82-.1-.82.43a18.6,18.6,0,0,0,.83,5.07,20.16,20.16,0,0,0,5.37,7.77c3.19,3,5.93,7.8,7.45,11.08A9.6,9.6,0,0,1,30,49.09a9.31,9.31,0,0,1,2.86.45c1.52-3.28,4.26-8.11,7.44-11.09a20.46,20.46,0,0,0,5.09-7,19,19,0,0,0,1.11-5.82"}),(0,w.jsx)(v.Path,{fill:"#5ba745",d:"M36.12,58.44A6.12,6.12,0,1,1,30,52.32a6.11,6.11,0,0,1,6.12,6.12"})]})},I=window.wp.url,O=window.wp.apiFetch,C=r.n(O),R=function(){function e(){this.abortControllers=new Map}return e.prototype.cancelRequest=function(e){if(e)(t=this.abortControllers.get(e))&&(t.abort(),this.abortControllers.delete(e));else{var t,r=Array.from(this.abortControllers.keys()).pop();r&&(t=this.abortControllers.get(r))&&(t.abort(),this.abortControllers.delete(r))}},e.prototype.cancelAll=function(){this.abortControllers.forEach((function(e){return e.abort()})),this.abortControllers.clear()},e.prototype.getOrCreateController=function(e){if(e&&this.abortControllers.has(e))return{abortController:this.abortControllers.get(e),abortId:e};var t=null!=e?e:"auto-"+Date.now(),r=new AbortController;return this.abortControllers.set(t,r),{abortController:r,abortId:t}},e.prototype.fetch=function(e,t){return r=this,n=void 0,a=function(){var r,n,o,a,i,s;return function(e,t){var r,n,o,a={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]},i=Object.create(("function"==typeof Iterator?Iterator:Object).prototype);return i.next=s(0),i.throw=s(1),i.return=s(2),"function"==typeof Symbol&&(i[Symbol.iterator]=function(){return this}),i;function s(s){return function(l){return function(s){if(r)throw new TypeError("Generator is already executing.");for(;i&&(i=0,s[0]&&(a=0)),a;)try{if(r=1,n&&(o=2&s[0]?n.return:s[0]?n.throw||((o=n.return)&&o.call(n),0):n.next)&&!(o=o.call(n,s[1])).done)return o;switch(n=0,o&&(s=[2&s[0],o.value]),s[0]){case 0:case 1:o=s;break;case 4:return a.label++,{value:s[1],done:!1};case 5:a.label++,n=s[1],s=[0];continue;case 7:s=a.ops.pop(),a.trys.pop();continue;default:if(!((o=(o=a.trys).length>0&&o[o.length-1])||6!==s[0]&&2!==s[0])){a=0;continue}if(3===s[0]&&(!o||s[1]>o[0]&&s[1]0&&o[o.length-1])||6!==s[0]&&2!==s[0])){a=0;continue}if(3===s[0]&&(!o||s[1]>o[0]&&s[1]0&&o[o.length-1])||6!==s[0]&&2!==s[0])){a=0;continue}if(3===s[0]&&(!o||s[1]>o[0]&&s[1]0?(0,m.sprintf)(
// Translators: %1$s the number of words in the excerpt.
// Translators: %1$s the number of words in the excerpt.
(0,m._n)("%1$s word","%1$s words",e,"wp-parsely"),e):"")}),[h.currentExcerpt,k]),(0,P.useEffect)((function(){var e=document.querySelector(".editor-post-excerpt textarea");e&&(e.scrollTop=0)}),[h.newExcerptGeneratedCount]),(0,w.jsxs)("div",{className:"editor-post-excerpt",children:[(0,w.jsxs)("div",{style:{position:"relative"},children:[t&&(0,w.jsx)("div",{className:"editor-post-excerpt__loading_animation",children:(0,w.jsx)(H,{})}),(0,w.jsx)(v.TextareaControl,{__nextHasNoMarginBottom:!0,label:(0,m.__)("Write an excerpt (optional)","wp-parsely"),className:"editor-post-excerpt__textarea",onChange:function(e){h.isUnderReview||_({excerpt:e}),f(F(F({},h),{currentExcerpt:e})),l(!0)},onKeyUp:function(){var e;if(s)l(!1);else{var t=document.querySelector(".editor-post-excerpt textarea"),r=null!==(e=null==t?void 0:t.textContent)&&void 0!==e?e:"";f(F(F({},h),{currentExcerpt:r}))}},value:t?"":h.isUnderReview?h.currentExcerpt:k,help:u||null})]}),(0,w.jsxs)(v.Button,{href:(0,m.__)("https://wordpress.org/documentation/article/page-post-settings-sidebar/#excerpt","wp-parsely"),target:"_blank",variant:"link",children:[(0,m.__)("Learn more about manual excerpts","wp-parsely"),(0,w.jsx)(v.Icon,{icon:A,size:18,className:"parsely-external-link-icon"})]}),(0,w.jsxs)("div",{className:"wp-parsely-excerpt-generator",children:[(0,w.jsxs)("div",{className:"wp-parsely-excerpt-generator-header",children:[(0,w.jsx)(j,{size:16}),(0,w.jsxs)("div",{className:"wp-parsely-excerpt-generator-header-label",children:[(0,m.__)("Generate With Parse.ly","wp-parsely"),(0,w.jsx)("span",{className:"beta-label",children:(0,m.__)("Beta","wp-parsely")})]})]}),o&&(0,w.jsx)(v.Notice,{className:"wp-parsely-excerpt-generator-error",onRemove:function(){return a(void 0)},status:"info",children:o.Message()}),(0,w.jsx)("div",{className:"wp-parsely-excerpt-generator-controls",children:h.isUnderReview?(0,w.jsxs)(w.Fragment,{children:[(0,w.jsx)(v.Button,{variant:"secondary",onClick:function(){return L(void 0,void 0,void 0,(function(){return M(this,(function(e){switch(e.label){case 0:return[4,_({excerpt:h.currentExcerpt})];case 1:return e.sent(),f(F(F({},h),{isUnderReview:!1})),S.trackEvent("excerpt_generator_accepted"),[2]}}))}))},children:(0,m.__)("Accept","wp-parsely")}),(0,w.jsx)(v.Button,{isDestructive:!0,variant:"secondary",onClick:function(){return L(void 0,void 0,void 0,(function(){return M(this,(function(e){return _({excerpt:h.oldExcerpt}),f(F(F({},h),{currentExcerpt:h.oldExcerpt,isUnderReview:!1})),S.trackEvent("excerpt_generator_discarded"),[2]}))}))},children:(0,m.__)("Discard","wp-parsely")})]}):(0,w.jsxs)(v.Button,{onClick:function(){return L(void 0,void 0,void 0,(function(){var e,t;return M(this,(function(n){switch(n.label){case 0:r(!0),a(void 0),n.label=1;case 1:return n.trys.push([1,3,4,5]),S.trackEvent("excerpt_generator_pressed"),[4,D.getInstance().generateExcerpt(I,T)];case 2:return e=n.sent(),f({currentExcerpt:e,isUnderReview:!0,newExcerptGeneratedCount:h.newExcerptGeneratedCount+1,oldExcerpt:k}),[3,5];case 3:return(t=n.sent())instanceof N?a(t):(a(new N((0,m.__)("An unknown error occurred.","wp-parsely"),b.UnknownError)),console.error(t)),[3,5];case 4:return r(!1),[7];case 5:return[2]}}))}))},variant:"primary",isBusy:t,disabled:t||!T,children:[t&&(0,m.__)("Generating Excerpt…","wp-parsely"),!t&&h.newExcerptGeneratedCount>0&&(0,m.__)("Regenerate Excerpt","wp-parsely"),!t&&0===h.newExcerptGeneratedCount&&(0,m.__)("Generate Excerpt","wp-parsely")]})}),(0,w.jsxs)(v.Button,{href:"https://docs.parse.ly/plugin-content-helper/#h-excerpt-generator-beta",target:"_blank",variant:"link",children:[(0,m.__)("Learn more about Parse.ly AI","wp-parsely"),(0,w.jsx)(v.Icon,{icon:A,size:18,className:"parsely-external-link-icon"})]})]})]})},H=function(){return(0,w.jsx)(v.Animate,{type:"loading",children:function(e){var t=e.className;return(0,w.jsx)("span",{className:t,children:(0,m.__)("Generating…","wp-parsely")})}})},q=function(){return(0,w.jsx)(g.PostTypeSupportCheck,{supportKeys:"excerpt",children:(0,w.jsx)(p,{name:"parsely-post-excerpt",title:(0,m.__)("Excerpt","wp-parsely"),children:(0,w.jsx)(G,{})})})};(0,y.addFilter)("plugins.registerPlugin","wp-parsely-excerpt-generator",(function(e,t){var r,n,o;return"wp-parsely-block-editor-sidebar"!==t||((null===(r=null===window||void 0===window?void 0:window.Jetpack_Editor_Initial_State)||void 0===r?void 0:r.available_blocks["ai-content-lens"])&&(console.log("Parse.ly: Jetpack AI is enabled and will be disabled."),(0,y.removeFilter)("blocks.registerBlockType","jetpack/ai-content-lens-features")),(0,h.registerPlugin)("wp-parsely-excerpt-generator",{render:q}),(null===(n=(0,d.dispatch)("core/editor"))||void 0===n?void 0:n.removeEditorPanel)?null===(o=(0,d.dispatch)("core/editor"))||void 0===o||o.removeEditorPanel("post-excerpt"):null==f||f.removeEditorPanel("post-excerpt")),e}),1e3)}()}();
\ No newline at end of file
diff --git a/phpstan.neon b/phpstan.neon
index c00d788c7..7f1422252 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -13,7 +13,7 @@ parameters:
- vendor/php-stubs/wordpress-stubs/wordpress-stubs.php
- vendor/php-stubs/wordpress-tests-stubs/wordpress-tests-stubs.php
type_coverage:
- return_type: 91
+ return_type: 90
param_type: 79.2
property_type: 0 # We can't use property types until PHP 7.4 becomes the plugin's minimum version.
print_suggestions: false
diff --git a/src/Endpoints/class-analytics-post-detail-api-proxy.php b/src/Endpoints/class-analytics-post-detail-api-proxy.php
deleted file mode 100644
index 787ae0e50..000000000
--- a/src/Endpoints/class-analytics-post-detail-api-proxy.php
+++ /dev/null
@@ -1,48 +0,0 @@
-register_endpoint( '/stats/post/detail' );
- }
-
- /**
- * Cached "proxy" to the Parse.ly `/analytics/post/detail` API endpoint.
- *
- * @param WP_REST_Request $request The request object.
- * @return stdClass|WP_Error stdClass containing the data or a WP_Error object on failure.
- */
- public function get_items( WP_REST_Request $request ) {
- return $this->get_data( $request );
- }
-
- /**
- * Generates the final data from the passed response.
- *
- * @param array $response The response received by the proxy.
- * @return array The generated data.
- */
- protected function generate_data( $response ): array {
- return $this->generate_post_data( $response );
- }
-}
diff --git a/src/Endpoints/class-analytics-posts-api-proxy.php b/src/Endpoints/class-analytics-posts-api-proxy.php
deleted file mode 100644
index 3ee862caf..000000000
--- a/src/Endpoints/class-analytics-posts-api-proxy.php
+++ /dev/null
@@ -1,48 +0,0 @@
-register_endpoint( '/stats/posts' );
- }
-
- /**
- * Cached "proxy" to the Parse.ly `/analytics/posts` API endpoint.
- *
- * @param WP_REST_Request $request The request object.
- * @return stdClass|WP_Error stdClass containing the data or a WP_Error object on failure.
- */
- public function get_items( WP_REST_Request $request ) {
- return $this->get_data( $request );
- }
-
- /**
- * Generates the final data from the passed response.
- *
- * @param array $response The response received by the proxy.
- * @return array The generated data.
- */
- protected function generate_data( $response ): array {
- return $this->generate_post_data( $response );
- }
-}
diff --git a/src/Endpoints/class-base-api-proxy.php b/src/Endpoints/class-base-api-proxy.php
deleted file mode 100644
index 627543128..000000000
--- a/src/Endpoints/class-base-api-proxy.php
+++ /dev/null
@@ -1,281 +0,0 @@
- $response The response received by the proxy.
- * @return array The generated data.
- */
- abstract protected function generate_data( $response ): array;
-
- /**
- * Cached "proxy" to the Parse.ly API endpoint.
- *
- * @param WP_REST_Request $request The request object.
- * @return stdClass|WP_Error stdClass containing the data or a WP_Error object on failure.
- */
- abstract public function get_items( WP_REST_Request $request );
-
- /**
- * Returns whether the endpoint is available for access by the current
- * user.
- *
- * @since 3.14.0 Renamed from `permission_callback()`.
- *
- * @return bool
- */
- public function is_available_to_current_user(): bool {
- return $this->api->is_available_to_current_user();
- }
-
- /**
- * Constructor.
- *
- * @param Parsely $parsely Instance of Parsely class.
- * @param Remote_API_Interface $api API object which does the actual calls to the Parse.ly API.
- */
- public function __construct( Parsely $parsely, Remote_API_Interface $api ) {
- $this->parsely = $parsely;
- $this->api = $api;
- }
-
- /**
- * Registers the endpoint's WP REST route.
- *
- * @param string $endpoint The endpoint's route (e.g. /stats/posts).
- * @param array $methods The HTTP methods to use for the endpoint.
- */
- protected function register_endpoint( string $endpoint, array $methods = array( WP_REST_Server::READABLE ) ): void {
- if ( ! apply_filters( 'wp_parsely_enable_' . Utils::convert_endpoint_to_filter_key( $endpoint ) . '_api_proxy', true ) ) {
- return;
- }
-
- $get_items_args = array(
- 'query' => array(
- 'default' => array(),
- 'sanitize_callback' => function ( array $query ) {
- $sanitized_query = array();
- foreach ( $query as $key => $value ) {
- $sanitized_query[ sanitize_key( $key ) ] = sanitize_text_field( $value );
- }
-
- return $sanitized_query;
- },
- ),
- );
-
- $rest_route_args = array(
- array(
- 'methods' => $methods,
- 'callback' => array( $this, 'get_items' ),
- 'permission_callback' => array( $this, 'is_available_to_current_user' ),
- 'args' => $get_items_args,
- 'show_in_index' => $this->is_available_to_current_user(),
- ),
- );
-
- register_rest_route( 'wp-parsely/v1', $endpoint, $rest_route_args );
- }
-
- /**
- * Cached "proxy" to the endpoint.
- *
- * @param WP_REST_Request $request The request object.
- * @param bool $require_api_secret Specifies if the API Secret is required.
- * @param string|null $param_item The param element to use to get the items.
- * @return stdClass|WP_Error stdClass containing the data or a WP_Error object on failure.
- */
- protected function get_data( WP_REST_Request $request, bool $require_api_secret = true, string $param_item = null ) {
- // Validate Site ID and secret.
- $validation = $this->validate_apikey_and_secret( $require_api_secret );
- if ( is_wp_error( $validation ) ) {
- return $validation;
- }
-
- if ( null !== $param_item ) {
- $params = $request->get_param( $param_item );
- } else {
- $params = $request->get_params();
- }
-
- if ( is_array( $params ) && isset( $params['itm_source'] ) ) {
- $this->itm_source = $params['itm_source'];
- }
-
- // A proxy with caching behavior is used here.
- $response = $this->api->get_items( $params );
-
- if ( is_wp_error( $response ) ) {
- return $response;
- }
-
- return (object) array(
- 'data' => $this->generate_data( $response ), // @phpstan-ignore-line.
- );
- }
-
- /**
- * Validates that the Site ID and secret are set.
- * If the API secret is not required, it will not be validated.
- *
- * @since 3.13.0
- *
- * @param bool $require_api_secret Specifies if the API Secret is required.
- * @return WP_Error|bool
- */
- protected function validate_apikey_and_secret( bool $require_api_secret = true ) {
- if ( false === $this->parsely->site_id_is_set() ) {
- return new WP_Error(
- 'parsely_site_id_not_set',
- __( 'A Parse.ly Site ID must be set in site options to use this endpoint', 'wp-parsely' ),
- array( 'status' => 403 )
- );
- }
-
- if ( $require_api_secret && false === $this->parsely->api_secret_is_set() ) {
- return new WP_Error(
- 'parsely_api_secret_not_set',
- __( 'A Parse.ly API Secret must be set in site options to use this endpoint', 'wp-parsely' ),
- array( 'status' => 403 )
- );
- }
-
- return true;
- }
-
- /**
- * Extracts the post data from the passed object.
- *
- * Should only be used with endpoints that return post data.
- *
- * @since 3.10.0
- *
- * @param stdClass $item The object to extract the data from.
- * @return array The extracted data.
- */
- protected function extract_post_data( stdClass $item ): array {
- $data = array();
-
- if ( isset( $item->author ) ) {
- $data['author'] = $item->author;
- }
-
- if ( isset( $item->metrics->views ) ) {
- $data['views'] = number_format_i18n( $item->metrics->views );
- }
-
- if ( isset( $item->metrics->visitors ) ) {
- $data['visitors'] = number_format_i18n( $item->metrics->visitors );
- }
-
- // The avg_engaged metric can be in different locations depending on the
- // endpoint and passed sort/url parameters.
- $avg_engaged = $item->metrics->avg_engaged ?? $item->avg_engaged ?? null;
- if ( null !== $avg_engaged ) {
- $data['avgEngaged'] = Utils::get_formatted_duration( (float) $avg_engaged );
- }
-
- if ( isset( $item->pub_date ) ) {
- $data['date'] = wp_date( Utils::get_date_format(), strtotime( $item->pub_date ) );
- }
-
- if ( isset( $item->title ) ) {
- $data['title'] = $item->title;
- }
-
- if ( isset( $item->url ) ) {
- $site_id = $this->parsely->get_site_id();
- // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.url_to_postid_url_to_postid
- $post_id = url_to_postid( $item->url ); // 0 if the post cannot be found.
-
- $post_url = Parsely::get_url_with_itm_source( $item->url, null );
- if ( Utils::parsely_is_https_supported() ) {
- $post_url = str_replace( 'http://', 'https://', $post_url );
- }
-
- $data['rawUrl'] = $post_url;
- $data['dashUrl'] = Parsely::get_dash_url( $site_id, $post_url );
- $data['id'] = Parsely::get_url_with_itm_source( $post_url, null ); // Unique.
- $data['postId'] = $post_id; // Might not be unique.
- $data['url'] = Parsely::get_url_with_itm_source( $post_url, $this->itm_source );
-
- // Set thumbnail URL, falling back to the Parse.ly thumbnail if needed.
- $thumbnail_url = get_the_post_thumbnail_url( $post_id, 'thumbnail' );
- if ( false !== $thumbnail_url ) {
- $data['thumbnailUrl'] = $thumbnail_url;
- } elseif ( isset( $item->thumb_url_medium ) ) {
- $data['thumbnailUrl'] = $item->thumb_url_medium;
- }
- }
-
- return $data;
- }
-
- /**
- * Generates the post data from the passed response.
- *
- * Should only be used with endpoints that return post data.
- *
- * @since 3.10.0
- *
- * @param array $response The response received by the proxy.
- * @return array The generated data.
- */
- protected function generate_post_data( array $response ): array {
- $data = array();
-
- foreach ( $response as $item ) {
- $data [] = (object) $this->extract_post_data( $item );
- }
-
- return $data;
- }
-}
diff --git a/src/Endpoints/class-base-endpoint.php b/src/Endpoints/class-base-endpoint.php
deleted file mode 100644
index d9e18812c..000000000
--- a/src/Endpoints/class-base-endpoint.php
+++ /dev/null
@@ -1,194 +0,0 @@
-parsely = $parsely;
- }
-
- /**
- * Returns the user capability allowing access to the endpoint, after having
- * applied capability filters.
- *
- * `DEFAULT_ACCESS_CAPABILITY` is not passed here by default, to allow for
- * a more explicit declaration in child classes.
- *
- * @since 3.14.0
- *
- * @param string $capability The original capability allowing access.
- * @return string The capability allowing access after applying the filters.
- */
- protected function apply_capability_filters( string $capability ): string {
- /**
- * Filter to change the default user capability for all private endpoints.
- *
- * @var string
- */
- $default_user_capability = apply_filters(
- 'wp_parsely_user_capability_for_all_private_apis',
- $capability
- );
-
- /**
- * Filter to change the user capability for the specific endpoint.
- *
- * @var string
- */
- $endpoint_specific_user_capability = apply_filters(
- 'wp_parsely_user_capability_for_' . Utils::convert_endpoint_to_filter_key( static::ENDPOINT ) . '_api',
- $default_user_capability
- );
-
- return $endpoint_specific_user_capability;
- }
-
- /**
- * Registers the endpoint's WP REST route.
- *
- * @since 3.11.0 Moved from Base_Endpoint_Remote into Base_Endpoint.
- *
- * @param string $endpoint The endpoint's route.
- * @param string $callback The callback function to call when the endpoint is hit.
- * @param array $methods The HTTP methods to allow for the endpoint.
- */
- public function register_endpoint(
- string $endpoint,
- string $callback,
- array $methods = array( 'GET' )
- ): void {
- if ( ! apply_filters( 'wp_parsely_enable_' . Utils::convert_endpoint_to_filter_key( $endpoint ) . '_api_proxy', true ) ) {
- return;
- }
-
- $get_items_args = array(
- 'query' => array(
- 'default' => array(),
- 'sanitize_callback' => function ( array $query ) {
- $sanitized_query = array();
- foreach ( $query as $key => $value ) {
- $sanitized_query[ sanitize_key( $key ) ] = sanitize_text_field( $value );
- }
-
- return $sanitized_query;
- },
- ),
- );
-
- $rest_route_args = array(
- array(
- 'methods' => $methods,
- 'callback' => array( $this, $callback ),
- 'permission_callback' => array( $this, 'is_available_to_current_user' ),
- 'args' => $get_items_args,
- 'show_in_index' => static::is_available_to_current_user(),
- ),
- );
-
- register_rest_route( 'wp-parsely/v1', $endpoint, $rest_route_args );
- }
-
- /**
- * Registers the endpoint's WP REST route with arguments.
- *
- * @since 3.16.0
- *
- * @param string $endpoint The endpoint's route.
- * @param string $callback The callback function to call when the endpoint is hit.
- * @param array $methods The HTTP methods to allow for the endpoint.
- * @param array $args The arguments for the endpoint.
- */
- public function register_endpoint_with_args(
- string $endpoint,
- string $callback,
- array $methods = array( 'GET' ),
- array $args = array()
- ): void {
- if ( ! apply_filters( 'wp_parsely_enable_' . Utils::convert_endpoint_to_filter_key( $endpoint ) . '_api_proxy', true ) ) {
- return;
- }
-
- $rest_route_args = array(
- array(
- 'methods' => $methods,
- 'callback' => array( $this, $callback ),
- 'permission_callback' => array( $this, 'is_available_to_current_user' ),
- 'args' => $args,
- 'show_in_index' => static::is_available_to_current_user(),
- ),
- );
-
- register_rest_route( 'wp-parsely/v1', $endpoint, $rest_route_args );
- }
-}
diff --git a/src/Endpoints/class-referrers-post-detail-api-proxy.php b/src/Endpoints/class-referrers-post-detail-api-proxy.php
deleted file mode 100644
index 812a9ca81..000000000
--- a/src/Endpoints/class-referrers-post-detail-api-proxy.php
+++ /dev/null
@@ -1,259 +0,0 @@
-register_endpoint( '/referrers/post/detail' );
- }
-
- /**
- * Cached "proxy" to the Parse.ly `/referrers/post/detail` API endpoint.
- *
- * @since 3.6.0
- *
- * @param WP_REST_Request $request The request object.
- * @return stdClass|WP_Error stdClass containing the data or a WP_Error object on failure.
- */
- public function get_items( WP_REST_Request $request ) {
- $total_views = $request->get_param( 'total_views' ) ?? '0';
-
- if ( ! is_string( $total_views ) ) {
- $total_views = '0';
- }
-
- $this->total_views = Utils::convert_to_positive_integer( $total_views );
- $request->offsetUnset( 'total_views' ); // Remove param from request.
- return $this->get_data( $request );
- }
-
- /**
- * Generates the final data from the passed response.
- *
- * @since 3.6.0
- *
- * @param array $response The response received by the proxy.
- * @return array The generated data.
- */
- protected function generate_data( $response ): array {
- $referrers_types = $this->generate_referrer_types_data( $response );
- $direct_views = Utils::convert_to_positive_integer(
- $referrers_types->direct->views ?? '0'
- );
- $referrers_top = $this->generate_referrers_data( 5, $response, $direct_views );
-
- return array(
- 'top' => $referrers_top,
- 'types' => $referrers_types,
- );
- }
-
- /**
- * Generates the referrer types data.
- *
- * Referrer types are:
- * - `social`: Views coming from social media.
- * - `search`: Views coming from search engines.
- * - `other`: Views coming from other referrers, like external websites.
- * - `internal`: Views coming from linking pages of the same website.
- *
- * Returned object properties:
- * - `views`: The number of views.
- * - `viewPercentage`: The number of views as a percentage, compared to the
- * total views of all referrer types.
- *
- * @since 3.6.0
- *
- * @param array $response The response received by the proxy.
- * @return stdClass The generated data.
- */
- private function generate_referrer_types_data( array $response ): stdClass {
- $result = new stdClass();
- $total_referrer_views = 0; // Views from all referrer types combined.
-
- // Set referrer type order as it is displayed in the Parse.ly dashboard.
- $referrer_type_keys = array( 'social', 'search', 'other', 'internal', 'direct' );
- foreach ( $referrer_type_keys as $key ) {
- $result->$key = (object) array( 'views' => 0 );
- }
-
- // Set views and views totals.
- foreach ( $response as $referrer_data ) {
- /**
- * Variable.
- *
- * @var int
- */
- $current_views = $referrer_data->metrics->referrers_views ?? 0;
- $total_referrer_views += $current_views;
-
- /**
- * Variable.
- *
- * @var string
- */
- $current_key = $referrer_data->type ?? '';
- if ( '' !== $current_key ) {
- if ( ! isset( $result->$current_key->views ) ) {
- $result->$current_key = (object) array( 'views' => 0 );
- }
-
- $result->$current_key->views += $current_views;
- }
- }
-
- // Add direct and total views to the object.
- $result->direct->views = $this->total_views - $total_referrer_views;
- $result->totals = (object) array( 'views' => $this->total_views );
-
- // Remove referrer types without views.
- foreach ( $referrer_type_keys as $key ) {
- if ( 0 === $result->$key->views ) {
- unset( $result->$key );
- }
- }
-
- // Set percentage values and format numbers.
- // @phpstan-ignore-next-line.
- foreach ( $result as $key => $value ) {
- // Set and format percentage values.
- $result->{ $key }->viewsPercentage = $this->get_i18n_percentage(
- absint( $value->views ),
- $this->total_views
- );
-
- // Format views values.
- $result->{ $key }->views = number_format_i18n( $result->{ $key }->views );
- }
-
- return $result;
- }
-
- /**
- * Generates the top referrers data.
- *
- * Returned object properties:
- * - `views`: The number of views.
- * - `viewPercentage`: The number of views as a percentage, compared to the
- * total views of all referrer types.
- * - `datasetViewsPercentage: The number of views as a percentage, compared
- * to the total views of the current dataset.
- *
- * @since 3.6.0
- *
- * @param int $limit The limit of returned referrers.
- * @param array $response The response received by the proxy.
- * @param int $direct_views The count of direct views.
- * @return stdClass The generated data.
- */
- private function generate_referrers_data(
- int $limit,
- array $response,
- int $direct_views
- ): stdClass {
- $temp_views = array();
- $totals = 0;
- $referrer_count = count( $response );
-
- // Set views and views totals.
- $loop_count = $referrer_count > $limit ? $limit : $referrer_count;
- for ( $i = 0; $i < $loop_count; $i++ ) {
- $data = $response[ $i ];
-
- /**
- * Variable.
- *
- * @var int
- */
- $referrer_views = $data->metrics->referrers_views ?? 0;
- $totals += $referrer_views;
- if ( isset( $data->name ) ) {
- $temp_views[ $data->name ] = $referrer_views;
- }
- }
-
- // If applicable, add the direct views.
- if ( isset( $referrer_views ) && $direct_views >= $referrer_views ) {
- $temp_views['direct'] = $direct_views;
- $totals += $direct_views;
- arsort( $temp_views );
- if ( count( $temp_views ) > $limit ) {
- $totals -= array_pop( $temp_views );
- }
- }
-
- // Convert temporary array to result object and add totals.
- $result = new stdClass();
- foreach ( $temp_views as $key => $value ) {
- $result->$key = (object) array( 'views' => $value );
- }
- $result->totals = (object) array( 'views' => $totals );
-
- // Set percentages values and format numbers.
- // @phpstan-ignore-next-line.
- foreach ( $result as $key => $value ) {
- // Percentage against all referrer views, even those not included
- // in the dataset due to the $limit argument.
- $result->{ $key }->viewsPercentage = $this
- ->get_i18n_percentage( absint( $value->views ), $this->total_views );
-
- // Percentage against the current dataset that is limited due to the
- // $limit argument.
- $result->{ $key }->datasetViewsPercentage = $this
- ->get_i18n_percentage( absint( $value->views ), $totals );
-
- // Format views values.
- $result->{ $key }->views = number_format_i18n( $result->{ $key }->views );
- }
-
- return $result;
- }
-
- /**
- * Returns the passed number compared to the passed total, in an
- * internationalized percentage format.
- *
- * @since 3.6.0
- *
- * @param int $number The number to be calculated as a percentage.
- * @param int $total The total number to compare against.
- * @return string|false The internationalized percentage or false on error.
- */
- private function get_i18n_percentage( int $number, int $total ) {
- if ( 0 === $total ) {
- return false;
- }
-
- return number_format_i18n( $number / $total * 100, 2 );
- }
-}
diff --git a/src/Endpoints/class-related-api-proxy.php b/src/Endpoints/class-related-api-proxy.php
deleted file mode 100644
index 983536362..000000000
--- a/src/Endpoints/class-related-api-proxy.php
+++ /dev/null
@@ -1,61 +0,0 @@
-register_endpoint( '/related' );
- }
-
- /**
- * Cached "proxy" to the Parse.ly `/related` API endpoint.
- *
- * @param WP_REST_Request $request The request object.
- * @return stdClass|WP_Error stdClass containing the data or a WP_Error object on failure.
- */
- public function get_items( WP_REST_Request $request ) {
- return $this->get_data( $request, false, 'query' );
- }
-
- /**
- * Generates the final data from the passed response.
- *
- * @param array $response The response received by the proxy.
- * @return array The generated data.
- */
- protected function generate_data( $response ): array {
- $itm_source = $this->itm_source;
-
- return array_map(
- static function ( stdClass $item ) use ( $itm_source ) {
- return (object) array(
- 'image_url' => $item->image_url,
- 'thumb_url_medium' => $item->thumb_url_medium,
- 'title' => $item->title,
- 'url' => Parsely::get_url_with_itm_source( $item->url, $itm_source ),
- );
- },
- $response
- );
- }
-}
diff --git a/src/Endpoints/content-suggestions/class-suggest-brief-api-proxy.php b/src/Endpoints/content-suggestions/class-suggest-brief-api-proxy.php
deleted file mode 100644
index 78bb9c327..000000000
--- a/src/Endpoints/content-suggestions/class-suggest-brief-api-proxy.php
+++ /dev/null
@@ -1,152 +0,0 @@
-suggest_brief_api = new Suggest_Brief_API( $parsely );
- parent::__construct( $parsely, $this->suggest_brief_api );
- }
-
- /**
- * Registers the endpoint's WP REST route.
- *
- * @since 3.13.0
- */
- public function run(): void {
- $this->register_endpoint( '/content-suggestions/suggest-brief', array( 'POST' ) );
- }
-
- /**
- * Generates the final data from the passed response.
- *
- * @since 3.13.0
- *
- * @param array $response The response received by the proxy.
- * @return array The generated data.
- */
- protected function generate_data( $response ): array {
- // Unused function.
- return $response;
- }
-
- /**
- * Cached "proxy" to the Parse.ly API endpoint.
- *
- * @since 3.13.0
- *
- * @param WP_REST_Request $request The request object.
- * @return stdClass|WP_Error stdClass containing the data or a WP_Error object on failure.
- */
- public function get_items( WP_REST_Request $request ) {
- $validation = $this->validate_apikey_and_secret();
- if ( is_wp_error( $validation ) ) {
- return $validation;
- }
-
- $pch_options = $this->parsely->get_options()['content_helper'];
- if ( ! Permissions::current_user_can_use_pch_feature( 'excerpt_suggestions', $pch_options ) ) {
- return new WP_Error( 'ch_access_to_feature_disabled', '', array( 'status' => 403 ) );
- }
-
- /**
- * The post content to be sent to the API.
- *
- * @var string|null $post_content
- */
- $post_content = $request->get_param( 'content' );
-
- /**
- * The post title to be sent to the API.
- *
- * @var string|null $post_title
- */
- $post_title = $request->get_param( 'title' );
-
- /**
- * The persona to be sent to the API.
- *
- * @var string|null $persona
- */
- $persona = $request->get_param( 'persona' );
-
- /**
- * The style to be sent to the API.
- *
- * @var string|null $style
- */
- $style = $request->get_param( 'style' );
-
- if ( null === $post_content ) {
- return new WP_Error(
- 'parsely_content_not_set',
- __( 'A post content must be set to use this endpoint', 'wp-parsely' ),
- array( 'status' => 403 )
- );
- }
-
- if ( null === $post_title ) {
- return new WP_Error(
- 'parsely_title_not_set',
- __( 'A post title must be set to use this endpoint', 'wp-parsely' ),
- array( 'status' => 403 )
- );
- }
-
- if ( null === $persona ) {
- $persona = 'journalist';
- }
-
- if ( null === $style ) {
- $style = 'neutral';
- }
-
- $response = $this->suggest_brief_api->get_suggestion( $post_title, $post_content, $persona, $style );
-
- if ( is_wp_error( $response ) ) {
- return $response;
- }
-
- return (object) array(
- 'data' => $response,
- );
- }
-}
diff --git a/src/Endpoints/content-suggestions/class-suggest-headline-api-proxy.php b/src/Endpoints/content-suggestions/class-suggest-headline-api-proxy.php
deleted file mode 100644
index 49e02a77a..000000000
--- a/src/Endpoints/content-suggestions/class-suggest-headline-api-proxy.php
+++ /dev/null
@@ -1,123 +0,0 @@
-suggest_headline_api = new Suggest_Headline_API( $parsely );
- parent::__construct( $parsely, $this->suggest_headline_api );
- }
-
- /**
- * Registers the endpoint's WP REST route.
- *
- * @since 3.12.0
- */
- public function run(): void {
- $this->register_endpoint( '/content-suggestions/suggest-headline', array( 'POST' ) );
- }
-
- /**
- * Generates the final data from the passed response.
- *
- * @since 3.12.0
- *
- * @param array $response The response received by the proxy.
- * @return array The generated data.
- */
- protected function generate_data( $response ): array {
- // Unused function.
- return $response;
- }
-
- /**
- * Cached "proxy" to the Parse.ly API endpoint.
- *
- * @since 3.12.0
- *
- * @param WP_REST_Request $request The request object.
- * @return stdClass|WP_Error stdClass containing the data or a WP_Error object on failure.
- */
- public function get_items( WP_REST_Request $request ) {
- $validation = $this->validate_apikey_and_secret();
- if ( is_wp_error( $validation ) ) {
- return $validation;
- }
-
- $pch_options = $this->parsely->get_options()['content_helper'];
- if ( ! Permissions::current_user_can_use_pch_feature( 'title_suggestions', $pch_options ) ) {
- return new WP_Error( 'ch_access_to_feature_disabled', '', array( 'status' => 403 ) );
- }
-
- /**
- * The post content to be sent to the API.
- *
- * @var string|null $post_content
- */
- $post_content = $request->get_param( 'content' );
-
- if ( null === $post_content ) {
- return new WP_Error(
- 'parsely_content_not_set',
- __( 'A post content must be set to use this endpoint', 'wp-parsely' ),
- array( 'status' => 403 )
- );
- }
-
- $limit = is_numeric( $request->get_param( 'limit' ) ) ? intval( $request->get_param( 'limit' ) ) : 3;
- $tone = is_string( $request->get_param( 'tone' ) ) ? $request->get_param( 'tone' ) : 'neutral';
- $persona = is_string( $request->get_param( 'persona' ) ) ? $request->get_param( 'persona' ) : 'journalist';
-
- if ( 0 === $limit ) {
- $limit = 3;
- }
-
- $response = $this->suggest_headline_api->get_titles( $post_content, $limit, $persona, $tone );
-
- if ( is_wp_error( $response ) ) {
- return $response;
- }
-
- return (object) array(
- 'data' => $response,
- );
- }
-}
diff --git a/src/Endpoints/content-suggestions/class-suggest-linked-reference-api-proxy.php b/src/Endpoints/content-suggestions/class-suggest-linked-reference-api-proxy.php
deleted file mode 100644
index c224155ed..000000000
--- a/src/Endpoints/content-suggestions/class-suggest-linked-reference-api-proxy.php
+++ /dev/null
@@ -1,162 +0,0 @@
-suggest_linked_reference_api = new Suggest_Linked_Reference_API( $parsely );
- parent::__construct( $parsely, $this->suggest_linked_reference_api );
- }
-
- /**
- * Registers the endpoint's WP REST route.
- *
- * @since 3.14.0
- */
- public function run(): void {
- $this->register_endpoint( '/content-suggestions/suggest-linked-reference', array( 'POST' ) );
- }
-
- /**
- * Generates the final data from the passed response.
- *
- * @since 3.14.0
- *
- * @param array $response The response received by the proxy.
- * @return array The generated data.
- */
- protected function generate_data( $response ): array {
- // Unused function.
- return $response;
- }
-
- /**
- * Cached "proxy" to the Parse.ly API endpoint.
- *
- * @since 3.14.0
- *
- * @param WP_REST_Request $request The request object.
- * @return stdClass|WP_Error stdClass containing the data or a WP_Error
- * object on failure.
- */
- public function get_items( WP_REST_Request $request ) {
- $validation = $this->validate_apikey_and_secret();
- if ( is_wp_error( $validation ) ) {
- return $validation;
- }
-
- $pch_options = $this->parsely->get_options()['content_helper'];
- if ( ! Permissions::current_user_can_use_pch_feature( 'smart_linking', $pch_options ) ) {
- return new WP_Error( 'ch_access_to_feature_disabled', '', array( 'status' => 403 ) );
- }
-
- /**
- * The post content to be sent to the API.
- *
- * @var string|null $post_content
- */
- $post_content = $request->get_param( 'text' );
-
- /**
- * The maximum amount of words of the link text.
- *
- * @var string|null $max_link_words
- */
- $max_link_words = $request->get_param( 'max_link_words' );
-
- /**
- * The maximum number of links to return.
- *
- * @var string|null $max_links
- */
- $max_links = $request->get_param( 'max_links' );
-
- /**
- * The URL exclusion list.
- *
- * @var mixed $url_exclusion_list
- */
- $url_exclusion_list = $request->get_param( 'url_exclusion_list' ) ?? array();
-
- if ( null === $post_content ) {
- return new WP_Error(
- 'parsely_content_not_set',
- __( 'A post content must be set to use this endpoint', 'wp-parsely' ),
- array( 'status' => 403 )
- );
- }
-
- if ( is_numeric( $max_link_words ) ) {
- $max_link_words = (int) $max_link_words;
- } else {
- $max_link_words = 4;
- }
-
- if ( is_numeric( $max_links ) ) {
- $max_links = (int) $max_links;
- } else {
- $max_links = 10;
- }
-
- if ( ! is_array( $url_exclusion_list ) ) {
- $url_exclusion_list = array();
- }
-
- $response = $this->suggest_linked_reference_api->get_links(
- $post_content,
- $max_link_words,
- $max_links,
- $url_exclusion_list
- );
-
- if ( is_wp_error( $response ) ) {
- return $response;
- }
-
- // Convert the smart links to an array of objects.
- $smart_links = array();
- foreach ( $response as $link ) {
- $smart_links[] = $link->to_array();
- }
-
- return (object) array(
- 'data' => $smart_links,
- );
- }
-}
diff --git a/src/Models/class-smart-link.php b/src/Models/class-smart-link.php
index 009998a63..469a00bee 100644
--- a/src/Models/class-smart-link.php
+++ b/src/Models/class-smart-link.php
@@ -127,7 +127,9 @@ public function __construct(
int $offset,
int $post_id = 0
) {
- $this->set_href( $href );
+ if ( '' !== $href ) {
+ $this->set_href( $href );
+ }
// Set the title to be the destination post title if the destination post ID is set.
if ( 0 !== $this->destination_post_id ) {
diff --git a/src/RemoteAPI/class-analytics-post-detail-api.php b/src/RemoteAPI/class-analytics-post-detail-api.php
deleted file mode 100644
index 8d946f521..000000000
--- a/src/RemoteAPI/class-analytics-post-detail-api.php
+++ /dev/null
@@ -1,45 +0,0 @@
-apply_capability_filters(
- Base_Endpoint::DEFAULT_ACCESS_CAPABILITY
- )
- );
- }
-}
diff --git a/src/RemoteAPI/class-analytics-posts-api.php b/src/RemoteAPI/class-analytics-posts-api.php
deleted file mode 100644
index 9b70421cc..000000000
--- a/src/RemoteAPI/class-analytics-posts-api.php
+++ /dev/null
@@ -1,107 +0,0 @@
-,
- * }
- *
- * @phpstan-type Analytics_Post array{
- * title?: string,
- * url?: string,
- * link?: string,
- * author?: string,
- * authors?: string[],
- * section?: string,
- * tags?: string[],
- * metrics?: Analytics_Post_Metrics,
- * full_content_word_count?: int,
- * image_url?: string,
- * metadata?: string,
- * pub_date?: string,
- * thumb_url_medium?: string,
- * }
- *
- * @phpstan-type Analytics_Post_Metrics array{
- * avg_engaged?: float,
- * views?: int,
- * visitors?: int,
- * }
- */
-class Analytics_Posts_API extends Base_Endpoint_Remote {
- public const MAX_RECORDS_LIMIT = 2000;
- public const ANALYTICS_API_DAYS_LIMIT = 7;
-
- protected const API_BASE_URL = Parsely::PUBLIC_API_BASE_URL;
- protected const ENDPOINT = '/analytics/posts';
- protected const QUERY_FILTER = 'wp_parsely_analytics_posts_endpoint_args';
-
- /**
- * Returns whether the endpoint is available for access by the current
- * user.
- *
- * @since 3.14.0
- * @since 3.16.0 Added the `$request` parameter.
- *
- * @param WP_REST_Request|null $request The request object.
- * @return bool
- */
- public function is_available_to_current_user( $request = null ): bool {
- return current_user_can(
- // phpcs:ignore WordPress.WP.Capabilities.Undetermined
- $this->apply_capability_filters(
- Base_Endpoint::DEFAULT_ACCESS_CAPABILITY
- )
- );
- }
-
- /**
- * Calls Parse.ly Analytics API to get posts info.
- *
- * Main purpose of this function is to enforce typing.
- *
- * @param Analytics_Post_API_Params $api_params Parameters of the API.
- * @return Analytics_Post[]|WP_Error|null
- */
- public function get_posts_analytics( $api_params ) {
- return $this->get_items( $api_params, true ); // @phpstan-ignore-line
- }
-
- /**
- * Returns the request's options for the remote API call.
- *
- * @since 3.9.0
- *
- * @return array The array of options.
- */
- public function get_request_options(): array {
- return array(
- 'timeout' => 30, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout
- );
- }
-}
diff --git a/src/RemoteAPI/class-base-endpoint-remote.php b/src/RemoteAPI/class-base-endpoint-remote.php
deleted file mode 100644
index 334f38e0d..000000000
--- a/src/RemoteAPI/class-base-endpoint-remote.php
+++ /dev/null
@@ -1,149 +0,0 @@
- $query The query arguments to send to the remote API.
- * @throws UnexpectedValueException If the endpoint constant is not defined.
- * @throws UnexpectedValueException If the query filter constant is not defined.
- * @return string
- */
- public function get_api_url( array $query ): string {
- $this->validate_required_constraints();
-
- $query['apikey'] = $this->parsely->get_site_id();
- if ( $this->parsely->api_secret_is_set() ) {
- $query['secret'] = $this->parsely->get_api_secret();
- }
- $query = array_filter( $query );
-
- // Sort by key so the query args are in alphabetical order.
- ksort( $query );
-
- // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound -- Hook names are defined in child classes.
- $query = apply_filters( static::QUERY_FILTER, $query );
- return add_query_arg( $query, static::API_BASE_URL . static::ENDPOINT );
- }
-
- /**
- * Gets items from the specified endpoint.
- *
- * @since 3.2.0
- * @since 3.7.0 Added $associative param.
- *
- * @param array $query The query arguments to send to the remote API.
- * @param bool $associative When TRUE, returned objects will be converted into associative arrays.
- * @return array|object|WP_Error
- */
- public function get_items( array $query, bool $associative = false ) {
- $full_api_url = $this->get_api_url( $query );
-
- /**
- * GET request options.
- *
- * @var WP_HTTP_Request_Args $options
- */
- $options = $this->get_request_options();
- $response = wp_safe_remote_get( $full_api_url, $options );
-
- if ( is_wp_error( $response ) ) {
- return $response;
- }
-
- $body = wp_remote_retrieve_body( $response );
- $decoded = json_decode( $body );
-
- if ( ! is_object( $decoded ) ) {
- return new WP_Error( 400, __( 'Unable to decode upstream API response', 'wp-parsely' ) );
- }
-
- if ( ! property_exists( $decoded, 'data' ) ) {
- return new WP_Error( $decoded->code ?? 400, $decoded->message ?? __( 'Unable to read data from upstream API', 'wp-parsely' ) );
- }
-
- if ( ! is_array( $decoded->data ) ) {
- return new WP_Error( 400, __( 'Unable to parse data from upstream API', 'wp-parsely' ) );
- }
-
- $data = $decoded->data;
-
- return $associative ? Utils::convert_to_associative_array( $data ) : $data;
- }
-
- /**
- * Returns the request's options for the remote API call.
- *
- * @since 3.9.0
- *
- * @return array The array of options.
- */
- public function get_request_options(): array {
- return array();
- }
-
- /**
- * Validates that required constants are defined.
- *
- * @since 3.14.0
- *
- * @throws UnexpectedValueException If any required constant is not defined.
- */
- protected function validate_required_constraints(): void {
- if ( static::ENDPOINT === '' ) {
- throw new UnexpectedValueException( 'ENDPOINT constant must be defined in child class.' );
- }
- if ( static::QUERY_FILTER === '' ) {
- throw new UnexpectedValueException( 'QUERY_FILTER constant must be defined in child class.' );
- }
- }
-}
diff --git a/src/RemoteAPI/class-referrers-post-detail-api.php b/src/RemoteAPI/class-referrers-post-detail-api.php
deleted file mode 100644
index 45233630e..000000000
--- a/src/RemoteAPI/class-referrers-post-detail-api.php
+++ /dev/null
@@ -1,45 +0,0 @@
-apply_capability_filters(
- Base_Endpoint::DEFAULT_ACCESS_CAPABILITY
- )
- );
- }
-}
diff --git a/src/RemoteAPI/class-related-api.php b/src/RemoteAPI/class-related-api.php
deleted file mode 100644
index ea130bf02..000000000
--- a/src/RemoteAPI/class-related-api.php
+++ /dev/null
@@ -1,39 +0,0 @@
-remote_api = $remote_api;
- $this->cache = $cache;
- }
-
- /**
- * Implements caching for the Remote API interface.
- *
- * @param array $query The query arguments to send to the remote API.
- * @param bool $associative Always `false`, just present to make definition compatible
- * with interface.
- * @return array|object|WP_Error The response from the remote API, or false if the
- * response is empty.
- */
- public function get_items( array $query, bool $associative = false ) {
- $cache_key = 'parsely_api_' .
- wp_hash( $this->remote_api->get_endpoint() ) . '_' .
- wp_hash( (string) wp_json_encode( $query ) );
-
- /**
- * Variable.
- *
- * @var array|false
- */
- $items = $this->cache->get( $cache_key, self::CACHE_GROUP );
-
- if ( false === $items ) {
- $items = $this->remote_api->get_items( $query );
- $this->cache->set( $cache_key, $items, self::CACHE_GROUP, self::OBJECT_CACHE_TTL );
- }
-
- return $items;
- }
-
- /**
- * Returns whether the endpoint is available for access by the current
- * user.
- *
- * @since 3.7.0
- * @since 3.14.0 Renamed from `is_user_allowed_to_make_api_call()`.
- * @since 3.16.0 Added the `$request` parameter.
- *
- * @param WP_REST_Request|null $request The request object.
- * @return bool
- */
- public function is_available_to_current_user( $request = null ): bool {
- return $this->remote_api->is_available_to_current_user( $request );
- }
-}
diff --git a/src/RemoteAPI/class-validate-api.php b/src/RemoteAPI/class-validate-api.php
deleted file mode 100644
index 2fb0674e7..000000000
--- a/src/RemoteAPI/class-validate-api.php
+++ /dev/null
@@ -1,124 +0,0 @@
-apply_capability_filters(
- Base_Endpoint::DEFAULT_ACCESS_CAPABILITY
- )
- );
- }
-
- /**
- * Gets the URL for the Parse.ly API credentials validation endpoint.
- *
- * @since 3.11.0
- *
- * @param array $query The query arguments to send to the remote API.
- * @return string
- */
- public function get_api_url( array $query ): string {
- $query = array(
- 'apikey' => $query['apikey'],
- 'secret' => $query['secret'],
- );
-
- return add_query_arg( $query, static::API_BASE_URL . static::ENDPOINT );
- }
-
- /**
- * Queries the Parse.ly API credentials validation endpoint.
- * The API will return a 200 response if the credentials are valid and a 401 response if they are not.
- *
- * @param array $query The query arguments to send to the remote API.
- * @return object|WP_Error The response from the remote API, or a WP_Error object if the response is an error.
- */
- private function api_validate_credentials( array $query ) {
- /**
- * GET request options.
- *
- * @var WP_HTTP_Request_Args $options
- */
- $options = $this->get_request_options();
- $response = wp_safe_remote_get( $this->get_api_url( $query ), $options );
-
- if ( is_wp_error( $response ) ) {
- return $response;
- }
-
- $body = wp_remote_retrieve_body( $response );
- $decoded = json_decode( $body );
-
- if ( ! is_object( $decoded ) ) {
- return new WP_Error(
- 400,
- __(
- 'Unable to decode upstream API response',
- 'wp-parsely'
- )
- );
- }
-
- if ( ! property_exists( $decoded, 'success' ) || false === $decoded->success ) {
- return new WP_Error(
- $decoded->code ?? 400,
- $decoded->message ?? __( 'Unable to read data from upstream API', 'wp-parsely' )
- );
- }
-
- return $decoded;
- }
-
- /**
- * Returns the response from the Parse.ly API credentials validation endpoint.
- *
- * @since 3.11.0
- *
- * @param array $query The query arguments to send to the remote API.
- * @param bool $associative (optional) When TRUE, returned objects will be converted into
- * associative arrays.
- * @return array|object|WP_Error
- */
- public function get_items( array $query, bool $associative = false ) {
- $api_request = $this->api_validate_credentials( $query );
- return $associative ? Utils::convert_to_associative_array( $api_request ) : $api_request;
- }
-}
diff --git a/src/RemoteAPI/class-wordpress-cache.php b/src/RemoteAPI/class-wordpress-cache.php
deleted file mode 100644
index 59c63e32b..000000000
--- a/src/RemoteAPI/class-wordpress-cache.php
+++ /dev/null
@@ -1,52 +0,0 @@
-apply_capability_filters(
- Base_Endpoint::DEFAULT_ACCESS_CAPABILITY
- )
- );
- }
-
- /**
- * Returns the request's options for the remote API call.
- *
- * @since 3.12.0
- *
- * @return array The array of options.
- */
- public function get_request_options(): array {
- $options = array(
- 'headers' => array( 'Content-Type' => 'application/json; charset=utf-8' ),
- 'data_format' => 'body',
- 'timeout' => 60, //phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout
- 'body' => '{}',
- );
-
- // Add API key to request headers.
- if ( $this->parsely->api_secret_is_set() ) {
- $options['headers']['X-APIKEY-SECRET'] = $this->parsely->get_api_secret();
- }
-
- return $options;
- }
-
- /**
- * Gets the URL for a particular Parse.ly API Content Suggestion endpoint.
- *
- * @since 3.14.0
- *
- * @param array $query The query arguments to send to the remote API.
- * @throws UnexpectedValueException If the endpoint constant is not defined.
- * @throws UnexpectedValueException If the query filter constant is not defined.
- * @return string
- */
- public function get_api_url( array $query = array() ): string {
- $this->validate_required_constraints();
-
- $query['apikey'] = $this->parsely->get_site_id();
-
- // Remove empty entries and sort by key so the query args are in
- // alphabetical order.
- $query = array_filter( $query );
- ksort( $query );
-
- // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound -- Hook names are defined in child classes.
- $query = apply_filters( static::QUERY_FILTER, $query );
- return add_query_arg( $query, static::API_BASE_URL . static::ENDPOINT );
- }
-
- /**
- * Sends a POST request to the Parse.ly Content Suggestion API.
- *
- * This method sends a POST request to the Parse.ly Content Suggestion API and returns the
- * response. The response is either a WP_Error object in case of an error, or a decoded JSON
- * object in case of a successful request.
- *
- * @since 3.13.0
- *
- * @param array $query An associative array containing the query
- * parameters for the API request.
- * @param array> $body An associative array containing the body
- * parameters for the API request.
- * @return WP_Error|object Returns a WP_Error object in case of an error, or a decoded JSON
- * object in case of a successful request.
- */
- protected function post_request( array $query = array(), array $body = array() ) {
- $full_api_url = $this->get_api_url( $query );
-
- /**
- * GET request options.
- *
- * @var WP_HTTP_Request_Args $options
- */
- $options = $this->get_request_options();
- if ( count( $body ) > 0 ) {
- $body = $this->truncate_array_content( $body );
-
- $options['body'] = wp_json_encode( $body );
- if ( false === $options['body'] ) {
- return new WP_Error( 400, __( 'Unable to encode request body', 'wp-parsely' ) );
- }
- }
-
- $response = wp_safe_remote_post( $full_api_url, $options );
- if ( is_wp_error( $response ) ) {
- return $response;
- }
-
- // Handle any errors returned by the API.
- if ( 200 !== $response['response']['code'] ) {
- $error = json_decode( wp_remote_retrieve_body( $response ), true );
-
- if ( ! is_array( $error ) ) {
- return new WP_Error(
- 400,
- __( 'Unable to decode upstream API error', 'wp-parsely' )
- );
- }
-
- return new WP_Error( $error['error'], $error['detail'] );
- }
-
- $body = wp_remote_retrieve_body( $response );
- $decoded = json_decode( $body );
-
- if ( ! is_object( $decoded ) ) {
- return new WP_Error( 400, __( 'Unable to decode upstream API response', 'wp-parsely' ) );
- }
-
- return $decoded;
- }
-
- /**
- * Truncates the content of an array to a maximum length.
- *
- * @since 3.14.1
- *
- * @param string|array|mixed $content The content to truncate.
- * @return string|array|mixed The truncated content.
- */
- public function truncate_array_content( $content ) {
- if ( is_array( $content ) ) {
- // If the content is an array, iterate over its elements.
- foreach ( $content as $key => $value ) {
- // Recursively process/truncate each element of the array.
- $content[ $key ] = $this->truncate_array_content( $value );
- }
- return $content;
- } elseif ( is_string( $content ) ) {
- // If the content is a string, truncate it.
- if ( static::TRUNCATE_CONTENT ) {
- // Check if the string length exceeds the maximum and truncate if necessary.
- if ( mb_strlen( $content ) > self::TRUNCATE_CONTENT_LENGTH ) {
- return mb_substr( $content, 0, self::TRUNCATE_CONTENT_LENGTH );
- }
- }
- return $content;
- }
- return $content;
- }
-}
diff --git a/src/RemoteAPI/content-suggestions/class-suggest-brief-api.php b/src/RemoteAPI/content-suggestions/class-suggest-brief-api.php
deleted file mode 100644
index 7a9ffa7fe..000000000
--- a/src/RemoteAPI/content-suggestions/class-suggest-brief-api.php
+++ /dev/null
@@ -1,73 +0,0 @@
- array(
- 'persona' => $persona,
- 'style' => $style,
- ),
- 'title' => $title,
- 'text' => wp_strip_all_tags( $content ),
- );
-
- $decoded = $this->post_request( array(), $body );
-
- if ( is_wp_error( $decoded ) ) {
- return $decoded;
- }
-
- if ( ! property_exists( $decoded, 'result' ) ||
- ! is_string( $decoded->result[0] ) ) {
- return new WP_Error(
- 400,
- __( 'Unable to parse meta description from upstream API', 'wp-parsely' )
- );
- }
-
- return $decoded->result[0];
- }
-}
diff --git a/src/RemoteAPI/content-suggestions/class-suggest-headline-api.php b/src/RemoteAPI/content-suggestions/class-suggest-headline-api.php
deleted file mode 100644
index 346697663..000000000
--- a/src/RemoteAPI/content-suggestions/class-suggest-headline-api.php
+++ /dev/null
@@ -1,69 +0,0 @@
-|WP_Error The response from the remote API, or a WP_Error
- * object if the response is an error.
- */
- public function get_titles(
- string $content,
- int $limit,
- string $persona = 'journalist',
- string $tone = 'neutral'
- ) {
- $body = array(
- 'output_config' => array(
- 'persona' => $persona,
- 'style' => $tone,
- 'max_items' => $limit,
- ),
- 'text' => wp_strip_all_tags( $content ),
- );
-
- $decoded = $this->post_request( array(), $body );
-
- if ( is_wp_error( $decoded ) ) {
- return $decoded;
- }
-
- if ( ! property_exists( $decoded, 'result' ) || ! is_array( $decoded->result ) ) {
- return new WP_Error(
- 400,
- __( 'Unable to parse titles from upstream API', 'wp-parsely' )
- );
- }
-
- return $decoded->result;
- }
-}
diff --git a/src/RemoteAPI/content-suggestions/class-suggest-linked-reference-api.php b/src/RemoteAPI/content-suggestions/class-suggest-linked-reference-api.php
deleted file mode 100644
index 073d401e2..000000000
--- a/src/RemoteAPI/content-suggestions/class-suggest-linked-reference-api.php
+++ /dev/null
@@ -1,88 +0,0 @@
- array(
- 'max_link_words' => $max_link_words,
- 'max_items' => $max_links,
- ),
- 'text' => wp_strip_all_tags( $content ),
- );
-
- if ( count( $url_exclusion_list ) > 0 ) {
- $body['url_exclusion_list'] = $url_exclusion_list;
- }
-
- $decoded = $this->post_request( array(), $body );
-
- if ( is_wp_error( $decoded ) ) {
- return $decoded;
- }
-
- if ( ! property_exists( $decoded, 'result' ) ||
- ! is_array( $decoded->result ) ) {
- return new WP_Error(
- 400,
- __( 'Unable to parse suggested links from upstream API', 'wp-parsely' )
- );
- }
-
- // Convert the links to Smart_Link objects.
- $links = array();
- foreach ( $decoded->result as $link ) {
- $link = apply_filters( 'wp_parsely_suggest_linked_reference_link', $link );
- $link_obj = new Smart_Link(
- esc_url( $link->canonical_url ),
- esc_attr( $link->title ),
- wp_kses_post( $link->text ),
- $link->offset
- );
- $links[] = $link_obj;
- }
-
- return $links;
- }
-}
diff --git a/src/RemoteAPI/interface-cache.php b/src/RemoteAPI/interface-cache.php
deleted file mode 100644
index 9b20fa5fc..000000000
--- a/src/RemoteAPI/interface-cache.php
+++ /dev/null
@@ -1,46 +0,0 @@
- $query The query arguments to send to the remote API.
- * @param bool $associative (optional) When TRUE, returned objects will be converted into
- * associative arrays.
- * @return array|object|WP_Error
- */
- public function get_items( array $query, bool $associative = false );
-
- /**
- * Returns whether the endpoint is available for access by the current
- * user.
- *
- * @since 3.14.0 Renamed from `is_user_allowed_to_make_api_call()`.
- * @since 3.16.0 Added the `$request` parameter.
- *
- * @param WP_REST_Request|null $request The request object.
- * @return bool
- */
- public function is_available_to_current_user( $request = null ): bool;
-}
diff --git a/src/UI/class-recommended-widget.php b/src/UI/class-recommended-widget.php
index b72bd2e5e..25f8fca2e 100644
--- a/src/UI/class-recommended-widget.php
+++ b/src/UI/class-recommended-widget.php
@@ -92,7 +92,7 @@ public function __construct( Parsely $parsely ) {
* @return string API URL.
*/
private function get_api_url( string $site_id, ?int $published_within, ?string $sort, int $return_limit ): string {
- $related_api_endpoint = Parsely::PUBLIC_API_BASE_URL . '/related';
+ $related_api_endpoint = $this->parsely->get_content_api()->get_endpoint( '/related' );
$query_args = array(
'apikey' => $site_id,
@@ -104,7 +104,7 @@ private function get_api_url( string $site_id, ?int $published_within, ?string $
$query_args['pub_date_start'] = $published_within . 'd';
}
- return add_query_arg( $query_args, $related_api_endpoint );
+ return $related_api_endpoint->get_endpoint_url( $query_args );
}
/**
diff --git a/src/UI/class-settings-page.php b/src/UI/class-settings-page.php
index 5e756525b..a901ace8d 100644
--- a/src/UI/class-settings-page.php
+++ b/src/UI/class-settings-page.php
@@ -1198,7 +1198,7 @@ public function validate_options( $input ) {
* @param ParselySettingOptions $input Options from the settings page.
* @return ParselySettingOptions Validated inputs.
*/
- private function validate_basic_section( $input ) {
+ private function validate_basic_section( $input ): array {
$are_credentials_managed = $this->parsely->are_credentials_managed;
$options = $this->parsely->get_options();
@@ -1217,7 +1217,10 @@ private function validate_basic_section( $input ) {
$valid_credentials = true;
}
- if ( is_wp_error( $valid_credentials ) && Validator::INVALID_API_CREDENTIALS === $valid_credentials->get_error_code() ) {
+ if (
+ ( is_wp_error( $valid_credentials ) && Validator::INVALID_API_CREDENTIALS === $valid_credentials->get_error_code() ) ||
+ ( is_bool( $valid_credentials ) && ! $valid_credentials )
+ ) {
add_settings_error(
Parsely::OPTIONS_KEY,
'api_secret',
diff --git a/src/blocks/recommendations/components/parsely-recommendations-fetcher.tsx b/src/blocks/recommendations/components/parsely-recommendations-fetcher.tsx
index 37fb2ef01..1f1169ff3 100644
--- a/src/blocks/recommendations/components/parsely-recommendations-fetcher.tsx
+++ b/src/blocks/recommendations/components/parsely-recommendations-fetcher.tsx
@@ -44,7 +44,7 @@ export const ParselyRecommendationsFetcher = (
try {
response = await apiFetch>( {
- path: addQueryArgs( '/wp-parsely/v1/related', { query } ),
+ path: addQueryArgs( '/wp-parsely/v2/stats/related', query ),
} );
} catch ( wpError ) {
error = wpError;
diff --git a/src/blocks/recommendations/components/parsely-recommendations.tsx b/src/blocks/recommendations/components/parsely-recommendations.tsx
index 4e9ae75c8..fa2f117ea 100644
--- a/src/blocks/recommendations/components/parsely-recommendations.tsx
+++ b/src/blocks/recommendations/components/parsely-recommendations.tsx
@@ -41,7 +41,7 @@ export function ParselyRecommendations( {
if ( httpError ) {
message = __( 'The Parse.ly Recommendations API is not accessible. You may be offline.', 'wp-parsely' );
- } else if ( message.includes( 'Error: {"code":403,"message":"Forbidden","data":null}' ) ) {
+ } else if ( error.message === 'Forbidden' && error.data?.status === 403 && error.code === 403 ) {
message = __( 'Access denied. Please verify that your Site ID is valid.', 'wp-parsely' );
} else if ( message.startsWith( 'Error: {"code":"parsely_site_id_not_set"' ) ) {
message = __( 'To use this Block, a Parse.ly Site ID must be set in the plugin\'s options', 'wp-parsely' );
diff --git a/src/blocks/recommendations/recommendations-store.tsx b/src/blocks/recommendations/recommendations-store.tsx
index fff294e4d..65d3008a9 100644
--- a/src/blocks/recommendations/recommendations-store.tsx
+++ b/src/blocks/recommendations/recommendations-store.tsx
@@ -9,12 +9,14 @@ import { createContext, useContext, useMemo, useReducer } from '@wordpress/eleme
import { RecommendationsAction } from './constants';
import { Recommendation } from './models/Recommendation';
+type RecommendationError = Error & { code: string|number, data?: { status?: string|number } };
+
interface RecommendationState {
isLoaded: boolean;
recommendations: Recommendation[];
uuid: string | null;
clientId: string | null;
- error: Error | null;
+ error: RecommendationError | null;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
diff --git a/src/class-parsely.php b/src/class-parsely.php
index e7c673fed..a7a9bee5e 100644
--- a/src/class-parsely.php
+++ b/src/class-parsely.php
@@ -10,6 +10,9 @@
namespace Parsely;
+use Parsely\REST_API\REST_API_Controller;
+use Parsely\Services\Content_API\Content_API_Service;
+use Parsely\Services\Suggestions_API\Suggestions_API_Service;
use Parsely\UI\Metadata_Renderer;
use Parsely\UI\Settings_Page;
use Parsely\Utils\Utils;
@@ -58,12 +61,12 @@
* }
*
* @phpstan-type WP_HTTP_Request_Args array{
- * method: string,
- * timeout: float,
- * blocking: bool,
- * headers: array,
- * body: string,
- * data_format: string,
+ * method?: string,
+ * timeout?: float,
+ * blocking?: bool,
+ * headers?: array,
+ * body?: string,
+ * data_format?: string,
* }
*
* @phpstan-import-type Metadata_Attributes from Metadata
@@ -72,13 +75,32 @@ class Parsely {
/**
* Declare our constants
*/
- public const VERSION = PARSELY_VERSION;
- public const MENU_SLUG = 'parsely'; // The page param passed to options-general.php.
- public const OPTIONS_KEY = 'parsely'; // The key used to store options in the WP database.
- public const CAPABILITY = 'manage_options'; // The capability required to administer settings.
- public const DASHBOARD_BASE_URL = 'https://dash.parsely.com';
- public const PUBLIC_API_BASE_URL = 'https://api.parsely.com/v2';
- public const PUBLIC_SUGGESTIONS_API_BASE_URL = 'https://content-suggestions-api.parsely.net/prod';
+ public const VERSION = PARSELY_VERSION;
+ public const MENU_SLUG = 'parsely'; // The page param passed to options-general.php.
+ public const OPTIONS_KEY = 'parsely'; // The key used to store options in the WP database.
+ public const CAPABILITY = 'manage_options'; // The capability required to administer settings.
+ public const DASHBOARD_BASE_URL = 'https://dash.parsely.com';
+
+ /**
+ * The Content API service.
+ *
+ * @var ?Content_API_Service $content_api_service
+ */
+ private $content_api_service;
+
+ /**
+ * The Suggestions API service.
+ *
+ * @var ?Suggestions_API_Service $suggestions_api_service
+ */
+ private $suggestions_api_service;
+
+ /**
+ * The Parse.ly internal REST API controller.
+ *
+ * @var REST_API_Controller|null $rest_api_controller
+ */
+ private $rest_api_controller;
/**
* Declare some class properties
@@ -237,6 +259,57 @@ public function run(): void {
add_action( 'save_post', array( $this, 'update_metadata_endpoint' ) );
}
+ /**
+ * Returns the Content API service.
+ *
+ * This method returns the Content API service, which is used to interact with the Parse.ly Content API.
+ *
+ * @since 3.17.0
+ *
+ * @return Content_API_Service
+ */
+ public function get_content_api(): Content_API_Service {
+ if ( ! isset( $this->content_api_service ) ) {
+ $this->content_api_service = new Content_API_Service( $this );
+ }
+
+ return $this->content_api_service;
+ }
+
+ /**
+ * Returns the Suggestions API service.
+ *
+ * This method returns the Suggestions API service, which is used to interact with the Parse.ly Suggestions API.
+ *
+ * @since 3.17.0
+ *
+ * @return Suggestions_API_Service
+ */
+ public function get_suggestions_api(): Suggestions_API_Service {
+ if ( ! isset( $this->suggestions_api_service ) ) {
+ $this->suggestions_api_service = new Suggestions_API_Service( $this );
+ }
+
+ return $this->suggestions_api_service;
+ }
+
+ /**
+ * Gets the REST API controller.
+ *
+ * If the controller is not set, a new instance is created.
+ *
+ * @since 3.17.0
+ *
+ * @return REST_API_Controller
+ */
+ public function get_rest_api_controller(): REST_API_Controller {
+ if ( ! isset( $this->rest_api_controller ) ) {
+ $this->rest_api_controller = new REST_API_Controller( $this );
+ }
+
+ return $this->rest_api_controller;
+ }
+
/**
* Gets the full URL of the JavaScript tracker file for the site. If an API
* key is not set, return an empty string.
@@ -384,7 +457,8 @@ public function update_metadata_endpoint( int $post_id ): void {
'tags' => $metadata['keywords'] ?? '',
);
- $parsely_api_endpoint = self::PUBLIC_API_BASE_URL . '/metadata/posts';
+ $parsely_api_base_url = Content_API_Service::get_base_url();
+ $parsely_api_endpoint = $parsely_api_base_url . '/metadata/posts';
$parsely_metadata_secret = $parsely_options['metadata_secret'];
$headers = array( 'Content-Type' => 'application/json' );
@@ -938,8 +1012,8 @@ private function sanitize_managed_option( string $option_id, $value ) {
private function allow_parsely_remote_requests(): void {
$allowed_urls = array(
self::DASHBOARD_BASE_URL,
- self::PUBLIC_API_BASE_URL,
- self::PUBLIC_SUGGESTIONS_API_BASE_URL,
+ Content_API_Service::get_base_url(),
+ Suggestions_API_Service::get_base_url(),
);
add_filter(
diff --git a/src/class-validator.php b/src/class-validator.php
index 26c915c85..2f0bfe0da 100644
--- a/src/class-validator.php
+++ b/src/class-validator.php
@@ -44,7 +44,7 @@ public static function validate_metadata_secret( string $metadata_secret ): bool
* @param Parsely $parsely The Parsely instance.
* @param string $site_id The Site ID to be validated.
* @param string $api_secret The API Secret to be validated.
- * @return true|WP_Error True if the API Credentials are valid, WP_Error otherwise.
+ * @return bool|WP_Error True if the API Credentials are valid, WP_Error otherwise.
*/
public static function validate_api_credentials( Parsely $parsely, string $site_id, string $api_secret ) {
// If the API secret is empty, the validation endpoint will always fail.
@@ -54,21 +54,16 @@ public static function validate_api_credentials( Parsely $parsely, string $site_
return true;
}
- $query_args = array(
- 'apikey' => $site_id,
- 'secret' => $api_secret,
- );
+ $content_api = $parsely->get_content_api();
+ $is_valid = $content_api->validate_credentials( $site_id, $api_secret );
- $validate_api = new RemoteAPI\Validate_API( $parsely );
- $request = $validate_api->get_items( $query_args );
-
- if ( is_wp_error( $request ) ) {
+ if ( is_wp_error( $is_valid ) ) {
return new WP_Error(
self::INVALID_API_CREDENTIALS,
__( 'Invalid API Credentials', 'wp-parsely' )
);
}
- return true;
+ return $is_valid;
}
}
diff --git a/src/content-helper/common/class-content-helper-feature.php b/src/content-helper/common/class-content-helper-feature.php
index b0edb6505..83c6f04b7 100644
--- a/src/content-helper/common/class-content-helper-feature.php
+++ b/src/content-helper/common/class-content-helper-feature.php
@@ -166,15 +166,17 @@ protected function inject_inline_scripts(
$settings = rest_do_request(
new WP_REST_Request(
'GET',
- '/wp-parsely/v1' . $settings_route
+ '/wp-parsely/v2/settings/' . $settings_route
)
)->get_data();
}
- if ( ! is_string( $settings ) ) {
- $settings = '';
+ if ( ! is_array( $settings ) ) {
+ $settings = array();
}
+ $settings = wp_json_encode( $settings );
+
wp_add_inline_script(
static::get_script_id(),
"window.wpParselyContentHelperSettings = '$settings';",
diff --git a/src/content-helper/common/settings/provider.tsx b/src/content-helper/common/settings/provider.tsx
index ef4ea23f7..5e696b4bc 100644
--- a/src/content-helper/common/settings/provider.tsx
+++ b/src/content-helper/common/settings/provider.tsx
@@ -78,6 +78,7 @@ type ReactDeps = React.DependencyList | undefined;
*
* @since 3.13.0
* @since 3.14.0 Moved from `content-helper/common/hooks/useSaveSettings.ts`.
+ * @since 3.17.0 Updated to the new API endpoints.
*
* @param {string} endpoint The settings endpoint to send the data to.
* @param {Settings} data The data to send.
@@ -96,7 +97,7 @@ const useSaveSettings = (
}
apiFetch( {
- path: '/wp-parsely/v1/user-meta/content-helper/' + endpoint,
+ path: '/wp-parsely/v2/settings/' + endpoint,
method: 'PUT',
data,
} );
diff --git a/src/content-helper/dashboard-widget/class-dashboard-widget.php b/src/content-helper/dashboard-widget/class-dashboard-widget.php
index a166adff6..cc72d0e79 100644
--- a/src/content-helper/dashboard-widget/class-dashboard-widget.php
+++ b/src/content-helper/dashboard-widget/class-dashboard-widget.php
@@ -10,11 +10,9 @@
namespace Parsely\Content_Helper;
-use Parsely\Endpoints\User_Meta\Dashboard_Widget_Settings_Endpoint;
use Parsely\Parsely;
-use Parsely\RemoteAPI\Analytics_Posts_API;
-
use Parsely\Utils\Utils;
+use Parsely\REST_API\Settings\Endpoint_Dashboard_Widget_Settings;
use const Parsely\PARSELY_FILE;
@@ -89,12 +87,13 @@ public function run(): void {
* @return bool Whether the Dashboard Widget can be enabled.
*/
public function can_enable_widget(): bool {
- $screen = get_current_screen();
- $posts_api = new Analytics_Posts_API( $GLOBALS['parsely'] );
+ $screen = get_current_screen();
return $this->can_enable_feature(
null !== $screen && 'dashboard' === $screen->id,
- $posts_api->is_available_to_current_user()
+ $this->parsely->get_rest_api_controller()->is_available_to_current_user(
+ '/stats/posts'
+ )
);
}
@@ -136,7 +135,7 @@ public function enqueue_assets(): void {
true
);
- $this->inject_inline_scripts( Dashboard_Widget_Settings_Endpoint::get_route() );
+ $this->inject_inline_scripts( Endpoint_Dashboard_Widget_Settings::get_endpoint_name() );
wp_enqueue_style(
static::get_style_id(),
diff --git a/src/content-helper/dashboard-widget/dashboard-widget.tsx b/src/content-helper/dashboard-widget/dashboard-widget.tsx
index 09959ee2f..b67f4acf6 100644
--- a/src/content-helper/dashboard-widget/dashboard-widget.tsx
+++ b/src/content-helper/dashboard-widget/dashboard-widget.tsx
@@ -56,7 +56,7 @@ window.addEventListener(
if ( null !== container ) {
const component =
diff --git a/src/content-helper/dashboard-widget/provider.ts b/src/content-helper/dashboard-widget/provider.ts
index 23bc4673e..0338e490a 100644
--- a/src/content-helper/dashboard-widget/provider.ts
+++ b/src/content-helper/dashboard-widget/provider.ts
@@ -83,7 +83,7 @@ export class DashboardWidgetProvider extends BaseProvider {
settings: TopPostsSettings, page: number
): Promise {
const response = this.fetch( {
- path: addQueryArgs( '/wp-parsely/v1/stats/posts/', {
+ path: addQueryArgs( '/wp-parsely/v2/stats/posts/', {
limit: TOP_POSTS_DEFAULT_LIMIT,
...getApiPeriodParams( settings.Period ),
sort: settings.Metric,
diff --git a/src/content-helper/editor-sidebar/class-editor-sidebar.php b/src/content-helper/editor-sidebar/class-editor-sidebar.php
index 9d0accb25..7b2256aa3 100644
--- a/src/content-helper/editor-sidebar/class-editor-sidebar.php
+++ b/src/content-helper/editor-sidebar/class-editor-sidebar.php
@@ -12,20 +12,14 @@
use Parsely\Content_Helper\Editor_Sidebar\Smart_Linking;
use Parsely\Dashboard_Link;
-use Parsely\Endpoints\User_Meta\Editor_Sidebar_Settings_Endpoint;
use Parsely\Parsely;
-use Parsely\Content_Helper\Content_Helper_Feature;
+use Parsely\REST_API\Settings\Endpoint_Editor_Sidebar_Settings;
use Parsely\Utils\Utils;
use WP_Post;
use const Parsely\PARSELY_FILE;
-/**
- * Features requires for the PCH Editor Sidebar.
- */
-require_once __DIR__ . '/smart-linking/class-smart-linking.php';
-
/**
* Class that generates and manages the PCH Editor Sidebar.
*
@@ -163,7 +157,7 @@ public function run(): void {
true
);
- $this->inject_inline_scripts( Editor_Sidebar_Settings_Endpoint::get_route() );
+ $this->inject_inline_scripts( Endpoint_Editor_Sidebar_Settings::get_endpoint_name() );
// Inject inline variables for the editor sidebar, without UTM parameters.
$parsely_post_url = $this->get_parsely_post_url( null, false );
diff --git a/src/content-helper/editor-sidebar/editor-sidebar.tsx b/src/content-helper/editor-sidebar/editor-sidebar.tsx
index f3e36d922..2c57a4988 100644
--- a/src/content-helper/editor-sidebar/editor-sidebar.tsx
+++ b/src/content-helper/editor-sidebar/editor-sidebar.tsx
@@ -228,7 +228,7 @@ const ContentHelperEditorSidebar = (): React.JSX.Element => {
title={ __( 'Parse.ly', 'wp-parsely' ) }
>
@@ -279,7 +279,7 @@ registerPlugin( BLOCK_PLUGIN_ID, {
icon: LeafIcon,
render: () => (
diff --git a/src/content-helper/editor-sidebar/performance-stats/provider.ts b/src/content-helper/editor-sidebar/performance-stats/provider.ts
index 5bc524cd6..df8293aca 100644
--- a/src/content-helper/editor-sidebar/performance-stats/provider.ts
+++ b/src/content-helper/editor-sidebar/performance-stats/provider.ts
@@ -68,13 +68,13 @@ export class PerformanceStatsProvider extends BaseProvider {
);
}
- // Get post URL.
- const postUrl = editor.getPermalink();
+ // Get post ID.
+ const postID = editor.getCurrentPostId();
- if ( null === postUrl ) {
+ if ( null === postID ) {
return Promise.reject(
new ContentHelperError( __(
- "The post's URL returned null.",
+ "The post's ID returned null.",
'wp-parsely' ), ContentHelperErrorCode.PostIsNotPublished
)
);
@@ -84,10 +84,10 @@ export class PerformanceStatsProvider extends BaseProvider {
let performanceData, referrerData;
try {
performanceData = await this.fetchPerformanceDataFromWpEndpoint(
- period, postUrl
+ period, postID
);
referrerData = await this.fetchReferrerDataFromWpEndpoint(
- period, postUrl, performanceData.views
+ period, postID, performanceData.views
);
} catch ( contentHelperError ) {
return Promise.reject( contentHelperError );
@@ -100,20 +100,19 @@ export class PerformanceStatsProvider extends BaseProvider {
* Fetches the performance data for the current post from the WordPress REST
* API.
*
- * @param {Period} period The period for which to fetch data.
- * @param {string} postUrl
+ * @param {Period} period The period for which to fetch data.
+ * @param {number} postId The post's ID.
*
* @return {Promise } The current post's details.
*/
private async fetchPerformanceDataFromWpEndpoint(
- period: Period, postUrl: string
+ period: Period, postId: number
): Promise {
const response = await this.fetch( {
path: addQueryArgs(
- '/wp-parsely/v1/stats/post/detail', {
+ `/wp-parsely/v2/stats/post/${ postId }/details`, {
...getApiPeriodParams( period ),
itm_source: this.itmSource,
- url: postUrl,
} ),
} );
@@ -122,8 +121,8 @@ export class PerformanceStatsProvider extends BaseProvider {
return Promise.reject( new ContentHelperError(
sprintf(
/* translators: URL of the published post */
- __( 'The post %s has 0 views, or the Parse.ly API returned no data.',
- 'wp-parsely' ), postUrl
+ __( 'The post %d has 0 views, or the Parse.ly API returned no data.',
+ 'wp-parsely' ), postId
), ContentHelperErrorCode.ParselyApiReturnedNoData, ''
) );
}
@@ -133,8 +132,8 @@ export class PerformanceStatsProvider extends BaseProvider {
return Promise.reject( new ContentHelperError(
sprintf(
/* translators: URL of the published post */
- __( 'Multiple results were returned for the post %s by the Parse.ly API.',
- 'wp-parsely' ), postUrl
+ __( 'Multiple results were returned for the post %d by the Parse.ly API.',
+ 'wp-parsely' ), postId
), ContentHelperErrorCode.ParselyApiReturnedTooManyResults
) );
}
@@ -145,22 +144,21 @@ export class PerformanceStatsProvider extends BaseProvider {
/**
* Fetches referrer data for the current post from the WordPress REST API.
*
- * @param {Period} period The period for which to fetch data.
- * @param {string} postUrl The post's URL.
- * @param {string} totalViews Total post views (including direct views).
+ * @param {Period} period The period for which to fetch data.
+ * @param {string|number} postId The post's ID.
+ * @param {string} totalViews Total post views (including direct views).
*
* @return {Promise} The post's referrer data.
*/
private async fetchReferrerDataFromWpEndpoint(
- period: Period, postUrl: string, totalViews: string
+ period: Period, postId: string|number, totalViews: string
): Promise {
const response = await this.fetch( {
path: addQueryArgs(
- '/wp-parsely/v1/referrers/post/detail', {
+ `/wp-parsely/v2/stats/post/${ postId }/referrers`, {
...getApiPeriodParams( period ),
itm_source: this.itmSource,
total_views: totalViews, // Needed to calculate direct views.
- url: postUrl,
} ),
} );
diff --git a/src/content-helper/editor-sidebar/related-posts/provider.ts b/src/content-helper/editor-sidebar/related-posts/provider.ts
index 6345f4f19..6051c4ca8 100644
--- a/src/content-helper/editor-sidebar/related-posts/provider.ts
+++ b/src/content-helper/editor-sidebar/related-posts/provider.ts
@@ -146,7 +146,7 @@ export class RelatedPostsProvider extends BaseProvider {
*/
private async fetchRelatedPostsFromWpEndpoint( query: RelatedPostsApiQuery ): Promise {
const response = this.fetch( {
- path: addQueryArgs( '/wp-parsely/v1/stats/posts', {
+ path: addQueryArgs( '/wp-parsely/v2/stats/posts', {
...query.query,
itm_source: 'wp-parsely-content-helper',
} ),
diff --git a/src/content-helper/editor-sidebar/smart-linking/provider.ts b/src/content-helper/editor-sidebar/smart-linking/provider.ts
index ce30569f5..4e60e8edf 100644
--- a/src/content-helper/editor-sidebar/smart-linking/provider.ts
+++ b/src/content-helper/editor-sidebar/smart-linking/provider.ts
@@ -140,7 +140,7 @@ export class SmartLinkingProvider extends BaseProvider {
): Promise {
const response = await this.fetch( {
method: 'POST',
- path: addQueryArgs( '/wp-parsely/v1/content-suggestions/suggest-linked-reference', {
+ path: addQueryArgs( '/wp-parsely/v2/content-helper/smart-linking/generate', {
max_links: maxLinksPerPost,
} ),
data: {
@@ -165,7 +165,7 @@ export class SmartLinkingProvider extends BaseProvider {
public async addSmartLink( postID: number, linkSuggestion: SmartLink ): Promise {
return await this.fetch( {
method: 'POST',
- path: `/wp-parsely/v1/smart-linking/${ postID }/add`,
+ path: `/wp-parsely/v2/content-helper/smart-linking/${ postID }/add`,
data: {
link: linkSuggestion,
},
@@ -195,7 +195,7 @@ export class SmartLinkingProvider extends BaseProvider {
return await this.fetch( {
method: 'POST',
- path: `/wp-parsely/v1/smart-linking/${ postID }/add-multiple`,
+ path: `/wp-parsely/v2/content-helper/smart-linking/${ postID }/add-multiple`,
data: {
links: linkSuggestions,
},
@@ -230,7 +230,7 @@ export class SmartLinkingProvider extends BaseProvider {
return await this.fetch( {
method: 'POST',
- path: `/wp-parsely/v1/smart-linking/${ postID }/set`,
+ path: `/wp-parsely/v2/content-helper/smart-linking/${ postID }/set`,
data: {
links: appliedSmartLinks,
},
@@ -249,7 +249,7 @@ export class SmartLinkingProvider extends BaseProvider {
public async getSmartLinks( postID: number ): Promise {
return await this.fetch( {
method: 'GET',
- path: `/wp-parsely/v1/smart-linking/${ postID }/get`,
+ path: `/wp-parsely/v2/content-helper/smart-linking/${ postID }/get`,
} );
}
@@ -265,7 +265,7 @@ export class SmartLinkingProvider extends BaseProvider {
public async getPostTypeByURL( url: string ): Promise {
return await this.fetch( {
method: 'POST',
- path: '/wp-parsely/v1/smart-linking/url-to-post-type',
+ path: '/wp-parsely/v2/content-helper/smart-linking/url-to-post-type',
data: {
url,
},
diff --git a/src/content-helper/editor-sidebar/smart-linking/smart-linking.tsx b/src/content-helper/editor-sidebar/smart-linking/smart-linking.tsx
index bdec814f6..cb40a3558 100644
--- a/src/content-helper/editor-sidebar/smart-linking/smart-linking.tsx
+++ b/src/content-helper/editor-sidebar/smart-linking/smart-linking.tsx
@@ -53,7 +53,7 @@ const withSettingsProvider = createHigherOrderComponent( ( BlockEdit ) => {
return (
diff --git a/src/content-helper/editor-sidebar/title-suggestions/provider.ts b/src/content-helper/editor-sidebar/title-suggestions/provider.ts
index c6095bf5e..9b85f4767 100644
--- a/src/content-helper/editor-sidebar/title-suggestions/provider.ts
+++ b/src/content-helper/editor-sidebar/title-suggestions/provider.ts
@@ -52,13 +52,13 @@ export class TitleSuggestionsProvider extends BaseProvider {
public async generateTitles( content: string, limit: number = 3, tone: ToneProp, persona: PersonaProp ): Promise {
const response = this.fetch( {
method: 'POST',
- path: addQueryArgs( '/wp-parsely/v1/content-suggestions/suggest-headline', {
+ path: addQueryArgs( '/wp-parsely/v2/content-helper/title-suggestions/generate', {
limit,
tone: getToneLabel( tone ),
persona: getPersonaLabel( persona ),
} ),
data: {
- content,
+ text: content,
},
} );
diff --git a/src/content-helper/excerpt-generator/provider.ts b/src/content-helper/excerpt-generator/provider.ts
index 6fb4dcaf0..2a5ebaf83 100644
--- a/src/content-helper/excerpt-generator/provider.ts
+++ b/src/content-helper/excerpt-generator/provider.ts
@@ -51,11 +51,11 @@ export class ExcerptGeneratorProvider extends BaseProvider {
return await this.fetch( {
method: 'POST',
- path: addQueryArgs( '/wp-parsely/v1/content-suggestions/suggest-brief', {
+ path: addQueryArgs( '/wp-parsely/v2/content-helper/excerpt-generator/generate', {
title,
} ),
data: {
- content,
+ text: content,
},
} );
}
diff --git a/src/content-helper/post-list-stats/class-post-list-stats.php b/src/content-helper/post-list-stats/class-post-list-stats.php
index 42b2452e8..9ea12d9a4 100644
--- a/src/content-helper/post-list-stats/class-post-list-stats.php
+++ b/src/content-helper/post-list-stats/class-post-list-stats.php
@@ -11,10 +11,9 @@
namespace Parsely\Content_Helper;
use DateTime;
-use Parsely\Content_Helper\Content_Helper_Feature;
use Parsely\Parsely;
-use Parsely\RemoteAPI\Base_Endpoint_Remote;
-use Parsely\RemoteAPI\Analytics_Posts_API;
+use Parsely\Services\Content_API\Content_API_Service;
+use Parsely\Services\Content_API\Endpoints\Endpoint_Analytics_Posts;
use Parsely\Utils\Utils;
use WP_Screen;
@@ -26,9 +25,14 @@
* @since 3.7.0
* @since 3.9.0 Renamed FQCN from `Parsely\UI\Admin_Columns_Parsely_Stats` to `Parsely\Content_Helper\Post_List_Stats`.
*
- * @phpstan-import-type Analytics_Post_API_Params from Analytics_Posts_API
- * @phpstan-import-type Analytics_Post from Analytics_Posts_API
- * @phpstan-import-type Remote_API_Error from Base_Endpoint_Remote
+ * @phpstan-import-type Analytics_Posts_API_Params from Endpoint_Analytics_Posts
+ * @phpstan-import-type Analytics_Post from Endpoint_Analytics_Posts
+ *
+ * @phpstan-type Remote_API_Error array{
+ * code: int,
+ * message: string,
+ * htmlMessage: string,
+ * }
*
* @phpstan-type Parsely_Post_Stats array{
* page_views: string,
@@ -42,12 +46,13 @@
* }
*/
class Post_List_Stats extends Content_Helper_Feature {
+
/**
- * Instance of Parsely Analytics Posts API.
+ * Instance of Content API Service.
*
- * @var Analytics_Posts_API
+ * @var Content_API_Service
*/
- private $analytics_api;
+ private $content_api;
/**
* Internal Variable.
@@ -72,7 +77,8 @@ class Post_List_Stats extends Content_Helper_Feature {
* @param Parsely $parsely Instance of Parsely class.
*/
public function __construct( Parsely $parsely ) {
- $this->parsely = $parsely;
+ $this->parsely = $parsely;
+ $this->content_api = $parsely->get_content_api();
}
/**
@@ -114,12 +120,10 @@ public static function get_style_id(): string {
* @since 3.7.0
*/
public function run(): void {
- $this->analytics_api = new Analytics_Posts_API( $this->parsely );
-
if ( ! $this->can_enable_feature(
$this->parsely->site_id_is_set(),
$this->parsely->api_secret_is_set(),
- $this->analytics_api->is_available_to_current_user()
+ $this->parsely->get_rest_api_controller()->is_available_to_current_user( '/stats/posts' )
) ) {
return;
}
@@ -225,7 +229,7 @@ public function enqueue_parsely_stats_script_with_data(): void {
return; // Avoid calling the API if column is hidden.
}
- $parsely_stats_response = $this->get_parsely_stats_response( $this->analytics_api );
+ $parsely_stats_response = $this->get_parsely_stats_response();
if ( null === $parsely_stats_response ) {
return;
@@ -267,10 +271,9 @@ public function is_parsely_stats_column_hidden(): bool {
*
* @since 3.7.0
*
- * @param Analytics_Posts_API $analytics_api Instance of Analytics_Posts_API.
* @return Parsely_Posts_Stats_Response|null
*/
- public function get_parsely_stats_response( $analytics_api ) {
+ public function get_parsely_stats_response(): ?array {
if ( ! $this->is_tracked_as_post_type() ) {
return null;
}
@@ -296,12 +299,12 @@ public function get_parsely_stats_response( $analytics_api ) {
return null;
}
- $response = $analytics_api->get_posts_analytics(
+ $response = $this->content_api->get_posts(
array(
- 'period_start' => Analytics_Posts_API::ANALYTICS_API_DAYS_LIMIT . 'd',
+ 'period_start' => Endpoint_Analytics_Posts::MAX_PERIOD,
'pub_date_start' => $date_params['pub_date_start'] ?? '',
'pub_date_end' => $date_params['pub_date_end'] ?? '',
- 'limit' => Analytics_Posts_API::MAX_RECORDS_LIMIT,
+ 'limit' => Endpoint_Analytics_Posts::MAX_LIMIT,
'sort' => 'avg_engaged', // Note: API sends different stats on different sort options.
)
);
@@ -322,20 +325,14 @@ public function get_parsely_stats_response( $analytics_api ) {
);
}
- if ( null === $response ) {
- return array(
- 'data' => array(),
- 'error' => null,
- );
- }
-
/**
- * Variable.
- *
- * @var array
+ * @var array $parsely_stats_map
*/
$parsely_stats_map = array();
+ /**
+ * @var Analytics_Post $post_analytics
+ */
foreach ( $response as $post_analytics ) {
$key = $this->get_unique_stats_key_from_analytics( $post_analytics );
@@ -349,9 +346,7 @@ public function get_parsely_stats_response( $analytics_api ) {
$engaged_seconds = isset( $metrics['avg_engaged'] ) ? round( $metrics['avg_engaged'] * 60, 2 ) : 0;
/**
- * Variable.
- *
- * @var Parsely_Post_Stats
+ * @var Parsely_Post_Stats $stats
*/
$stats = array(
'page_views' => Utils::get_formatted_number( (string) $views ) . ' ' . _n( 'page view', 'page views', $views, 'wp-parsely' ),
@@ -375,7 +370,7 @@ public function get_parsely_stats_response( $analytics_api ) {
*
* @since 3.7.0
*
- * @return Analytics_Post_API_Params|null
+ * @return Analytics_Posts_API_Params|null
*/
private function get_publish_date_params_for_analytics_api() {
$published_times = $this->utc_published_times;
diff --git a/src/rest-api/class-base-api-controller.php b/src/rest-api/class-base-api-controller.php
new file mode 100644
index 000000000..b0553ff21
--- /dev/null
+++ b/src/rest-api/class-base-api-controller.php
@@ -0,0 +1,204 @@
+
+ */
+ private $endpoints;
+
+ /**
+ * The Parsely instance.
+ *
+ * @since 3.17.0
+ *
+ * @var Parsely
+ */
+ private $parsely;
+
+ /**
+ * Constructor.
+ *
+ * @since 3.17.0
+ *
+ * @param Parsely $parsely The Parsely instance.
+ */
+ public function __construct( Parsely $parsely ) {
+ $this->parsely = $parsely;
+ $this->endpoints = array();
+ }
+
+ /**
+ * Initializes the API controller.
+ *
+ * This method should be overridden by child classes and used to register
+ * endpoints.
+ *
+ * @since 3.17.0
+ *
+ * @return void
+ */
+ abstract protected function init(): void;
+
+ /**
+ * Gets the namespace for the API.
+ *
+ * This method should be overridden by child classes to define the namespace.
+ *
+ * @since 3.17.0
+ *
+ * @return string The namespace.
+ */
+ abstract protected function get_namespace(): string;
+
+ /**
+ * Gets the version for the API.
+ *
+ * This method can be overridden by child classes to define the version.
+ *
+ * @since 3.17.0
+ *
+ * @return string The version.
+ */
+ protected function get_version(): string {
+ return '';
+ }
+
+ /**
+ * Gets the route prefix, which acts as a namespace for the endpoints.
+ *
+ * This method can be overridden by child classes to define the route prefix.
+ *
+ * @since 3.17.0
+ *
+ * @return string The route prefix.
+ */
+ public static function get_route_prefix(): string {
+ return '';
+ }
+
+ /**
+ * Returns the full namespace for the API, including the version if defined.
+ *
+ * @since 3.17.0
+ *
+ * @return string
+ */
+ public function get_full_namespace(): string {
+ $namespace = $this->get_namespace();
+
+ if ( '' !== $this->get_version() ) {
+ $namespace .= '/' . $this->get_version();
+ }
+
+ return $namespace;
+ }
+
+ /**
+ * Gets the Parsely instance.
+ *
+ * @since 3.17.0
+ *
+ * @return Parsely The Parsely instance.
+ */
+ public function get_parsely(): Parsely {
+ return $this->parsely;
+ }
+
+ /**
+ * Gets the registered endpoints.
+ *
+ * @since 3.17.0
+ *
+ * @return Base_Endpoint[] The registered endpoints.
+ */
+ public function get_endpoints(): array {
+ return $this->endpoints;
+ }
+
+ /**
+ * Registers a single endpoint.
+ *
+ * @since 3.17.0
+ *
+ * @param Base_Endpoint $endpoint The endpoint to register.
+ */
+ protected function register_endpoint( Base_Endpoint $endpoint ): void {
+ $this->endpoints[ $endpoint->get_endpoint_slug() ] = $endpoint;
+ $endpoint->init();
+ }
+
+ /**
+ * Registers multiple endpoints.
+ *
+ * @since 3.17.0
+ *
+ * @param Base_Endpoint[] $endpoints The endpoints to register.
+ */
+ protected function register_endpoints( array $endpoints ): void {
+ foreach ( $endpoints as $endpoint ) {
+ $this->register_endpoint( $endpoint );
+ }
+ }
+
+ /**
+ * Prefixes a route with the route prefix.
+ *
+ * @since 3.17.0
+ *
+ * @param string $route The route to prefix.
+ * @return string The prefixed route.
+ */
+ public function prefix_route( string $route ): string {
+ if ( '' === static::get_route_prefix() ) {
+ return $route;
+ }
+
+ return static::get_route_prefix() . '/' . $route;
+ }
+
+ /**
+ * Returns a specific endpoint by name.
+ *
+ * @since 3.17.0
+ *
+ * @param string $endpoint The endpoint name/path.
+ * @return Base_Endpoint|null The endpoint object, or null if not found.
+ */
+ protected function get_endpoint( string $endpoint ): ?Base_Endpoint {
+ return $this->endpoints[ $endpoint ] ?? null;
+ }
+
+ /**
+ * Checks if a specific endpoint is available to the current user.
+ *
+ * @since 3.17.0
+ *
+ * @param string $endpoint The endpoint to check.
+ * @return bool True if the controller is available to the current user, false otherwise.
+ */
+ abstract public function is_available_to_current_user( string $endpoint ): bool;
+}
diff --git a/src/rest-api/class-base-endpoint.php b/src/rest-api/class-base-endpoint.php
new file mode 100644
index 000000000..ecea575a6
--- /dev/null
+++ b/src/rest-api/class-base-endpoint.php
@@ -0,0 +1,307 @@
+
+ */
+ protected $registered_routes = array();
+
+ /**
+ * Constructor.
+ *
+ * @since 3.17.0
+ *
+ * @param Base_API_Controller $controller The REST API controller.
+ */
+ public function __construct( Base_API_Controller $controller ) {
+ $this->api_controller = $controller;
+ $this->parsely = $controller->get_parsely();
+ }
+
+ /**
+ * Initializes the API endpoint, by registering the routes.
+ *
+ * Allows for the endpoint to be disabled via the
+ * `wp_parsely_api_{endpoint}_endpoint_enabled` filter.
+ *
+ * @since 3.17.0
+ */
+ public function init(): void {
+ /**
+ * Filter to enable/disable the endpoint.
+ *
+ * @return bool
+ */
+ $filter_name = 'wp_parsely_api_' .
+ Utils::convert_endpoint_to_filter_key( static::get_endpoint_name() ) .
+ '_endpoint_enabled';
+ if ( ! apply_filters( $filter_name, true ) ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
+ return;
+ }
+
+ // Register the routes.
+ add_action( 'rest_api_init', array( $this, 'register_routes' ) );
+ }
+
+ /**
+ * Returns the endpoint name.
+ *
+ * This method should be overridden by child classes and used to return the
+ * endpoint name.
+ *
+ * @since 3.17.0
+ *
+ * @return string
+ */
+ abstract public static function get_endpoint_name(): string;
+
+ /**
+ * Returns the default access capability for the endpoint.
+ *
+ * This method can be overridden by child classes to return a different
+ * default access capability.
+ *
+ * @since 3.17.0
+ *
+ * @return string
+ */
+ protected function get_default_access_capability(): string {
+ return 'publish_posts';
+ }
+
+ /**
+ * Registers the routes for the endpoint.
+ *
+ * This method should be overridden by child classes and used to register
+ * the routes for the endpoint.
+ *
+ * @since 3.17.0
+ */
+ abstract public function register_routes(): void;
+
+ /**
+ * Registers a REST route.
+ *
+ * @since 3.17.0
+ *
+ * @param string $route The route to register.
+ * @param string[] $methods Array with the allowed methods.
+ * @param callable $callback Callback function to call when the endpoint is hit.
+ * @param array $args The endpoint arguments definition.
+ */
+ public function register_rest_route( string $route, array $methods, callable $callback, array $args = array() ): void {
+ // Trim any possible slashes from the route.
+ $route = trim( $route, '/' );
+
+ // Store the route for later reference.
+ $this->registered_routes[] = $route;
+
+ // Create the full route for the endpoint.
+ $route = static::get_endpoint_name() . '/' . $route;
+
+ // Register the route.
+ register_rest_route(
+ $this->api_controller->get_full_namespace(),
+ $this->api_controller->prefix_route( $route ),
+ array(
+ array(
+ 'methods' => $methods,
+ 'callback' => $callback,
+ 'permission_callback' => array( $this, 'is_available_to_current_user' ),
+ 'args' => $args,
+ 'show_in_index' => ! is_wp_error( $this->is_available_to_current_user() ),
+ ),
+ )
+ );
+ }
+
+ /**
+ * Returns the full endpoint path for a given route.
+ *
+ * @since 3.17.0
+ *
+ * @param string $route The route.
+ * @return string
+ */
+ public function get_full_endpoint( string $route = '' ): string {
+ $route = trim( $route, '/' );
+
+ if ( '' !== $route ) {
+ $route = static::get_endpoint_name() . '/' . $route;
+ } else {
+ $route = static::get_endpoint_name();
+ }
+
+ return '/' .
+ $this->api_controller->get_full_namespace() .
+ '/' .
+ $this->api_controller->prefix_route( $route );
+ }
+
+ /**
+ * Returns the endpoint slug.
+ *
+ * The slug is the endpoint name prefixed with the route prefix, from
+ * the API controller.
+ *
+ * Used as an identifier for the endpoint, when registering routes.
+ *
+ * @since 3.17.0
+ *
+ * @return string
+ */
+ public function get_endpoint_slug(): string {
+ return $this->api_controller->prefix_route( '' ) . static::get_endpoint_name();
+ }
+
+ /**
+ * Returns the registered routes.
+ *
+ * @since 3.17.0
+ *
+ * @return array
+ */
+ public function get_registered_routes(): array {
+ return $this->registered_routes;
+ }
+
+ /**
+ * Returns whether the endpoint is available for access by the current
+ * user.
+ *
+ * @since 3.14.0 Replaced `is_public_endpoint`, `user_capability` and `permission_callback()`.
+ * @since 3.16.0 Added the `$request` parameter.
+ * @since 3.17.0 Moved to the new API structure.
+ *
+ * @param WP_REST_Request|null $request The request object.
+ * @return WP_Error|bool True if the endpoint is available.
+ */
+ public function is_available_to_current_user( ?WP_REST_Request $request = null ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found
+ // Validate the API key and secret.
+ $api_key_validation = $this->validate_site_id_and_secret();
+ if ( is_wp_error( $api_key_validation ) ) {
+ return $api_key_validation;
+ }
+
+ // Validate the user capability.
+ $capability = $this->get_default_access_capability();
+ return current_user_can(
+ // phpcs:ignore WordPress.WP.Capabilities.Undetermined
+ $this->apply_capability_filters( $capability )
+ );
+ }
+
+ /**
+ * Returns the user capability allowing access to the endpoint, after having
+ * applied capability filters.
+ *
+ * The default access capability is not passed here by default, to allow for
+ * a more explicit declaration in child classes.
+ *
+ * @since 3.14.0
+ * @since 3.17.0 Moved to the new API structure.
+ *
+ * @param string $capability The original capability allowing access.
+ * @return string The capability allowing access after applying the filters.
+ */
+ public function apply_capability_filters( string $capability ): string {
+ /**
+ * Filter to change the default user capability for all private endpoints.
+ *
+ * @var string
+ */
+ $default_user_capability = apply_filters(
+ 'wp_parsely_user_capability_for_all_private_apis',
+ $capability
+ );
+
+ /**
+ * Filter to change the user capability for the specific endpoint.
+ *
+ * @var string
+ */
+ $endpoint_specific_user_capability = apply_filters(
+ 'wp_parsely_user_capability_for_' .
+ Utils::convert_endpoint_to_filter_key( static::get_endpoint_name() ) .
+ '_api',
+ $default_user_capability
+ );
+
+ return $endpoint_specific_user_capability;
+ }
+
+ /**
+ * Validates that the Site ID and secret are set.
+ * If the API secret is not required, it will not be validated.
+ *
+ * @since 3.13.0
+ * @since 3.17.0 Moved to the new API structure and renamed from `validate_apikey_and_secret`.
+ *
+ * @param bool $require_api_secret Specifies if the API Secret is required.
+ * @return WP_Error|bool
+ */
+ public function validate_site_id_and_secret( bool $require_api_secret = true ) {
+ if ( false === $this->parsely->site_id_is_set() ) {
+ return new WP_Error(
+ 'parsely_site_id_not_set',
+ __( 'A Parse.ly Site ID must be set in site options to use this endpoint', 'wp-parsely' ),
+ array( 'status' => 403 )
+ );
+ }
+
+ if ( $require_api_secret && false === $this->parsely->api_secret_is_set() ) {
+ return new WP_Error(
+ 'parsely_api_secret_not_set',
+ __( 'A Parse.ly API Secret must be set in site options to use this endpoint', 'wp-parsely' ),
+ array( 'status' => 403 )
+ );
+ }
+
+ return true;
+ }
+}
diff --git a/src/rest-api/class-rest-api-controller.php b/src/rest-api/class-rest-api-controller.php
new file mode 100644
index 000000000..de83801c8
--- /dev/null
+++ b/src/rest-api/class-rest-api-controller.php
@@ -0,0 +1,127 @@
+get_parsely() ),
+ new Stats_Controller( $this->get_parsely() ),
+ new Settings_Controller( $this->get_parsely() ),
+ );
+
+ // Initialize the controllers.
+ foreach ( $controllers as $controller ) {
+ $controller->init();
+ }
+
+ $this->controllers = $controllers;
+ }
+
+ /**
+ * Determines if the specified endpoint is available to the current user.
+ *
+ * @since 3.17.0
+ *
+ * @param string $endpoint The endpoint to check.
+ * @return bool True if the endpoint is available to the current user, false otherwise.
+ */
+ public function is_available_to_current_user( string $endpoint ): bool {
+ // Remove any forward or trailing slashes.
+ $endpoint = trim( $endpoint, '/' );
+
+ // Get the controller for the endpoint.
+ $controller = $this->get_controller_for_endpoint( $endpoint );
+ if ( null === $controller ) {
+ return false;
+ }
+
+ // Get the endpoint object.
+ $endpoint_obj = $controller->get_endpoint( $endpoint );
+ if ( null === $endpoint_obj ) {
+ return false;
+ }
+
+ // Check if the endpoint is available to the current user.
+ $is_available = $endpoint_obj->is_available_to_current_user();
+ if ( is_wp_error( $is_available ) ) {
+ return false;
+ }
+
+ return $is_available;
+ }
+
+ /**
+ * Gets the controller for the specified endpoint.
+ *
+ * @since 3.17.0
+ *
+ * @param string $endpoint The endpoint to get the controller for.
+ * @return Base_API_Controller|null The controller for the specified endpoint.
+ */
+ private function get_controller_for_endpoint( string $endpoint ): ?Base_API_Controller {
+ foreach ( $this->controllers as $controller ) {
+ if ( null !== $controller->get_endpoint( $endpoint ) ) {
+ return $controller;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/src/rest-api/content-helper/class-content-helper-controller.php b/src/rest-api/content-helper/class-content-helper-controller.php
new file mode 100644
index 000000000..872e47c23
--- /dev/null
+++ b/src/rest-api/content-helper/class-content-helper-controller.php
@@ -0,0 +1,49 @@
+register_endpoints( $endpoints );
+ }
+}
diff --git a/src/rest-api/content-helper/class-endpoint-excerpt-generator.php b/src/rest-api/content-helper/class-endpoint-excerpt-generator.php
new file mode 100644
index 000000000..921a7fd43
--- /dev/null
+++ b/src/rest-api/content-helper/class-endpoint-excerpt-generator.php
@@ -0,0 +1,198 @@
+suggestions_api = $controller->get_parsely()->get_suggestions_api();
+ }
+
+ /**
+ * Returns the name of the endpoint.
+ *
+ * @since 3.17.0
+ *
+ * @return string The endpoint name.
+ */
+ public static function get_endpoint_name(): string {
+ return 'excerpt-generator';
+ }
+
+ /**
+ * Returns the name of the feature associated with the current endpoint.
+ *
+ * @since 3.17.0
+ *
+ * @return string The feature name.
+ */
+ public function get_pch_feature_name(): string {
+ return 'excerpt_suggestions';
+ }
+
+ /**
+ * Registers the routes for the endpoint.
+ *
+ * @since 3.17.0
+ */
+ public function register_routes(): void {
+ /**
+ * POST /excerpt-generator/generate
+ * Generates an excerpt for the given content.
+ */
+ $this->register_rest_route(
+ 'generate',
+ array( 'POST' ),
+ array( $this, 'generate_excerpt' ),
+ array(
+ 'text' => array(
+ 'description' => __( 'The text to generate the excerpt from.', 'wp-parsely' ),
+ 'type' => 'string',
+ 'required' => true,
+ ),
+ 'title' => array(
+ 'description' => __( 'The title of the content.', 'wp-parsely' ),
+ 'type' => 'string',
+ 'required' => true,
+ ),
+ 'persona' => array(
+ 'description' => __( 'The persona to use for the suggestion.', 'wp-parsely' ),
+ 'type' => 'string',
+ 'required' => false,
+ 'default' => 'journalist',
+ ),
+ 'style' => array(
+ 'description' => __( 'The style to use for the suggestion.', 'wp-parsely' ),
+ 'type' => 'string',
+ 'required' => false,
+ 'default' => 'neutral',
+ ),
+ 'max_items' => array(
+ 'description' => __( 'The maximum number of items to generate.', 'wp-parsely' ),
+ 'type' => 'integer',
+ 'required' => false,
+ 'default' => 1,
+ ),
+ 'max_characters' => array(
+ 'description' => __( 'The maximum number of characters to generate.', 'wp-parsely' ),
+ 'type' => 'integer',
+ 'required' => false,
+ 'default' => 160,
+ ),
+ )
+ );
+ }
+
+ /**
+ * API Endpoint: POST /excerpt-generator/generate
+ *
+ * Generates an excerpt for the passed content.
+ *
+ * @since 3.17.0
+ *
+ * @param WP_REST_Request $request The request object.
+ * @return WP_REST_Response|WP_Error The response object.
+ */
+ public function generate_excerpt( WP_REST_Request $request ) {
+ /**
+ * The post content to be sent to the API.
+ *
+ * @var string $post_content
+ */
+ $post_content = $request->get_param( 'text' );
+
+ /**
+ * The post title to be sent to the API.
+ *
+ * @var string $post_title
+ */
+ $post_title = $request->get_param( 'title' );
+
+ /**
+ * The persona to be sent to the API.
+ *
+ * @var string $persona
+ */
+ $persona = $request->get_param( 'persona' );
+
+ /**
+ * The style to be sent to the API.
+ *
+ * @var string $style
+ */
+ $style = $request->get_param( 'style' );
+
+ /**
+ * The maximum number of items to generate.
+ *
+ * @var int $max_items
+ */
+ $max_items = $request->get_param( 'max_items' );
+
+ /**
+ * The maximum number of characters to generate.
+ *
+ * @var int $max_characters
+ */
+ $max_characters = $request->get_param( 'max_characters' );
+
+ $response = $this->suggestions_api->get_brief_suggestions(
+ $post_title,
+ $post_content,
+ array(
+ 'persona' => $persona,
+ 'style' => $style,
+ 'max_items' => $max_items,
+ 'max_characters' => $max_characters,
+ )
+ );
+
+ if ( is_wp_error( $response ) ) {
+ return $response;
+ }
+
+ // TODO: For now, only return the first suggestion. When the UI is ready to handle multiple suggestions, we can return the entire array.
+ $response = $response[0] ?? '';
+ return new WP_REST_Response( array( 'data' => $response ), 200 );
+ }
+}
diff --git a/src/Endpoints/content-helper/class-smart-linking-endpoint.php b/src/rest-api/content-helper/class-endpoint-smart-linking.php
similarity index 68%
rename from src/Endpoints/content-helper/class-smart-linking-endpoint.php
rename to src/rest-api/content-helper/class-endpoint-smart-linking.php
index d52e7e2c0..b2d45735f 100644
--- a/src/Endpoints/content-helper/class-smart-linking-endpoint.php
+++ b/src/rest-api/content-helper/class-endpoint-smart-linking.php
@@ -1,128 +1,139 @@
get_param( 'post_id' );
- if ( is_numeric( $temp_post_id ) ) {
- $post_id = intval( $temp_post_id );
- }
- }
-
- $can_access_pch = Permissions::current_user_can_use_pch_feature(
- 'smart_linking',
- $this->parsely->get_options()['content_helper'],
- $post_id
- );
+ public function __construct( Content_Helper_Controller $controller ) {
+ parent::__construct( $controller );
+ $this->suggestions_api = $controller->get_parsely()->get_suggestions_api();
+ }
- // Check if the current user has the smart linking capability.
- $has_capability = current_user_can(
- // phpcs:ignore WordPress.WP.Capabilities.Undetermined
- $this->apply_capability_filters(
- Base_Endpoint::DEFAULT_ACCESS_CAPABILITY
- )
- );
+ /**
+ * Returns the name of the endpoint.
+ *
+ * @since 3.17.0
+ *
+ * @return string The endpoint name.
+ */
+ public static function get_endpoint_name(): string {
+ return 'smart-linking';
+ }
- return $can_access_pch && $has_capability;
+ /**
+ * Returns the name of the feature associated with the current endpoint.
+ *
+ * @since 3.17.0
+ *
+ * @return string The feature name.
+ */
+ public function get_pch_feature_name(): string {
+ return 'smart_linking';
}
/**
- * Registers the endpoints.
+ * Registers the routes for the endpoint.
*
- * @since 3.16.0
+ * @since 3.17.0
*/
- public function run(): void {
+ public function register_routes(): void {
/**
- * POST /smart-linking/url-to-post-type
- * Converts a URL to a post type.
+ * GET /smart-linking/generate
+ * Generates smart links for a post.
*/
- $this->register_endpoint(
- static::ENDPOINT . '/url-to-post-type',
- 'url_to_post_type',
- array( 'POST' )
+ $this->register_rest_route(
+ 'generate',
+ array( 'POST' ),
+ array( $this, 'generate_smart_links' ),
+ array(
+ 'text' => array(
+ 'required' => true,
+ 'type' => 'string',
+ 'description' => __( 'The text to generate smart links for.', 'wp-parsely' ),
+ ),
+ 'max_links' => array(
+ 'type' => 'integer',
+ 'description' => __( 'The maximum number of smart links to generate.', 'wp-parsely' ),
+ 'default' => 10,
+ ),
+ 'url_exclusion_list' => array(
+ 'type' => 'array',
+ 'description' => __( 'The list of URLs to exclude from the smart links.', 'wp-parsely' ),
+ 'validate_callback' => array( $this, 'validate_url_exclusion_list' ),
+ 'default' => array(),
+ ),
+ )
);
/**
* GET /smart-linking/{post_id}/get
* Gets the smart links for a post.
*/
- $this->register_endpoint_with_args(
- static::ENDPOINT . '/(?P\d+)/get',
- 'get_smart_links',
+ $this->register_rest_route_with_post_id(
+ '/get',
array( 'GET' ),
- array(
- 'post_id' => array(
- 'required' => true,
- 'description' => __( 'The post ID.', 'wp-parsely' ),
- 'validate_callback' => array( $this, 'private_api_request_validate_post_id' ),
- ),
- )
+ array( $this, 'get_smart_links' )
);
/**
* POST /smart-linking/{post_id}/add
* Adds a smart link to a post.
*/
- $this->register_endpoint_with_args(
- static::ENDPOINT . '/(?P\d+)/add',
- 'add_smart_link',
+ $this->register_rest_route_with_post_id(
+ '/add',
array( 'POST' ),
+ array( $this, 'add_smart_link' ),
array(
- 'post_id' => array(
- 'required' => true,
- 'description' => __( 'The post ID.', 'wp-parsely' ),
- 'validate_callback' => array( $this, 'private_api_request_validate_post_id' ),
- ),
- 'link' => array(
+ 'link' => array(
'required' => true,
- 'type' => 'array',
+ 'type' => 'object',
'description' => __( 'The smart link data to add.', 'wp-parsely' ),
- 'validate_callback' => array( $this, 'private_api_request_validate_smart_link_params' ),
+ 'validate_callback' => array( $this, 'validate_smart_link_params' ),
),
- 'update' => array(
+ 'update' => array(
'type' => 'boolean',
'description' => __( 'Whether to update the existing smart link.', 'wp-parsely' ),
'default' => false,
@@ -134,23 +145,18 @@ public function run(): void {
* POST /smart-linking/{post_id}/add-multiple
* Adds multiple smart links to a post.
*/
- $this->register_endpoint_with_args(
- static::ENDPOINT . '/(?P\d+)/add-multiple',
- 'add_multiple_smart_links',
+ $this->register_rest_route_with_post_id(
+ '/add-multiple',
array( 'POST' ),
+ array( $this, 'add_multiple_smart_links' ),
array(
- 'post_id' => array(
- 'required' => true,
- 'description' => __( 'The post ID.', 'wp-parsely' ),
- 'validate_callback' => array( $this, 'private_api_request_validate_post_id' ),
- ),
- 'links' => array(
+ 'links' => array(
'required' => true,
'type' => 'array',
'description' => __( 'The multiple smart links data to add.', 'wp-parsely' ),
- 'validate_callback' => array( $this, 'private_api_request_validate_multiple_smart_links' ),
+ 'validate_callback' => array( $this, 'validate_multiple_smart_links' ),
),
- 'update' => array(
+ 'update' => array(
'type' => 'boolean',
'description' => __( 'Whether to update the existing smart links.', 'wp-parsely' ),
'default' => false,
@@ -162,79 +168,84 @@ public function run(): void {
* POST /smart-linking/{post_id}/set
* Updates the smart links of a given post and removes the ones that are not in the request.
*/
- $this->register_endpoint_with_args(
- static::ENDPOINT . '/(?P\d+)/set',
- 'set_smart_links',
+ $this->register_rest_route_with_post_id(
+ '/set',
array( 'POST' ),
+ array( $this, 'set_smart_links' ),
array(
- 'post_id' => array(
- 'required' => true,
- 'description' => __( 'The post ID.', 'wp-parsely' ),
- 'validate_callback' => array( $this, 'private_api_request_validate_post_id' ),
- ),
- 'links' => array(
+ 'links' => array(
'required' => true,
'type' => 'array',
'description' => __( 'The smart links data to set.', 'wp-parsely' ),
- 'validate_callback' => array( $this, 'private_api_request_validate_multiple_smart_links' ),
+ 'validate_callback' => array( $this, 'validate_multiple_smart_links' ),
),
)
);
+
+ /**
+ * POST /smart-linking/url-to-post-type
+ * Converts a URL to a post type.
+ */
+ $this->register_rest_route(
+ 'url-to-post-type',
+ array( 'POST' ),
+ array( $this, 'url_to_post_type' )
+ );
}
/**
- * API Endpoint: POST /smart-linking/url-to-post-type.
+ * API Endpoint: GET /smart-linking/generate.
*
- * Converts a URL to a post type.
+ * Generates smart links for a post.
*
* @since 3.16.0
*
* @param WP_REST_Request $request The request object.
- * @return WP_REST_Response The response object.
+ * @return WP_REST_Response|WP_Error The response object.
*/
- public function url_to_post_type( WP_REST_Request $request ): WP_REST_Response {
- $url = $request->get_param( 'url' );
-
- if ( ! is_string( $url ) ) {
- return new WP_REST_Response(
- array(
- 'error' => array(
- 'name' => 'invalid_request',
- 'message' => __( 'Invalid request body.', 'wp-parsely' ),
- ),
- ),
- 400
- );
- }
+ public function generate_smart_links( WP_REST_Request $request ) {
+ /**
+ * The text to generate smart links for.
+ *
+ * @var string $post_content
+ */
+ $post_content = $request->get_param( 'text' );
- $post_id = 0;
- $cache = wp_cache_get( $url, 'wp_parsely_smart_link_url_to_postid' );
+ /**
+ * The maximum number of smart links to generate.
+ *
+ * @var int $max_links
+ */
+ $max_links = $request->get_param( 'max_links' );
- if ( is_integer( $cache ) ) {
- $post_id = $cache;
- } elseif ( function_exists( 'wpcom_vip_url_to_postid' ) ) {
- $post_id = wpcom_vip_url_to_postid( $url );
- } else {
- // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.url_to_postid_url_to_postid
- $post_id = url_to_postid( $url );
- wp_cache_set( $url, $post_id, 'wp_parsely_smart_link_url_to_postid' );
- }
+ /**
+ * The URL exclusion list.
+ *
+ * @var array $url_exclusion_list
+ */
+ $url_exclusion_list = $request->get_param( 'url_exclusion_list' ) ?? array();
- $response = array(
- 'data' => array(
- 'post_id' => false,
- 'post_type' => false,
+ $response = $this->suggestions_api->get_smart_links(
+ $post_content,
+ array(
+ 'max_items' => $max_links,
),
+ $url_exclusion_list
);
- if ( 0 !== $post_id ) {
- $response['data']['post_id'] = $post_id;
- $response['data']['post_type'] = get_post_type( $post_id );
+ if ( is_wp_error( $response ) ) {
+ return $response;
}
- return new WP_REST_Response( $response, 200 );
- }
+ $smart_links = array_map(
+ function ( Smart_Link $link ) {
+ return $link->to_array();
+ },
+ $response
+ );
+ return new WP_REST_Response( array( 'data' => $smart_links ), 200 );
+ }
/**
* API Endpoint: GET /smart-linking/{post_id}/get.
@@ -464,35 +475,87 @@ public function set_smart_links( WP_REST_Request $request ): WP_REST_Response {
return new WP_REST_Response( array( 'data' => $response ), 200 );
}
+
/**
- * Validates the post ID parameter.
+ * API Endpoint: POST /smart-linking/url-to-post-type.
*
- * The callback sets the post object in the request object if the parameter is valid.
+ * Converts a URL to a post type.
*
* @since 3.16.0
- * @access private
*
- * @param string $param The parameter value.
* @param WP_REST_Request $request The request object.
- * @return bool Whether the parameter is valid.
+ * @return WP_REST_Response The response object.
*/
- public function private_api_request_validate_post_id( string $param, WP_REST_Request $request ): bool {
- if ( ! is_numeric( $param ) ) {
- return false;
+ public function url_to_post_type( WP_REST_Request $request ): WP_REST_Response {
+ $url = $request->get_param( 'url' );
+
+ if ( ! is_string( $url ) ) {
+ return new WP_REST_Response(
+ array(
+ 'error' => array(
+ 'name' => 'invalid_request',
+ 'message' => __( 'Invalid request body.', 'wp-parsely' ),
+ ),
+ ),
+ 400
+ );
}
- $param = filter_var( $param, FILTER_VALIDATE_INT );
+ $post_id = 0;
+ $cache = wp_cache_get( $url, 'wp_parsely_smart_link_url_to_postid' );
- if ( false === $param ) {
- return false;
+ if ( is_integer( $cache ) ) {
+ $post_id = $cache;
+ } elseif ( function_exists( 'wpcom_vip_url_to_postid' ) ) {
+ $post_id = wpcom_vip_url_to_postid( $url );
+ } else {
+ // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.url_to_postid_url_to_postid
+ $post_id = url_to_postid( $url );
+ wp_cache_set( $url, $post_id, 'wp_parsely_smart_link_url_to_postid' );
}
- // Validate if the post ID exists.
- $post = get_post( $param );
- // Set the post attribute in the request.
- $request->set_param( 'post', $post );
+ $response = array(
+ 'data' => array(
+ 'post_id' => false,
+ 'post_type' => false,
+ ),
+ );
- return null !== $post;
+ if ( 0 !== $post_id ) {
+ $response['data']['post_id'] = $post_id;
+ $response['data']['post_type'] = get_post_type( $post_id );
+ }
+
+ return new WP_REST_Response( $response, 200 );
+ }
+
+ /**
+ * Validates the URL exclusion list parameter.
+ *
+ * The callback sets the URL exclusion list in the request object if the parameter is valid.
+ *
+ * @since 3.17.0
+ * @access private
+ *
+ * @param mixed $param The parameter value.
+ * @param WP_REST_Request $request The request object.
+ * @return true|WP_Error Whether the parameter is valid.
+ */
+ public function validate_url_exclusion_list( $param, WP_REST_Request $request ) {
+ if ( ! is_array( $param ) ) {
+ return new WP_Error( 'invalid_url_exclusion_list', __( 'The URL exclusion list must be an array.', 'wp-parsely' ) );
+ }
+
+ $valid_urls = array_filter(
+ $param,
+ function ( $url ) {
+ return is_string( $url ) && false !== filter_var( $url, FILTER_VALIDATE_URL );
+ }
+ );
+
+ $request->set_param( 'url_exclusion_list', $valid_urls );
+
+ return true;
}
/**
@@ -507,7 +570,7 @@ public function private_api_request_validate_post_id( string $param, WP_REST_Req
* @param WP_REST_Request $request The request object.
* @return bool Whether the parameters are valid.
*/
- public function private_api_request_validate_smart_link_params( array $params, WP_REST_Request $request ): bool {
+ public function validate_smart_link_params( array $params, WP_REST_Request $request ): bool {
$required_params = array( 'uid', 'href', 'title', 'text', 'offset' );
foreach ( $required_params as $param ) {
@@ -566,11 +629,11 @@ public function private_api_request_validate_smart_link_params( array $params, W
* @param WP_REST_Request $request The request object.
* @return bool Whether the parameter is valid.
*/
- public function private_api_request_validate_multiple_smart_links( array $param, WP_REST_Request $request ): bool {
+ public function validate_multiple_smart_links( array $param, WP_REST_Request $request ): bool {
$smart_links = array();
foreach ( $param as $link ) {
- if ( $this->private_api_request_validate_smart_link_params( $link, $request ) ) {
+ if ( $this->validate_smart_link_params( $link, $request ) ) {
$smart_link = $request->get_param( 'smart_link' );
$smart_links[] = $smart_link;
} else {
diff --git a/src/rest-api/content-helper/class-endpoint-title-suggestions.php b/src/rest-api/content-helper/class-endpoint-title-suggestions.php
new file mode 100644
index 000000000..b737b7566
--- /dev/null
+++ b/src/rest-api/content-helper/class-endpoint-title-suggestions.php
@@ -0,0 +1,173 @@
+suggestions_api = $controller->get_parsely()->get_suggestions_api();
+ }
+
+ /**
+ * Returns the name of the endpoint.
+ *
+ * @since 3.17.0
+ *
+ * @return string The endpoint name.
+ */
+ public static function get_endpoint_name(): string {
+ return 'title-suggestions';
+ }
+
+ /**
+ * Returns the name of the feature associated with the current endpoint.
+ *
+ * @since 3.17.0
+ *
+ * @return string
+ */
+ public function get_pch_feature_name(): string {
+ return 'title_suggestions';
+ }
+
+ /**
+ * Registers the routes for the endpoint.
+ *
+ * @since 3.17.0
+ */
+ public function register_routes(): void {
+ /**
+ * POST /title-suggestions/generate
+ * Generates titles for the given content.
+ */
+ $this->register_rest_route(
+ 'generate',
+ array( 'POST' ),
+ array( $this, 'generate_titles' ),
+ array(
+ 'text' => array(
+ 'description' => __( 'The content for which to generate titles.', 'wp-parsely' ),
+ 'required' => true,
+ 'type' => 'string',
+ ),
+ 'limit' => array(
+ 'description' => __( 'The maximum number of titles to be suggested.', 'wp-parsely' ),
+ 'required' => false,
+ 'type' => 'integer',
+ 'default' => 3,
+ ),
+ 'style' => array(
+ 'description' => __( 'The style of the titles to be suggested.', 'wp-parsely' ),
+ 'required' => false,
+ 'type' => 'string',
+ 'default' => 'neutral',
+ ),
+ 'persona' => array(
+ 'description' => __( 'The persona of the titles to be suggested.', 'wp-parsely' ),
+ 'required' => false,
+ 'type' => 'string',
+ 'default' => 'journalist',
+ ),
+ )
+ );
+ }
+
+ /**
+ * API Endpoint: POST /title-suggestions/generate
+ *
+ * Generates titles for the given content.
+ *
+ * @since 3.17.0
+ *
+ * @param WP_REST_Request $request The request object.
+ * @return WP_REST_Response|WP_Error The response object or a WP_Error object on failure.
+ */
+ public function generate_titles( WP_REST_Request $request ) {
+ /**
+ * The post content to be sent to the API.
+ *
+ * @var string $post_content
+ */
+ $post_content = $request->get_param( 'text' );
+
+ /**
+ * The maximum number of titles to generate.
+ *
+ * @var int $limit
+ */
+ $limit = $request->get_param( 'limit' );
+
+ /**
+ * The style of the titles to generate.
+ *
+ * @var string $style
+ */
+ $style = $request->get_param( 'style' );
+
+ /**
+ * The tone of the titles to generate.
+ *
+ * @var string $persona
+ */
+ $persona = $request->get_param( 'persona' );
+
+ if ( 0 === $limit ) {
+ $limit = 3;
+ }
+
+ $response = $this->suggestions_api->get_title_suggestions(
+ $post_content,
+ array(
+ 'persona' => $persona,
+ 'style' => $style,
+ 'max_items' => $limit,
+ )
+ );
+
+ if ( is_wp_error( $response ) ) {
+ return $response;
+ }
+
+ return new WP_REST_Response( array( 'data' => $response ), 200 );
+ }
+}
diff --git a/src/rest-api/content-helper/trait-content-helper-feature.php b/src/rest-api/content-helper/trait-content-helper-feature.php
new file mode 100644
index 000000000..383e7c2ce
--- /dev/null
+++ b/src/rest-api/content-helper/trait-content-helper-feature.php
@@ -0,0 +1,68 @@
+get_pch_feature_name(),
+ $this->parsely->get_options()['content_helper']
+ );
+ }
+
+ /**
+ * Checks if the endpoint is available to the current user.
+ *
+ * Overrides the method in the Base_Endpoint class to check if the
+ * current user has permission to use the feature.
+ *
+ * @since 3.17.0
+ *
+ * @param WP_REST_Request|null $request The request object.
+ * @return bool|WP_Error True if the endpoint is available.
+ */
+ public function is_available_to_current_user( WP_REST_Request $request = null ) {
+ $can_use_feature = $this->is_pch_feature_enabled_for_user();
+
+ if ( ! $can_use_feature ) {
+ return new WP_Error( 'ch_access_to_feature_disabled', '', array( 'status' => 403 ) );
+ }
+
+ return parent::is_available_to_current_user( $request );
+ }
+}
diff --git a/src/Endpoints/user-meta/class-base-endpoint-user-meta.php b/src/rest-api/settings/class-base-settings-endpoint.php
similarity index 64%
rename from src/Endpoints/user-meta/class-base-endpoint-user-meta.php
rename to src/rest-api/settings/class-base-settings-endpoint.php
index 72d1ca9e3..e4c47bf4b 100644
--- a/src/Endpoints/user-meta/class-base-endpoint-user-meta.php
+++ b/src/rest-api/settings/class-base-settings-endpoint.php
@@ -1,7 +1,6 @@
, default: mixed}
*/
-abstract class Base_Endpoint_User_Meta extends Base_Endpoint {
+abstract class Base_Settings_Endpoint extends Base_Endpoint {
/**
* The meta entry's default value. Initialized in the constructor.
*
* @since 3.13.0
+ * @since 3.17.0 Moved from Base_Endpoint_User_Meta.
*
* @var array
*/
@@ -38,6 +41,7 @@ abstract class Base_Endpoint_User_Meta extends Base_Endpoint {
* constructor.
*
* @since 3.13.0
+ * @since 3.17.0 Moved from Base_Endpoint_User_Meta.
*
* @var array>
*/
@@ -47,6 +51,7 @@ abstract class Base_Endpoint_User_Meta extends Base_Endpoint {
* The current user's ID.
*
* @since 3.14.0
+ * @since 3.17.0 Moved from Base_Endpoint_User_Meta.
*
* @var int
*/
@@ -56,6 +61,7 @@ abstract class Base_Endpoint_User_Meta extends Base_Endpoint {
* Returns the meta entry's key.
*
* @since 3.13.0
+ * @since 3.17.0 Moved from Base_Endpoint_User_Meta.
*
* @return string The meta entry's key.
*/
@@ -65,6 +71,7 @@ abstract protected function get_meta_key(): string;
* Returns the endpoint's subvalues specifications.
*
* @since 3.13.0
+ * @since 3.17.0 Moved from Base_Endpoint_User_Meta.
*
* @return array
*/
@@ -74,11 +81,12 @@ abstract protected function get_subvalues_specs(): array;
* Constructor.
*
* @since 3.13.0
+ * @since 3.17.0 Moved from Base_Endpoint_User_Meta.
*
- * @param Parsely $parsely Parsely instance.
+ * @param Base_API_Controller $controller The REST API controller.
*/
- public function __construct( Parsely $parsely ) {
- parent::__construct( $parsely );
+ public function __construct( Base_API_Controller $controller ) {
+ parent::__construct( $controller );
$subvalues_specs = $this->get_subvalues_specs();
@@ -89,109 +97,151 @@ public function __construct( Parsely $parsely ) {
}
/**
- * Registers the endpoint's WP REST route.
+ * Initializes the endpoint and sets the current user ID.
*
- * @since 3.13.0
+ * @since 3.17.0
*/
- public function run(): void {
- // Initialize the current user ID here, as doing it in the constructor
- // is too early.
+ public function init(): void {
+ parent::init();
$this->current_user_id = get_current_user_id();
-
- $this->register_endpoint(
- static::get_route(),
- 'process_request',
- array( 'GET', 'PUT' )
- );
}
/**
- * Returns the endpoint's route.
- *
- * @since 3.13.0
+ * Registers the routes for the endpoint.
*
- * @return string The endpoint's route.
+ * @since 3.17.0
*/
- public static function get_route(): string {
- return static::ENDPOINT;
+ public function register_routes(): void {
+ /**
+ * GET settings/{endpoint}/get
+ * Retrieves the settings for the current user.
+ */
+ $this->register_rest_route(
+ '/get',
+ array( 'GET' ),
+ array( $this, 'get_settings' )
+ );
+
+ /**
+ * PUT settings/{endpoint}/set
+ * Updates the settings for the current user.
+ */
+ $this->register_rest_route(
+ '/set',
+ array( 'PUT' ),
+ array( $this, 'set_settings' )
+ );
+
+ /**
+ * GET|PUT settings/{endpoint}
+ * Handles direct requests to the endpoint.
+ */
+ $this->register_rest_route(
+ '/',
+ array( 'GET', 'PUT' ),
+ array( $this, 'process_request' )
+ );
}
/**
- * Processes the requests sent to the endpoint.
+ * API Endpoint: GET|PUT settings/{endpoint}/
*
- * @since 3.13.0
+ * Processes the requests sent directly to the main endpoint.
+ *
+ * @since 3.17.0
*
* @param WP_REST_Request $request The request sent to the endpoint.
- * @return string The meta entry's value as JSON.
+ * @return WP_REST_Response|WP_Error The response object.
*/
- public function process_request( WP_REST_Request $request ): string {
+ public function process_request( WP_REST_Request $request ) {
$request_method = $request->get_method();
// Update the meta entry's value if the request method is PUT.
if ( 'PUT' === $request_method ) {
- $meta_value = $request->get_json_params();
- $this->set_value( $meta_value );
+ return $this->set_settings( $request );
}
- return $this->get_value();
+ return $this->get_settings();
}
/**
- * Returns whether the endpoint is available for access by the current
- * user.
+ * API Endpoint: GET settings/{endpoint}/get
*
- * @since 3.14.0
- * @since 3.16.0 Added the `$request` parameter.
+ * Retrieves the settings for the current user.
*
- * @param WP_REST_Request|null $request The request object.
- * @return bool
- */
- public function is_available_to_current_user( $request = null ): bool {
- return current_user_can( 'edit_user', $this->current_user_id );
- }
-
- /**
- * Returns the meta entry's value as JSON.
- *
- * @since 3.13.0
+ * @since 3.17.0
*
- * @return string The meta entry's value as JSON.
+ * @return WP_REST_Response The response object.
*/
- protected function get_value(): string {
- $meta_key = $this->get_meta_key();
- $meta_value = get_user_meta( $this->current_user_id, $meta_key, true );
+ public function get_settings(): WP_REST_Response {
+ $meta_key = $this->get_meta_key();
+ $settings = get_user_meta( $this->current_user_id, $meta_key, true );
- if ( ! is_array( $meta_value ) || 0 === count( $meta_value ) ) {
- $meta_value = $this->default_value;
+ if ( ! is_array( $settings ) || 0 === count( $settings ) ) {
+ $settings = $this->default_value;
}
- $result = wp_json_encode( $meta_value );
-
- return false !== $result ? $result : '';
+ return new WP_REST_Response( $settings, 200 );
}
/**
- * Sets the meta entry's value.
+ * API Endpoint: PUT settings/{endpoint}/set
*
- * @since 3.13.0
+ * Updates the settings for the current user.
*
- * @param array $meta_value The value to set the meta entry to.
- * @return bool Whether updating the meta entry's value was successful.
+ * @since 3.17.0
+ *
+ * @param WP_REST_Request $request The request object.
+ * @return WP_REST_Response|WP_Error The response object.
*/
- protected function set_value( array $meta_value ): bool {
+ public function set_settings( WP_REST_Request $request ) {
+ $meta_value = $request->get_json_params();
+
+ // Validates the settings format.
+ if ( ! is_array( $meta_value ) ) { // @phpstan-ignore-line
+ return new WP_Error(
+ 'ch_settings_invalid_format',
+ __( 'Settings must be a valid JSON array', 'wp-parsely' )
+ );
+ }
+
$sanitized_value = $this->sanitize_value( $meta_value );
+ // If the current settings are the same as the new settings, return early.
+ $current_settings = $this->get_settings();
+ if ( $current_settings->get_data() === $sanitized_value ) {
+ return $current_settings;
+ }
+
$update_meta = update_user_meta(
$this->current_user_id,
$this->get_meta_key(),
$sanitized_value
);
- if ( false !== $update_meta ) {
- return true;
+ if ( false === $update_meta ) {
+ return new WP_Error(
+ 'ch_settings_update_failed',
+ __( 'Failed to update settings', 'wp-parsely' )
+ );
}
- return false;
+ return new WP_REST_Response( $sanitized_value, 200 );
+ }
+
+ /**
+ * Returns whether the endpoint is available for access by the current
+ * user.
+ *
+ * @since 3.14.0
+ * @since 3.16.0 Added the `$request` parameter.
+ * @since 3.17.0 Moved from Base_Endpoint_User_Meta.
+ *
+ * @param WP_REST_Request|null $request The request object.
+ * @return bool
+ */
+ public function is_available_to_current_user( ?WP_REST_Request $request = null ): bool {
+ return current_user_can( 'edit_user', $this->current_user_id );
}
/**
@@ -199,6 +249,7 @@ protected function set_value( array $meta_value ): bool {
*
* @since 3.13.0
* @since 3.14.0 Added support for nested arrays.
+ * @since 3.17.0 Moved from Base_Endpoint_User_Meta.
*
* @param array $meta_value The meta value to sanitize.
* @param string $parent_key The parent key for the current level of the meta.
@@ -246,8 +297,9 @@ protected function sanitize_value( array $meta_value, string $parent_key = '' ):
/**
* Sanitizes the passed subvalue.
*
- * @since 3.14.0 Added support for nested arrays.
* @since 3.13.0
+ * @since 3.14.0 Added support for nested arrays.
+ * @since 3.17.0 Moved from Base_Endpoint_User_Meta.
*
* @param string $composite_key The subvalue's key.
* @param mixed $value The value to sanitize.
@@ -284,41 +336,11 @@ protected function sanitize_subvalue( string $composite_key, $value ) {
return $value;
}
- /**
- * Checks if a given composite key is valid.
- *
- * @since 3.14.3
- *
- * @param string|mixed $composite_key The composite key representing the nested path.
- * @return bool Whether the key is valid.
- */
- protected function is_valid_key( $composite_key ): bool {
- if ( ! is_string( $composite_key ) ) {
- return false; // Key path is not a string.
- }
-
- $keys = explode( '.', $composite_key );
- $current = $this->valid_subvalues;
-
- foreach ( $keys as $key ) {
- if ( ! is_array( $current ) || ! isset( $current[ $key ] ) ) {
- return false; // Key path is invalid.
- }
-
- if ( isset( $current[ $key ]['values'] ) ) {
- $current = $current[ $key ]['values'];
- } else {
- $current = $current[ $key ];
- }
- }
-
- return true;
- }
-
/**
* Gets the valid values for a given setting path.
*
* @since 3.14.3
+ * @since 3.17.0 Moved from Base_Endpoint_User_Meta.
*
* @param array $keys The path to the setting.
* @return array The valid values for the setting path.
@@ -344,6 +366,7 @@ protected function get_valid_values( array $keys ): array {
* Gets the default value for a given setting path.
*
* @since 3.14.3
+ * @since 3.17.0 Moved from Base_Endpoint_User_Meta.
*
* @param array $keys The path to the setting.
* @return mixed|array|null The default value for the setting path.
@@ -370,6 +393,7 @@ protected function get_default( array $keys ) {
* Gets the specifications for nested settings based on a composite key.
*
* @since 3.14.3
+ * @since 3.17.0 Moved from Base_Endpoint_User_Meta.
*
* @param string $composite_key The composite key representing the nested path.
* @return array The specifications for the nested path.
diff --git a/src/Endpoints/user-meta/class-dashboard-widget-settings-endpoint.php b/src/rest-api/settings/class-endpoint-dashboard-widget-settings.php
similarity index 59%
rename from src/Endpoints/user-meta/class-dashboard-widget-settings-endpoint.php
rename to src/rest-api/settings/class-endpoint-dashboard-widget-settings.php
index 32ae65efd..f23707c9b 100644
--- a/src/Endpoints/user-meta/class-dashboard-widget-settings-endpoint.php
+++ b/src/rest-api/settings/class-endpoint-dashboard-widget-settings.php
@@ -1,30 +1,39 @@
*/
diff --git a/src/Endpoints/user-meta/class-editor-sidebar-settings-endpoint.php b/src/rest-api/settings/class-endpoint-editor-sidebar-settings.php
similarity index 78%
rename from src/Endpoints/user-meta/class-editor-sidebar-settings-endpoint.php
rename to src/rest-api/settings/class-endpoint-editor-sidebar-settings.php
index 75de86489..850a76519 100644
--- a/src/Endpoints/user-meta/class-editor-sidebar-settings-endpoint.php
+++ b/src/rest-api/settings/class-endpoint-editor-sidebar-settings.php
@@ -1,30 +1,39 @@
*/
diff --git a/src/rest-api/settings/class-settings-controller.php b/src/rest-api/settings/class-settings-controller.php
new file mode 100644
index 000000000..930856a66
--- /dev/null
+++ b/src/rest-api/settings/class-settings-controller.php
@@ -0,0 +1,47 @@
+register_endpoints( $endpoints );
+ }
+}
diff --git a/src/rest-api/stats/class-endpoint-post.php b/src/rest-api/stats/class-endpoint-post.php
new file mode 100644
index 000000000..2789d259c
--- /dev/null
+++ b/src/rest-api/stats/class-endpoint-post.php
@@ -0,0 +1,499 @@
+content_api = $this->parsely->get_content_api();
+ }
+
+ /**
+ * Returns the endpoint name.
+ *
+ * @since 3.17.0
+ *
+ * @return string The endpoint name.
+ */
+ public static function get_endpoint_name(): string {
+ return 'post';
+ }
+
+ /**
+ * Registers the routes for the endpoint.
+ *
+ * @since 3.17.0
+ */
+ public function register_routes(): void {
+ /**
+ * GET /stats/post/{post_id}/details
+ * Returns the analytics details of a post.
+ */
+ $this->register_rest_route_with_post_id(
+ '/details',
+ array( 'GET' ),
+ array( $this, 'get_post_details' ),
+ array_merge(
+ array(
+ 'period_start' => array(
+ 'description' => __( 'The start of the period.', 'wp-parsely' ),
+ 'type' => 'string',
+ 'required' => false,
+ ),
+ 'period_end' => array(
+ 'description' => __( 'The end of the period.', 'wp-parsely' ),
+ 'type' => 'string',
+ 'required' => false,
+ ),
+ ),
+ $this->get_itm_source_param_args()
+ )
+ );
+
+ /**
+ * GET /stats/post/{post_id}/referrers
+ * Returns the referrers of a post.
+ */
+ $this->register_rest_route_with_post_id(
+ '/referrers',
+ array( 'GET' ),
+ array( $this, 'get_post_referrers' ),
+ array(
+ 'period_start' => array(
+ 'description' => __( 'The start of the period.', 'wp-parsely' ),
+ 'type' => 'string',
+ 'required' => false,
+ ),
+ 'period_end' => array(
+ 'description' => __( 'The end of the period.', 'wp-parsely' ),
+ 'type' => 'string',
+ 'required' => false,
+ ),
+ 'total_views' => array(
+ 'description' => __( 'The total views of the post.', 'wp-parsely' ),
+ 'type' => 'string',
+ 'required' => false,
+ 'default' => '0',
+ ),
+ )
+ );
+
+ /**
+ * GET /stats/post/{post_id}/related
+ * Returns the related posts of a post.
+ */
+ $this->register_rest_route_with_post_id(
+ '/related',
+ array( 'GET' ),
+ array( $this, 'get_related_posts' ),
+ $this->get_related_posts_param_args()
+ );
+ }
+
+ /**
+ * API Endpoint: GET /stats/post/{post_id}/details
+ *
+ * Gets the details of a post.
+ *
+ * @since 3.17.0
+ *
+ * @param WP_REST_Request $request The request object.
+ * @return WP_REST_Response|WP_Error The response object.
+ */
+ public function get_post_details( WP_REST_Request $request ) {
+ /**
+ * The post object.
+ *
+ * @var WP_Post $post
+ */
+ $post = $request->get_param( 'post' );
+ $permalink = get_permalink( $post->ID );
+
+ if ( ! is_string( $permalink ) ) {
+ return new WP_Error( 'invalid_post', __( 'Invalid post.', 'wp-parsely' ), array( 'status' => 404 ) );
+ }
+
+ // Set the itm_source parameter.
+ $this->set_itm_source_from_request( $request );
+
+ // Get the data from the API.
+ $analytics_request = $this->content_api->get_post_details(
+ $permalink,
+ $request->get_param( 'period_start' ),
+ $request->get_param( 'period_end' )
+ );
+
+ if ( is_wp_error( $analytics_request ) ) {
+ return $analytics_request;
+ }
+
+ $post_data = array();
+
+ /**
+ * The analytics data object.
+ *
+ * @var array> $analytics_request
+ */
+ foreach ( $analytics_request as $data ) {
+ $post_data[] = $this->extract_post_data( $data );
+ }
+
+ $response_data = array(
+ 'params' => $request->get_params(),
+ 'data' => $post_data,
+ );
+
+ return new WP_REST_Response( $response_data, 200 );
+ }
+
+ /**
+ * API Endpoint: GET /stats/post/{post_id}/referrers
+ *
+ * Gets the referrers of a post.
+ *
+ * @since 3.17.0
+ *
+ * @param WP_REST_Request $request The request object.
+ * @return WP_REST_Response|WP_Error The response object.
+ */
+ public function get_post_referrers( WP_REST_Request $request ) {
+ /**
+ * The post object.
+ *
+ * @var WP_Post $post
+ */
+ $post = $request->get_param( 'post' );
+ $permalink = get_permalink( $post->ID );
+
+ if ( ! is_string( $permalink ) ) {
+ return new WP_Error( 'invalid_post', __( 'Invalid post.', 'wp-parsely' ), array( 'status' => 404 ) );
+ }
+
+ // Set the itm_source parameter.
+ $this->set_itm_source_from_request( $request );
+
+ // Get the total views.
+ $total_views = $request->get_param( 'total_views' ) ?? 0;
+
+ if ( is_string( $total_views ) ) {
+ $total_views = Utils::convert_to_positive_integer( $total_views );
+ }
+
+ $this->total_views = $total_views;
+
+ // Get the data from the API.
+ $analytics_request = $this->content_api->get_post_referrers(
+ $permalink,
+ $request->get_param( 'period_start' ),
+ $request->get_param( 'period_end' )
+ );
+
+ if ( is_wp_error( $analytics_request ) ) {
+ return $analytics_request;
+ }
+
+ /**
+ * The analytics data object.
+ *
+ * @var array $analytics_request
+ */
+ $referrers_types = $this->generate_referrer_types_data( $analytics_request );
+ $direct_views = Utils::convert_to_positive_integer(
+ $referrers_types['direct']['views'] ?? '0'
+ );
+ $referrers_top = $this->generate_referrers_data( 5, $analytics_request, $direct_views );
+
+ $response_data = array(
+ 'params' => $request->get_params(),
+ 'data' => array(
+ 'top' => $referrers_top,
+ 'types' => $referrers_types,
+ ),
+ );
+
+ return new WP_REST_Response( $response_data, 200 );
+ }
+
+ /**
+ * API Endpoint: GET /stats/post/{post_id}/related
+ *
+ * Gets the related posts of a post.
+ *
+ * @since 3.17.0
+ *
+ * @param WP_REST_Request $request The request object.
+ * @return WP_REST_Response|WP_Error The response data.
+ */
+ public function get_related_posts( WP_REST_Request $request ) {
+ /**
+ * The post object.
+ *
+ * @var WP_Post $post
+ */
+ $post = $request->get_param( 'post' );
+
+ /**
+ * The post permalink.
+ *
+ * @var string $permalink
+ */
+ $permalink = get_permalink( $post->ID );
+
+ $related_posts = $this->get_related_posts_of_url( $request, $permalink );
+
+ $response_data = array(
+ 'params' => $request->get_params(),
+ 'data' => $related_posts,
+ );
+
+ return new WP_REST_Response( $response_data, 200 );
+ }
+
+ /**
+ * Generates the referrer types data.
+ *
+ * Referrer types are:
+ * - `social`: Views coming from social media.
+ * - `search`: Views coming from search engines.
+ * - `other`: Views coming from other referrers, like external websites.
+ * - `internal`: Views coming from linking pages of the same website.
+ *
+ * Returned object properties:
+ * - `views`: The number of views.
+ * - `viewsPercentage`: The number of views as a percentage, compared to the
+ * total views of all referrer types.
+ *
+ * @since 3.6.0
+ * @since 3.17.0 Moved from the `Referrers_Post_Detail_API_Proxy` class.
+ *
+ * @param array $response The response received by the proxy.
+ * @return array The generated data.
+ */
+ private function generate_referrer_types_data( array $response ): array {
+ $result = array();
+ $total_referrer_views = 0; // Views from all referrer types combined.
+
+ // Set referrer type order as it is displayed in the Parse.ly dashboard.
+ $referrer_type_keys = array( 'social', 'search', 'other', 'internal', 'direct' );
+ foreach ( $referrer_type_keys as $key ) {
+ $result[ $key ] = array( 'views' => 0 );
+ }
+
+ // Set views and views totals.
+ foreach ( $response as $referrer_data ) {
+ /**
+ * @var int $current_views
+ */
+ $current_views = $referrer_data['metrics']['referrers_views'] ?? 0;
+ $total_referrer_views += $current_views;
+
+ /**
+ * @var string $current_key
+ */
+ $current_key = $referrer_data['type'] ?? '';
+ if ( '' !== $current_key ) {
+ if ( ! isset( $result[ $current_key ]['views'] ) ) {
+ $result[ $current_key ] = array( 'views' => 0 );
+ }
+
+ $result[ $current_key ]['views'] += $current_views;
+ }
+ }
+
+ // Add direct and total views to the object.
+ $result['direct']['views'] = $this->total_views - $total_referrer_views;
+ $result['totals'] = array( 'views' => $this->total_views );
+
+ // Remove referrer types without views.
+ foreach ( $referrer_type_keys as $key ) {
+ if ( 0 === $result[ $key ]['views'] ) {
+ unset( $result[ $key ] );
+ }
+ }
+
+ // Set percentage values and format numbers.
+ foreach ( $result as $key => $value ) {
+ // Set and format percentage values.
+ $result[ $key ]['viewsPercentage'] = $this->get_i18n_percentage(
+ absint( $value['views'] ),
+ $this->total_views
+ );
+
+ // Format views values.
+ $result[ $key ]['views'] = number_format_i18n( $result[ $key ]['views'] );
+ }
+
+ return $result;
+ }
+
+ /**
+ * Generates the top referrers data.
+ *
+ * Returned object properties:
+ * - `views`: The number of views.
+ * - `viewsPercentage`: The number of views as a percentage, compared to the
+ * total views of all referrer types.
+ * - `datasetViewsPercentage`: The number of views as a percentage, compared
+ * to the total views of the current dataset.
+ *
+ * @since 3.6.0
+ * @since 3.17.0 Moved from the `Referrers_Post_Detail_API_Proxy` class.
+ *
+ * @param int $limit The limit of returned referrers.
+ * @param array $response The response received by the proxy.
+ * @param int $direct_views The count of direct views.
+ * @return array The generated data.
+ */
+ private function generate_referrers_data(
+ int $limit,
+ array $response,
+ int $direct_views
+ ): array {
+ $temp_views = array();
+ $totals = 0;
+ $referrer_count = count( $response );
+
+ // Set views and views totals.
+ $loop_count = $referrer_count > $limit ? $limit : $referrer_count;
+ for ( $i = 0; $i < $loop_count; $i++ ) {
+ $data = $response[ $i ];
+
+ /**
+ * @var int $referrer_views
+ */
+ $referrer_views = $data['metrics']['referrers_views'] ?? 0;
+ $totals += $referrer_views;
+ if ( isset( $data['name'] ) ) {
+ $temp_views[ $data['name'] ] = $referrer_views;
+ }
+ }
+
+ // If applicable, add the direct views.
+ if ( isset( $referrer_views ) && $direct_views >= $referrer_views ) {
+ $temp_views['direct'] = $direct_views;
+ $totals += $direct_views;
+ arsort( $temp_views );
+ if ( count( $temp_views ) > $limit ) {
+ $totals -= array_pop( $temp_views );
+ }
+ }
+
+ // Convert temporary array to result object and add totals.
+ $result = array();
+ foreach ( $temp_views as $key => $value ) {
+ $result[ $key ] = array( 'views' => $value );
+ }
+ $result['totals'] = array( 'views' => $totals );
+
+ // Set percentage values and format numbers.
+ foreach ( $result as $key => $value ) {
+ // Percentage against all referrer views, even those not included
+ // in the dataset due to the $limit argument.
+ $result[ $key ]['viewsPercentage'] = $this
+ ->get_i18n_percentage( absint( $value['views'] ), $this->total_views );
+
+ // Percentage against the current dataset that is limited due to the
+ // $limit argument.
+ $result[ $key ]['datasetViewsPercentage'] = $this
+ ->get_i18n_percentage( absint( $value['views'] ), $totals );
+
+ // Format views values.
+ $result[ $key ]['views'] = number_format_i18n( $result[ $key ]['views'] );
+ }
+
+ return $result;
+ }
+
+ /**
+ * Returns the passed number compared to the passed total, in an
+ * internationalized percentage format.
+ *
+ * @since 3.6.0
+ * @since 3.17.0 Moved from the `Referrers_Post_Detail_API_Proxy` class.
+ *
+ * @param int $number The number to be calculated as a percentage.
+ * @param int $total The total number to compare against.
+ * @return string|false The internationalized percentage or false on error.
+ */
+ private function get_i18n_percentage( int $number, int $total ) {
+ if ( 0 === $total ) {
+ return false;
+ }
+
+ return number_format_i18n( $number / $total * 100, 2 );
+ }
+}
diff --git a/src/rest-api/stats/class-endpoint-posts.php b/src/rest-api/stats/class-endpoint-posts.php
new file mode 100644
index 000000000..0671b9e5e
--- /dev/null
+++ b/src/rest-api/stats/class-endpoint-posts.php
@@ -0,0 +1,280 @@
+
+ * @see https://docs.parse.ly/api-available-metrics/
+ */
+ public const SORT_METRICS = array(
+ 'views',
+ 'mobile_views',
+ 'tablet_views',
+ 'desktop_views',
+ 'visitors',
+ 'visitors_new',
+ 'visitors_returning',
+ 'engaged_minutes',
+ 'avg_engaged',
+ 'avg_engaged_new',
+ 'avg_engaged_returning',
+ 'social_interactions',
+ 'fb_interactions',
+ 'tw_interactions',
+ 'pi_interactions',
+ 'social_referrals',
+ 'fb_referrals',
+ 'tw_referrals',
+ 'pi_referrals',
+ 'search_refs',
+ );
+
+ /**
+ * The Parse.ly Content API service.
+ *
+ * @since 3.17.0
+ *
+ * @var Content_API_Service
+ */
+ public $content_api;
+
+ /**
+ * Constructor.
+ *
+ * @since 3.17.0
+ *
+ * @param Stats_Controller $controller The stats controller.
+ */
+ public function __construct( Stats_Controller $controller ) {
+ parent::__construct( $controller );
+ $this->content_api = $this->parsely->get_content_api();
+ }
+
+ /**
+ * Returns the endpoint name.
+ *
+ * @since 3.17.0
+ *
+ * @return string
+ */
+ public static function get_endpoint_name(): string {
+ return 'posts';
+ }
+
+ /**
+ * Registers the routes for the objects of the controller.
+ *
+ * @since 3.17.0
+ */
+ public function register_routes(): void {
+ /**
+ * GET /posts
+ * Retrieves posts for the given criteria.
+ */
+ $this->register_rest_route(
+ '/',
+ array( 'GET' ),
+ array( $this, 'get_posts' ),
+ array_merge(
+ array(
+ 'period_start' => array(
+ 'description' => 'The start of the period to query.',
+ 'type' => 'string',
+ 'required' => false,
+ ),
+ 'period_end' => array(
+ 'description' => 'The end of the period to query.',
+ 'type' => 'string',
+ 'required' => false,
+ ),
+ 'pub_date_start' => array(
+ 'description' => 'The start of the publication date range to query.',
+ 'type' => 'string',
+ 'required' => false,
+ ),
+ 'pub_date_end' => array(
+ 'description' => 'The end of the publication date range to query.',
+ 'type' => 'string',
+ 'required' => false,
+ ),
+ 'limit' => array(
+ 'description' => 'The number of posts to return.',
+ 'type' => 'integer',
+ 'required' => false,
+ 'default' => self::TOP_POSTS_DEFAULT_LIMIT,
+ ),
+ 'sort' => array(
+ 'description' => 'The sort order of the posts.',
+ 'type' => 'string',
+ 'enum' => self::SORT_METRICS,
+ 'default' => self::SORT_DEFAULT,
+ 'required' => false,
+ ),
+ 'page' => array(
+ 'description' => 'The page to fetch.',
+ 'type' => 'integer',
+ 'required' => false,
+ 'default' => 1,
+ ),
+ 'author' => array(
+ 'description' => 'Comma-separated list of authors to filter by.',
+ 'type' => 'string',
+ 'required' => false,
+ 'validate_callback' => array( $this, 'validate_max_length_is_5' ),
+ 'sanitize_callback' => array( $this, 'sanitize_string_to_array' ),
+ ),
+ 'section' => array(
+ 'description' => 'Comma-separated list of sections to filter by.',
+ 'type' => 'string',
+ 'required' => false,
+ 'validate_callback' => array( $this, 'validate_max_length_is_5' ),
+ 'sanitize_callback' => array( $this, 'sanitize_string_to_array' ),
+ ),
+ 'tag' => array(
+ 'description' => 'Comma-separated list of tags to filter by.',
+ 'type' => 'string',
+ 'required' => false,
+ 'validate_callback' => array( $this, 'validate_max_length_is_5' ),
+ 'sanitize_callback' => array( $this, 'sanitize_string_to_array' ),
+ ),
+ 'segment' => array(
+ 'description' => 'The segment to filter by.',
+ 'type' => 'string',
+ 'required' => false,
+ ),
+ ),
+ $this->get_itm_source_param_args()
+ )
+ );
+ }
+
+ /**
+ * Sanitizes a string to an array, splitting it by commas.
+ *
+ * @since 3.17.0
+ *
+ * @param string|array $str The string to sanitize.
+ * @return array The sanitized array.
+ */
+ public function sanitize_string_to_array( $str ): array {
+ if ( is_array( $str ) ) {
+ return $str;
+ }
+
+ return explode( ',', $str );
+ }
+
+ /**
+ * Validates that the parameter has at most 5 items.
+ *
+ * @since 3.17.0
+ *
+ * @param string|array $string_or_array The string or array to validate.
+ * @return true|WP_Error
+ */
+ public function validate_max_length_is_5( $string_or_array ) {
+ if ( is_string( $string_or_array ) ) {
+ $string_or_array = $this->sanitize_string_to_array( $string_or_array );
+ }
+
+ if ( count( $string_or_array ) > 5 ) {
+ return new WP_Error( 'invalid_param', __( 'The parameter must have at most 5 items.', 'wp-parsely' ) );
+ }
+
+ return true;
+ }
+
+ /**
+ * API Endpoint: GET /stats/posts
+ *
+ * Retrieves the posts with the given query parameters.
+ *
+ * @since 3.17.0
+ *
+ * @param WP_REST_Request $request The request.
+ * @return array|WP_Error|WP_REST_Response
+ */
+ public function get_posts( WP_REST_Request $request ) {
+ $params = $request->get_params();
+
+ // Setup the itm_source if it is provided.
+ $this->set_itm_source_from_request( $request );
+
+ /**
+ * The raw analytics data, received by the API.
+ *
+ * @var array|WP_Error $analytics_request
+ */
+ $analytics_request = $this->content_api->get_posts(
+ array(
+ 'period_start' => $params['period_start'] ?? null,
+ 'period_end' => $params['period_end'] ?? null,
+ 'pub_date_start' => $params['pub_date_start'] ?? null,
+ 'pub_date_end' => $params['pub_date_end'] ?? null,
+ 'limit' => $params['limit'] ?? self::TOP_POSTS_DEFAULT_LIMIT,
+ 'sort' => $params['sort'] ?? self::SORT_DEFAULT,
+ 'page' => $params['page'] ?? 1,
+ 'author' => $params['author'] ?? null,
+ 'section' => $params['section'] ?? null,
+ 'tag' => $params['tag'] ?? null,
+ 'segment' => $params['segment'] ?? null,
+ 'itm_source' => $params['itm_source'] ?? null,
+ )
+ );
+
+ if ( is_wp_error( $analytics_request ) ) {
+ return $analytics_request;
+ }
+
+ // Process the data.
+ $posts = array();
+
+ /**
+ * The analytics data object.
+ *
+ * @var array> $analytics_request
+ */
+ foreach ( $analytics_request as $item ) {
+ $posts[] = $this->extract_post_data( $item );
+ }
+
+ $response_data = array(
+ 'params' => $params,
+ 'data' => $posts,
+ );
+
+ return new WP_REST_Response( $response_data, 200 );
+ }
+}
diff --git a/src/rest-api/stats/class-endpoint-related.php b/src/rest-api/stats/class-endpoint-related.php
new file mode 100644
index 000000000..1f4526c8f
--- /dev/null
+++ b/src/rest-api/stats/class-endpoint-related.php
@@ -0,0 +1,121 @@
+content_api = $this->parsely->get_content_api();
+ }
+
+ /**
+ * Returns the endpoint name.
+ *
+ * @since 3.17.0
+ *
+ * @return string
+ */
+ public static function get_endpoint_name(): string {
+ return 'related';
+ }
+
+ /**
+ * Registers the routes for the endpoint.
+ *
+ * @since 3.17.0
+ */
+ public function register_routes(): void {
+ /**
+ * GET /related
+ * Gets related posts.
+ */
+ $this->register_rest_route(
+ '/',
+ array( 'GET' ),
+ array( $this, 'get_related_posts' ),
+ array(
+ 'url' => array(
+ 'description' => __( 'The URL of the post.', 'wp-parsely' ),
+ 'type' => 'string',
+ 'required' => true,
+ ),
+ $this->get_related_posts_param_args(),
+ )
+ );
+ }
+
+ /**
+ * API Endpoint: GET /stats/related
+ *
+ * Gets related posts for a given URL.
+ *
+ * @since 3.17.0
+ *
+ * @param WP_REST_Request $request The request object.
+ * @return WP_REST_Response|WP_Error
+ */
+ public function get_related_posts( WP_REST_Request $request ) {
+ $url = $request->get_param( 'url' );
+
+ $related_posts = $this->get_related_posts_of_url( $request, $url );
+
+ if ( is_wp_error( $related_posts ) ) {
+ return $related_posts;
+ }
+
+ return new WP_REST_Response( array( 'data' => $related_posts ), 200 );
+ }
+
+ /**
+ * Returns whether the endpoint is available for access by the current
+ * user.
+ *
+ * @since 3.17.0
+ *
+ * @param WP_REST_Request|null $request The request object.
+ * @return bool|WP_Error
+ */
+ public function is_available_to_current_user( ?WP_REST_Request $request = null ) {
+ return $this->validate_site_id_and_secret( false );
+ }
+}
diff --git a/src/rest-api/stats/class-stats-controller.php b/src/rest-api/stats/class-stats-controller.php
new file mode 100644
index 000000000..c1981984d
--- /dev/null
+++ b/src/rest-api/stats/class-stats-controller.php
@@ -0,0 +1,48 @@
+register_endpoints( $endpoints );
+ }
+}
diff --git a/src/rest-api/stats/trait-post-data.php b/src/rest-api/stats/trait-post-data.php
new file mode 100644
index 000000000..374333c63
--- /dev/null
+++ b/src/rest-api/stats/trait-post-data.php
@@ -0,0 +1,146 @@
+itm_source = $source;
+ }
+
+ /**
+ * Sets the itm_source value from the request, if it exists.
+ *
+ * @since 3.17.0
+ *
+ * @param WP_REST_Request $request The request object.
+ */
+ private function set_itm_source_from_request( WP_REST_Request $request ): void {
+ $source = $request->get_param( 'itm_source' );
+ if ( null !== $source ) {
+ $this->set_itm_source( $source );
+ }
+ }
+
+ /**
+ * Returns the itm_source parameter arguments, to be used in the REST API
+ * route registration.
+ *
+ * @since 3.17.0
+ *
+ * @return array
+ */
+ private function get_itm_source_param_args(): array {
+ return array(
+ 'itm_source' => array(
+ 'description' => __( 'The source of the item.', 'wp-parsely' ),
+ 'type' => 'string',
+ 'required' => false,
+ ),
+ );
+ }
+
+ /**
+ * Extracts the post data from the passed object.
+ *
+ * Should only be used with endpoints that return post data.
+ *
+ * @since 3.10.0
+ * @since 3.17.0 Moved from the `Base_API_Proxy` class.
+ *
+ * @param array $item The object to extract the data from.
+ * @return array The extracted data.
+ */
+ protected function extract_post_data( array $item ): array {
+ $data = array();
+
+ if ( isset( $item['author'] ) ) {
+ $data['author'] = $item['author'];
+ }
+
+ if ( isset( $item['metrics']['views'] ) ) {
+ $data['views'] = number_format_i18n( $item['metrics']['views'] );
+ }
+
+ if ( isset( $item['metrics']['visitors'] ) ) {
+ $data['visitors'] = number_format_i18n( $item['metrics']['visitors'] );
+ }
+
+ // The avg_engaged metric can be in different locations depending on the
+ // endpoint and passed sort/url parameters.
+ $avg_engaged = $item['metrics']['avg_engaged'] ?? $item['avg_engaged'] ?? null;
+ if ( null !== $avg_engaged ) {
+ $data['avgEngaged'] = Utils::get_formatted_duration( (float) $avg_engaged );
+ }
+
+ if ( isset( $item['pub_date'] ) ) {
+ $data['date'] = wp_date( Utils::get_date_format(), strtotime( $item['pub_date'] ) );
+ }
+
+ if ( isset( $item['title'] ) ) {
+ $data['title'] = $item['title'];
+ }
+
+ if ( isset( $item['url'] ) ) {
+ $site_id = $this->parsely->get_site_id();
+ // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.url_to_postid_url_to_postid
+ $post_id = url_to_postid( $item['url'] ); // 0 if the post cannot be found.
+
+ $post_url = Parsely::get_url_with_itm_source( $item['url'], null );
+ if ( Utils::parsely_is_https_supported() ) {
+ $post_url = str_replace( 'http://', 'https://', $post_url );
+ }
+
+ $data['rawUrl'] = $post_url;
+ $data['dashUrl'] = Parsely::get_dash_url( $site_id, $post_url );
+ $data['id'] = Parsely::get_url_with_itm_source( $post_url, null ); // Unique.
+ $data['postId'] = $post_id; // Might not be unique.
+ $data['url'] = Parsely::get_url_with_itm_source( $post_url, $this->itm_source );
+
+ // Set thumbnail URL, falling back to the Parse.ly thumbnail if needed.
+ $thumbnail_url = get_the_post_thumbnail_url( $post_id, 'thumbnail' );
+ if ( false !== $thumbnail_url ) {
+ $data['thumbnailUrl'] = $thumbnail_url;
+ } elseif ( isset( $item['thumb_url_medium'] ) ) {
+ $data['thumbnailUrl'] = $item['thumb_url_medium'];
+ }
+ }
+
+ return $data;
+ }
+}
diff --git a/src/rest-api/stats/trait-related-posts.php b/src/rest-api/stats/trait-related-posts.php
new file mode 100644
index 000000000..c165388de
--- /dev/null
+++ b/src/rest-api/stats/trait-related-posts.php
@@ -0,0 +1,155 @@
+
+ */
+ private function get_related_posts_param_args(): array {
+ return array_merge(
+ array(
+ 'sort' => array(
+ 'description' => __( 'The sort order.', 'wp-parsely' ),
+ 'type' => 'string',
+ 'enum' => array( '_score', 'pub_date' ),
+ 'required' => false,
+ 'default' => '_score',
+ ),
+ 'limit' => array(
+ 'description' => __( 'The number of related posts to return.', 'wp-parsely' ),
+ 'type' => 'integer',
+ 'required' => false,
+ 'default' => 10,
+ ),
+ 'pub_date_start' => array(
+ 'description' => __( 'The start of the publication date.', 'wp-parsely' ),
+ 'type' => 'string',
+ 'required' => false,
+ ),
+ 'pub_date_end' => array(
+ 'description' => __( 'The end of the publication date.', 'wp-parsely' ),
+ 'type' => 'string',
+ 'required' => false,
+ ),
+ 'page' => array(
+ 'description' => __( 'The page number.', 'wp-parsely' ),
+ 'type' => 'integer',
+ 'required' => false,
+ 'default' => 1,
+ ),
+ 'section' => array(
+ 'description' => __( 'The section of the post.', 'wp-parsely' ),
+ 'type' => 'string',
+ 'required' => false,
+ ),
+ 'tag' => array(
+ 'description' => __( 'The tag of the post.', 'wp-parsely' ),
+ 'type' => 'string',
+ 'required' => false,
+ ),
+ 'author' => array(
+ 'description' => __( 'The author of the post.', 'wp-parsely' ),
+ 'type' => 'string',
+ 'required' => false,
+ ),
+ ),
+ $this->get_itm_source_param_args()
+ );
+ }
+
+ /**
+ * Get related posts for a given URL.
+ *
+ * @since 3.17.0
+ *
+ * @param WP_REST_Request $request The request object.
+ * @param string $url The URL to get related posts for.
+ * @return array|WP_Error
+ */
+ public function get_related_posts_of_url( WP_REST_Request $request, string $url ) {
+ // Set the itm_source parameter.
+ $this->set_itm_source_from_request( $request );
+
+ /**
+ * The raw related posts data, received by the API.
+ *
+ * @var array>|WP_Error $related_posts_request
+ */
+ $related_posts_request = $this->content_api->get_related_posts_with_url(
+ $url,
+ array(
+ 'url' => $url,
+ 'sort' => $request->get_param( 'sort' ),
+ 'limit' => $request->get_param( 'limit' ),
+ 'pub_date_start' => $request->get_param( 'pub_date_start' ),
+ 'pub_date_end' => $request->get_param( 'pub_date_end' ),
+ 'page' => $request->get_param( 'page' ),
+ 'section' => $request->get_param( 'section' ),
+ 'tag' => $request->get_param( 'tag' ),
+ 'author' => $request->get_param( 'author' ),
+ )
+ );
+
+ if ( is_wp_error( $related_posts_request ) ) {
+ return $related_posts_request;
+ }
+
+ $itm_source = $this->itm_source;
+
+ $related_posts = array_map(
+ static function ( array $item ) use ( $itm_source ) {
+ return array(
+ 'image_url' => $item['image_url'],
+ 'thumb_url_medium' => $item['thumb_url_medium'],
+ 'title' => $item['title'],
+ 'url' => Parsely::get_url_with_itm_source( $item['url'], $itm_source ),
+ );
+ },
+ $related_posts_request
+ );
+
+ return $related_posts;
+ }
+}
diff --git a/src/rest-api/trait-use-post-id-parameter.php b/src/rest-api/trait-use-post-id-parameter.php
new file mode 100644
index 000000000..39047a2f7
--- /dev/null
+++ b/src/rest-api/trait-use-post-id-parameter.php
@@ -0,0 +1,89 @@
+ $methods The HTTP methods.
+ * @param callable $callback The callback function.
+ * @param array $args The route arguments.
+ */
+ public function register_rest_route_with_post_id(
+ string $route,
+ array $methods,
+ callable $callback,
+ array $args = array()
+ ): void {
+ // Append the post_id parameter to the route.
+ $route = '/(?P\d+)/' . trim( $route, '/' );
+
+ // Add the post_id parameter to the args.
+ $args = array_merge(
+ $args,
+ array(
+ 'post_id' => array(
+ 'description' => __( 'The ID of the post.', 'wp-parsely' ),
+ 'type' => 'integer',
+ 'required' => true,
+ 'validate_callback' => array( $this, 'validate_post_id' ),
+ ),
+ )
+ );
+
+ // Register the route.
+ $this->register_rest_route( $route, $methods, $callback, $args );
+ }
+
+ /**
+ * Validates the post ID parameter.
+ *
+ * The callback sets the post object in the request object if the parameter is valid.
+ *
+ * @since 3.16.0
+ * @since 3.17.0 Moved from the `Smart_Linking_Endpoint` class.
+ * @access private
+ *
+ * @param string $param The parameter value.
+ * @param WP_REST_Request $request The request object.
+ * @return bool Whether the parameter is valid.
+ */
+ public function validate_post_id( string $param, WP_REST_Request $request ): bool {
+ if ( ! is_numeric( $param ) ) {
+ return false;
+ }
+
+ $param = filter_var( $param, FILTER_VALIDATE_INT );
+
+ if ( false === $param ) {
+ return false;
+ }
+
+ // Validate if the post ID exists.
+ $post = get_post( $param );
+
+ // Set the post attribute in the request.
+ $request->set_param( 'post', $post );
+
+ return null !== $post;
+ }
+}
diff --git a/src/services/class-base-api-service.php b/src/services/class-base-api-service.php
new file mode 100644
index 000000000..4a1697e68
--- /dev/null
+++ b/src/services/class-base-api-service.php
@@ -0,0 +1,133 @@
+
+ */
+ protected $endpoints;
+
+ /**
+ * The Parsely instance.
+ *
+ * @since 3.17.0
+ *
+ * @var Parsely
+ */
+ private $parsely;
+
+
+ /**
+ * Initializes the class.
+ *
+ * @since 3.17.0
+ *
+ * @param Parsely $parsely The Parsely instance.
+ */
+ public function __construct( Parsely $parsely ) {
+ $this->parsely = $parsely;
+ $this->register_endpoints();
+ }
+
+ /**
+ * Registers an endpoint with the service.
+ *
+ * @since 3.17.0
+ *
+ * @param Base_Service_Endpoint $endpoint The endpoint to register.
+ */
+ protected function register_endpoint( Base_Service_Endpoint $endpoint ): void {
+ $this->endpoints[ $endpoint->get_endpoint() ] = $endpoint;
+ }
+
+ /**
+ * Registers a cached endpoint with the service.
+ *
+ * @since 3.17.0
+ *
+ * @param Base_Service_Endpoint $endpoint The endpoint to register.
+ * @param int $ttl The time-to-live for the cache, in seconds.
+ */
+ protected function register_cached_endpoint( Base_Service_Endpoint $endpoint, int $ttl ): void {
+ $this->endpoints[ $endpoint->get_endpoint() ] = new Cached_Service_Endpoint( $endpoint, $ttl );
+ }
+
+ /**
+ * Gets an endpoint by name.
+ *
+ * @since 3.17.0
+ *
+ * @param string $endpoint The name of the endpoint.
+ * @return Base_Service_Endpoint The endpoint.
+ */
+ public function get_endpoint( string $endpoint ): Base_Service_Endpoint {
+ return $this->endpoints[ $endpoint ];
+ }
+
+ /**
+ * Returns the base URL for the API service.
+ *
+ * This method should be overridden in the child class to return the base URL
+ * for the API service.
+ *
+ * @since 3.17.0
+ *
+ * @return string
+ */
+ abstract public static function get_base_url(): string;
+
+ /**
+ * Registers the endpoints for the service.
+ *
+ * This method should be overridden in the child class to register the
+ * endpoints for the service.
+ *
+ * @since 3.17.0
+ */
+ abstract protected function register_endpoints(): void;
+
+ /**
+ * Returns the API URL for the service.
+ *
+ * @since 3.17.0
+ *
+ * @return string
+ */
+ public function get_api_url(): string {
+ return static::get_base_url();
+ }
+
+ /**
+ * Returns the Parsely instance.
+ *
+ * @since 3.17.0
+ *
+ * @return Parsely
+ */
+ public function get_parsely(): Parsely {
+ return $this->parsely;
+ }
+}
diff --git a/src/services/class-base-service-endpoint.php b/src/services/class-base-service-endpoint.php
new file mode 100644
index 000000000..2077c5778
--- /dev/null
+++ b/src/services/class-base-service-endpoint.php
@@ -0,0 +1,260 @@
+,
+ * body: string,
+ * response: array{
+ * code: int|false,
+ * message: string|false,
+ * },
+ * cookies: array,
+ * http_response: \WP_HTTP_Requests_Response|null,
+ * }
+ *
+ * @phpstan-import-type WP_HTTP_Request_Args from Parsely
+ */
+abstract class Base_Service_Endpoint {
+ /**
+ * The API service that this endpoint belongs to.
+ *
+ * @since 3.17.0
+ *
+ * @var Base_API_Service
+ */
+ protected $api_service;
+
+ /**
+ * Flag to truncate the content of the request body.
+ *
+ * If set to true, the content of the request body will be truncated to a maximum length.
+ *
+ * @since 3.14.1
+ * @since 3.17.0 Moved to the Base_Service_Endpoint class.
+ *
+ * @var bool
+ */
+ protected const TRUNCATE_CONTENT = false;
+
+ /**
+ * The maximum length of the content of the request body.
+ *
+ * @since 3.14.1
+ * @since 3.17.0 Moved to the Base_Service_Endpoint class.
+ *
+ * @var int
+ */
+ protected const TRUNCATE_CONTENT_LENGTH = 25000;
+
+ /**
+ * Initializes the class.
+ *
+ * @since 3.17.0
+ *
+ * @param Base_API_Service $api_service The API service that this endpoint belongs to.
+ */
+ public function __construct( Base_API_Service $api_service ) {
+ $this->api_service = $api_service;
+ }
+
+ /**
+ * Returns the headers to send with the request.
+ *
+ * @since 3.17.0
+ *
+ * @return array The headers to send with the request.
+ */
+ protected function get_headers(): array {
+ return array(
+ 'Content-Type' => 'application/json',
+ );
+ }
+
+ /**
+ * Returns the request options for the remote API request.
+ *
+ * @since 3.17.0
+ *
+ * @param string $method The HTTP method to use for the request.
+ * @return WP_HTTP_Request_Args The request options for the remote API request.
+ */
+ protected function get_request_options( string $method ): array {
+ $options = array(
+ 'headers' => $this->get_headers(),
+ 'method' => $method,
+ );
+
+ return $options;
+ }
+
+ /**
+ * Returns the common query arguments to send to the remote API.
+ *
+ * This can be used for setting common query arguments that are shared
+ * across multiple endpoints, such as the API key.
+ *
+ * @since 3.17.0
+ *
+ * @param array $args Additional query arguments to send to the remote API.
+ * @return array The query arguments to send to the remote API.
+ */
+ protected function get_query_args( array $args = array() ): array {
+ return $args;
+ }
+
+ /**
+ * Executes the API request.
+ *
+ * @since 3.17.0
+ *
+ * @param array $args The arguments to pass to the API request.
+ * @return WP_Error|array The response from the API.
+ */
+ abstract public function call( array $args = array() );
+
+ /**
+ * Returns the endpoint for the API request.
+ *
+ * This should be the path to the endpoint, not the full URL.
+ * Override this method in the child class to return the endpoint.
+ *
+ * @since 3.17.0
+ *
+ * @return string The endpoint for the API request.
+ */
+ abstract public function get_endpoint(): string;
+
+ /**
+ * Returns the full URL for the API request, including the endpoint and query arguments.
+ *
+ * @since 3.17.0
+ *
+ * @param array $query_args The query arguments to send to the remote API.
+ * @return string The full URL for the API request.
+ */
+ public function get_endpoint_url( array $query_args = array() ): string {
+ // Get the base URL from the API service.
+ $base_url = $this->api_service->get_api_url();
+
+ // Append the endpoint to the base URL.
+ $base_url .= $this->get_endpoint();
+
+ // Append any necessary query arguments.
+ $endpoint = add_query_arg( $this->get_query_args( $query_args ), $base_url );
+
+ return $endpoint;
+ }
+
+ /**
+ * Sends a request to the remote API.
+ *
+ * @since 3.17.0
+ *
+ * @param string $method The HTTP method to use for the request.
+ * @param array $query_args The query arguments to send to the remote API.
+ * @param array $data The data to send in the request body.
+ * @return WP_Error|array The response from the remote API.
+ */
+ protected function request( string $method, array $query_args = array(), array $data = array() ) {
+ // Get the URL to send the request to.
+ $request_url = $this->get_endpoint_url( $query_args );
+
+ // Build the request options.
+ $request_options = $this->get_request_options( $method );
+
+ if ( count( $data ) > 0 ) {
+ if ( true === static::TRUNCATE_CONTENT ) {
+ $data = $this->truncate_array_content( $data );
+ }
+
+ $request_options['body'] = wp_json_encode( $data );
+ if ( false === $request_options['body'] ) {
+ return new WP_Error( 400, __( 'Unable to encode request body', 'wp-parsely' ) );
+ }
+ }
+
+ /** @var WP_HTTP_Response|WP_Error $response */
+ $response = wp_safe_remote_request( $request_url, $request_options );
+
+ return $this->process_response( $response );
+ }
+
+ /**
+ * Processes the response from the remote API.
+ *
+ * @since 3.17.0
+ *
+ * @param WP_HTTP_Response|WP_Error $response The response from the remote API.
+ * @return array|WP_Error The processed response.
+ */
+ protected function process_response( $response ) {
+ if ( is_wp_error( $response ) ) {
+ /** @var WP_Error $response */
+ return $response;
+ }
+
+ $body = wp_remote_retrieve_body( $response );
+ $decoded = json_decode( $body, true );
+
+ if ( ! is_array( $decoded ) ) {
+ return new WP_Error( 400, __( 'Unable to decode upstream API response', 'wp-parsely' ) );
+ }
+
+ return $decoded;
+ }
+
+ /**
+ * Returns the Parsely instance.
+ *
+ * @since 3.17.0
+ *
+ * @return Parsely The Parsely instance.
+ */
+ public function get_parsely(): Parsely {
+ return $this->api_service->get_parsely();
+ }
+
+ /**
+ * Truncates the content of an array to a maximum length.
+ *
+ * @since 3.14.1
+ * @since 3.17.0 Moved to the Base_Service_Endpoint class.
+ *
+ * @param string|array|mixed $content The content to truncate.
+ * @return string|array|mixed The truncated content.
+ */
+ private function truncate_array_content( $content ) {
+ if ( is_array( $content ) ) {
+ // If the content is an array, iterate over its elements.
+ foreach ( $content as $key => $value ) {
+ // Recursively process/truncate each element of the array.
+ $content[ $key ] = $this->truncate_array_content( $value );
+ }
+ return $content;
+ } elseif ( is_string( $content ) ) {
+ // Check if the string length exceeds the maximum and truncate if necessary.
+ if ( mb_strlen( $content ) > self::TRUNCATE_CONTENT_LENGTH ) {
+ return mb_substr( $content, 0, self::TRUNCATE_CONTENT_LENGTH );
+ }
+ return $content;
+ }
+ return $content;
+ }
+}
diff --git a/src/services/class-cached-service-endpoint.php b/src/services/class-cached-service-endpoint.php
new file mode 100644
index 000000000..debdb63d9
--- /dev/null
+++ b/src/services/class-cached-service-endpoint.php
@@ -0,0 +1,165 @@
+service_endpoint = $service_endpoint;
+ $this->cache_ttl = $cache_ttl;
+
+ parent::__construct( $service_endpoint->api_service );
+ }
+
+ /**
+ * Returns the cache key for the API request.
+ *
+ * @since 3.17.0
+ *
+ * @param array $args The arguments to pass to the API request.
+ * @return string The cache key for the API request.
+ */
+ private function get_cache_key( array $args ): string {
+ $api_service = $this->service_endpoint->api_service;
+
+ $cache_key = 'parsely_api_' .
+ wp_hash( $api_service->get_api_url() ) . '_' .
+ wp_hash( $this->get_endpoint() ) . '_' .
+ wp_hash( (string) wp_json_encode( $args ) );
+
+ return $cache_key;
+ }
+
+ /**
+ * Executes the API request, caching the response.
+ *
+ * If the response is already cached, it will be returned from the cache,
+ * otherwise the API request will be made and the response will be cached.
+ *
+ * @since 3.17.0
+ *
+ * @param array $args The arguments to pass to the API request.
+ * @return WP_Error|array The response from the API.
+ */
+ public function call( array $args = array() ) {
+ $cache_key = $this->get_cache_key( $args );
+ $cache = wp_cache_get( $cache_key, self::CACHE_GROUP );
+
+ if ( false !== $cache ) {
+ // @phpstan-ignore-next-line
+ return $cache;
+ }
+
+ $response = $this->service_endpoint->call( $args );
+
+ if ( ! is_wp_error( $response ) ) {
+ wp_cache_set( $cache_key, $response, self::CACHE_GROUP, $this->cache_ttl ); // phpcs:ignore
+ }
+
+ return $response;
+ }
+
+ /**
+ * Returns the endpoint for the API request.
+ *
+ * @since 3.17.0
+ *
+ * @return string The endpoint for the API request.
+ */
+ public function get_endpoint(): string {
+ return $this->service_endpoint->get_endpoint();
+ }
+
+ /**
+ * Returns the uncached endpoint for the API request.
+ *
+ * @since 3.17.0
+ *
+ * @return Base_Service_Endpoint The uncached endpoint for the API request.
+ */
+ public function get_uncached_endpoint(): Base_Service_Endpoint {
+ return $this->service_endpoint;
+ }
+
+ /**
+ * Returns the request options for the remote API request.
+ *
+ * Gets the request options from the uncached service endpoint.
+ *
+ * @since 3.17.0
+ *
+ * @param string $method The HTTP method to use for the request.
+ * @return WP_HTTP_Request_Args The request options for the remote API request.
+ */
+ protected function get_request_options( string $method ): array {
+ return $this->service_endpoint->get_request_options( $method );
+ }
+
+ /**
+ * Returns the common query arguments to send to the remote API.
+ *
+ * Gets the query arguments from the uncached service endpoint.
+ *
+ * @since 3.17.0
+ *
+ * @param array $args Additional query arguments to send to the remote API.
+ * @return array The query arguments to send to the remote API.
+ */
+ protected function get_query_args( array $args = array() ): array {
+ return $this->service_endpoint->get_query_args( $args );
+ }
+}
diff --git a/src/services/content-api/class-content-api-service.php b/src/services/content-api/class-content-api-service.php
new file mode 100644
index 000000000..83ceeb825
--- /dev/null
+++ b/src/services/content-api/class-content-api-service.php
@@ -0,0 +1,234 @@
+ $endpoints
+ */
+ $endpoints = array(
+ new Endpoints\Endpoint_Validate( $this ),
+ );
+
+ foreach ( $endpoints as $endpoint ) {
+ $this->register_endpoint( $endpoint );
+ }
+
+ /**
+ * The cached endpoints.
+ *
+ * The second element in the array is the time-to-live for the cache, in seconds.
+ *
+ * @var array $cached_endpoints
+ */
+ $cached_endpoints = array(
+ array( new Endpoints\Endpoint_Analytics_Posts( $this ), 300 ), // 5 minutes.
+ array( new Endpoints\Endpoint_Related( $this ), 600 ), // 10 minutes.
+ array( new Endpoints\Endpoint_Referrers_Post_Detail( $this ), 300 ), // 5 minutes.
+ array( new Endpoints\Endpoint_Analytics_Post_Details( $this ), 300 ), // 5 minutes.
+ );
+
+ foreach ( $cached_endpoints as $cached_endpoint ) {
+ $this->register_cached_endpoint( $cached_endpoint[0], $cached_endpoint[1] );
+ }
+ }
+
+ /**
+ * Returns the post’s metadata, as well as total views and visitors in the metrics field.
+ *
+ * By default, this returns the total pageviews on the link for the last 90 days.
+ *
+ * @since 3.17.0
+ *
+ * @link https://docs.parse.ly/api-analytics-endpoint/#2-get-analytics-post-detail
+ *
+ * @param string $url The URL of the post.
+ * @param string|null $period_start The start date of the period to get the data for.
+ * @param string|null $period_end The end date of the period to get the data for.
+ * @return array|WP_Error Returns the post details or a WP_Error object in case of an error.
+ */
+ public function get_post_details(
+ string $url,
+ string $period_start = null,
+ string $period_end = null
+ ) {
+ /** @var Endpoints\Endpoint_Analytics_Post_Details $endpoint */
+ $endpoint = $this->get_endpoint( '/analytics/post/detail' );
+
+ $args = array(
+ 'url' => $url,
+ 'period_start' => $period_start,
+ 'period_end' => $period_end,
+ );
+
+ return $endpoint->call( $args );
+ }
+
+ /**
+ * Returns the referrers for a given post URL.
+ *
+ * @since 3.17.0
+ *
+ * @link https://docs.parse.ly/api-referrers-endpoint/#3-get-referrers-post-detail
+ *
+ * @param string $url The URL of the post.
+ * @param string|null $period_start The start date of the period to get the data for.
+ * @param string|null $period_end The end date of the period to get the data for.
+ * @return array|WP_Error Returns the referrers or a WP_Error object in case of an error.
+ */
+ public function get_post_referrers(
+ string $url,
+ string $period_start = null,
+ string $period_end = null
+ ) {
+ /** @var Endpoints\Endpoint_Referrers_Post_Detail $endpoint */
+ $endpoint = $this->get_endpoint( '/referrers/post/detail' );
+
+ $args = array(
+ 'url' => $url,
+ 'period_start' => $period_start,
+ 'period_end' => $period_end,
+ );
+
+ return $endpoint->call( $args );
+ }
+
+ /**
+ * Returns the related posts for a given URL.
+ *
+ * @since 3.17.0
+ *
+ * @link https://docs.parse.ly/content-recommendations/#h-get-related
+ *
+ * @param string $url The URL of the post.
+ * @param array $params The parameters to pass to the API request.
+ * @return array|WP_Error Returns the related posts or a WP_Error object in case of an error.
+ */
+ public function get_related_posts_with_url( string $url, array $params = array() ) {
+ /** @var Endpoints\Endpoint_Related $endpoint */
+ $endpoint = $this->get_endpoint( '/related' );
+
+ $args = array(
+ 'url' => $url,
+ );
+
+ // Merge the optional params.
+ $args = array_merge( $params, $args );
+
+ return $endpoint->call( $args );
+ }
+
+ /**
+ * Returns the related posts for a given UUID.
+ *
+ * @since 3.17.0
+ *
+ * @link https://docs.parse.ly/content-recommendations/#h-get-related
+ *
+ * @param string $uuid The UUID of the user.
+ * @param array $params The parameters to pass to the API request.
+ * @return array|WP_Error Returns the related posts or a WP_Error object in case of an error.
+ */
+ public function get_related_posts_with_uuid( string $uuid, array $params = array() ) {
+ /** @var Endpoints\Endpoint_Related $endpoint */
+ $endpoint = $this->get_endpoint( '/related' );
+
+ $args = array(
+ 'uuid' => $uuid,
+ );
+
+ // Merge the optional params.
+ $args = array_merge( $params, $args );
+
+ return $endpoint->call( $args );
+ }
+
+ /**
+ * Returns the posts analytics.
+ *
+ * @since 3.17.0
+ *
+ * @link https://docs.parse.ly/api-analytics-endpoint/#1-get-analytics-posts
+ *
+ * @param array $params The parameters to pass to the API request.
+ * @return array|WP_Error Returns the posts analytics or a WP_Error object in case of an error.
+ */
+ public function get_posts( array $params = array() ) {
+ /** @var Endpoints\Endpoint_Analytics_Posts $endpoint */
+ $endpoint = $this->get_endpoint( '/analytics/posts' );
+
+ return $endpoint->call( $params );
+ }
+
+ /**
+ * Validates the Parse.ly API credentials.
+ *
+ * The API will return a 200 response if the credentials are valid and a 401 response if they are not.
+ *
+ * @since 3.17.0
+ *
+ * @param string $api_key The API key to validate.
+ * @param string $secret_key The secret key to validate.
+ * @return bool|WP_Error Returns true if the credentials are valid, false otherwise.
+ */
+ public function validate_credentials( string $api_key, string $secret_key ) {
+ /** @var Endpoints\Endpoint_Validate $endpoint */
+ $endpoint = $this->get_endpoint( '/validate/secret' );
+
+ $args = array(
+ 'apikey' => $api_key,
+ 'secret' => $secret_key,
+ );
+
+ $response = $endpoint->call( $args );
+
+ if ( is_wp_error( $response ) ) {
+ /** @var WP_Error $response */
+ return $response;
+ }
+
+ if ( true === $response['success'] ) {
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/src/services/content-api/endpoints/class-content-api-base-endpoint.php b/src/services/content-api/endpoints/class-content-api-base-endpoint.php
new file mode 100644
index 000000000..4a4692fac
--- /dev/null
+++ b/src/services/content-api/endpoints/class-content-api-base-endpoint.php
@@ -0,0 +1,89 @@
+,
+ * }
+ *
+ * @phpstan-type Content_API_Error_Response array{
+ * code?: int,
+ * message?: string,
+ * }
+ *
+ * @phpstan-type Content_API_Response = Content_API_Valid_Response|Content_API_Error_Response
+ *
+ * @phpstan-import-type WP_HTTP_Response from Base_Service_Endpoint
+ */
+abstract class Content_API_Base_Endpoint extends Base_Service_Endpoint {
+ /**
+ * Returns the common query arguments to send to the remote API.
+ *
+ * This method append the API key and secret to the query arguments.
+ *
+ * @since 3.17.0
+ *
+ * @param array $args Additional query arguments to send to the remote API.
+ * @return array The query arguments to send to the remote API.
+ */
+ protected function get_query_args( array $args = array() ): array {
+ $query_args = parent::get_query_args( $args );
+
+ // Set up the API key and secret.
+ $query_args['apikey'] = $this->get_parsely()->get_site_id();
+ if ( $this->get_parsely()->api_secret_is_set() ) {
+ $query_args['secret'] = $this->get_parsely()->get_api_secret();
+ }
+
+ return $query_args;
+ }
+
+ /**
+ * Processes the response from the remote API.
+ *
+ * @since 3.17.0
+ *
+ * @param WP_HTTP_Response|WP_Error $response The response from the remote API.
+ * @return array|WP_Error The processed response.
+ */
+ protected function process_response( $response ) {
+ $response = parent::process_response( $response );
+
+ if ( is_wp_error( $response ) ) {
+ /** @var WP_Error $response */
+ return $response;
+ }
+
+ if ( ! isset( $response['data'] ) ) {
+ /** @var Content_API_Error_Response $response */
+ return new WP_Error(
+ $response['code'] ?? 400,
+ $response['message'] ?? __( 'Unable to read data from upstream API', 'wp-parsely' ),
+ array( 'status' => $response['code'] ?? 400 )
+ );
+ }
+
+ if ( ! is_array( $response['data'] ) ) {
+ return new WP_Error( 400, __( 'Unable to parse data from upstream API', 'wp-parsely' ) );
+ }
+
+ /** @var Content_API_Valid_Response $response */
+ return $response['data'];
+ }
+}
diff --git a/src/services/content-api/endpoints/class-endpoint-analytics-post-details.php b/src/services/content-api/endpoints/class-endpoint-analytics-post-details.php
new file mode 100644
index 000000000..54d26d3ae
--- /dev/null
+++ b/src/services/content-api/endpoints/class-endpoint-analytics-post-details.php
@@ -0,0 +1,54 @@
+ $args The arguments to pass to the API request.
+ * @return WP_Error|array The response from the API request.
+ */
+ public function call( array $args = array() ) {
+ $query_args = array(
+ 'url' => $args['url'],
+ 'period_start' => $args['period_start'],
+ 'period_end' => $args['period_end'],
+ );
+
+ // Filter out the empty values.
+ $query_args = array_filter( $query_args );
+
+ return $this->request( 'GET', $query_args );
+ }
+}
diff --git a/src/services/content-api/endpoints/class-endpoint-analytics-posts.php b/src/services/content-api/endpoints/class-endpoint-analytics-posts.php
new file mode 100644
index 000000000..b8ebc6168
--- /dev/null
+++ b/src/services/content-api/endpoints/class-endpoint-analytics-posts.php
@@ -0,0 +1,182 @@
+,
+ * }
+ *
+ * @phpstan-type Analytics_Post array{
+ * title?: string,
+ * url?: string,
+ * link?: string,
+ * author?: string,
+ * authors?: string[],
+ * section?: string,
+ * tags?: string[],
+ * metrics?: Analytics_Post_Metrics,
+ * full_content_word_count?: int,
+ * image_url?: string,
+ * metadata?: string,
+ * pub_date?: string,
+ * thumb_url_medium?: string,
+ * }
+ *
+ * @phpstan-type Analytics_Post_Metrics array{
+ * avg_engaged?: float,
+ * views?: int,
+ * visitors?: int,
+ * }
+ */
+class Endpoint_Analytics_Posts extends Content_API_Base_Endpoint {
+ private const MAX_RECORDS_LIMIT = 2000;
+ private const ANALYTICS_API_DAYS_LIMIT = 7;
+
+ /**
+ * Maximum limit for the number of records to return, to be
+ * used in the limit parameter.
+ *
+ * @since 3.17.0
+ *
+ * @var string
+ */
+ public const MAX_LIMIT = 'max';
+
+ /**
+ * Maximum period for the API request, to be used in the period_start parameter.
+ *
+ * @since 3.17.0
+ *
+ * @var string
+ */
+ public const MAX_PERIOD = 'max_days';
+
+ /**
+ * Returns the endpoint for the API request.
+ *
+ * @since 3.17.0
+ *
+ * @return string
+ */
+ public function get_endpoint(): string {
+ return '/analytics/posts';
+ }
+
+ /**
+ * Returns the endpoint URL for the API request.
+ *
+ * This method appends the author, tag, and section parameters to the
+ * endpoint URL, if they are set. Since the Parse.ly API needs a key for
+ * every value (e.g. tag=tag1&tag=tag2), we need to append them manually.
+ *
+ * @since 3.17.0
+ *
+ * @param array $query_args The arguments to pass to the API request.
+ * @return string The endpoint URL for the API request.
+ */
+ public function get_endpoint_url( array $query_args = array() ): string {
+ // Store the author, tag, and section parameters.
+ /** @var array $authors */
+ $authors = $query_args['author'] ?? array();
+
+ /** @var array $tags */
+ $tags = $query_args['tag'] ?? array();
+
+ /** @var array $sections */
+ $sections = $query_args['section'] ?? array();
+
+ // Remove the author, tag, and section parameters from the query args.
+ unset( $query_args['author'] );
+ unset( $query_args['tag'] );
+ unset( $query_args['section'] );
+
+ // Generate the endpoint URL.
+ $endpoint_url = parent::get_endpoint_url( $query_args );
+
+ // Append the author, tag, and section parameters to the endpoint URL.
+ $endpoint_url = $this->append_multiple_params_to_url( $endpoint_url, $authors, 'author' );
+ $endpoint_url = $this->append_multiple_params_to_url( $endpoint_url, $tags, 'tag' );
+ $endpoint_url = $this->append_multiple_params_to_url( $endpoint_url, $sections, 'section' );
+
+ return $endpoint_url;
+ }
+
+ /**
+ * Executes the API request.
+ *
+ * @since 3.17.0
+ *
+ * @param array $args The arguments to pass to the API request.
+ * @return WP_Error|array The response from the API request.
+ */
+ public function call( array $args = array() ) {
+ // Filter out the empty values.
+ $query_args = array_filter( $args );
+
+ // If the period_start is set to 'max_days', set it to the maximum days limit.
+ if ( isset( $query_args['period_start'] ) && self::MAX_PERIOD === $query_args['period_start'] ) {
+ $query_args['period_start'] = self::ANALYTICS_API_DAYS_LIMIT . 'd';
+ }
+
+ // If the limit is set to 'max' or greater than the maximum records limit,
+ // set it to the maximum records limit.
+ if ( isset( $query_args['limit'] ) && (
+ self::MAX_LIMIT === $query_args['limit'] || $query_args['limit'] > self::MAX_RECORDS_LIMIT )
+ ) {
+ $query_args['limit'] = self::MAX_RECORDS_LIMIT;
+ }
+
+ return $this->request( 'GET', $query_args );
+ }
+
+
+ /**
+ * Appends multiple parameters to the URL.
+ *
+ * This is required because the Parsely API requires the multiple values for the author, tag,
+ * and section parameters to share the same key.
+ *
+ * @since 3.17.0
+ *
+ * @param string $url The URL to append the parameters to.
+ * @param array $params The parameters to append.
+ * @param string $param_name The name of the parameter.
+ * @return string The URL with the appended parameters.
+ */
+ protected function append_multiple_params_to_url( string $url, array $params, string $param_name ): string {
+ foreach ( $params as $param ) {
+ $param = rawurlencode( $param );
+ if ( strpos( $url, $param_name . '=' ) === false ) {
+ $url = add_query_arg( $param_name, $param, $url );
+ } else {
+ $url .= '&' . $param_name . '=' . $param;
+ }
+ }
+
+ return $url;
+ }
+}
diff --git a/src/services/content-api/endpoints/class-endpoint-referrers-post-detail.php b/src/services/content-api/endpoints/class-endpoint-referrers-post-detail.php
new file mode 100644
index 000000000..1c233fedd
--- /dev/null
+++ b/src/services/content-api/endpoints/class-endpoint-referrers-post-detail.php
@@ -0,0 +1,54 @@
+ $args The arguments to pass to the API request.
+ * @return WP_Error|array The response from the API request.
+ */
+ public function call( array $args = array() ) {
+ $query_args = array(
+ 'url' => $args['url'],
+ 'period_start' => $args['period_start'],
+ 'period_end' => $args['period_end'],
+ );
+
+ // Filter out the empty values.
+ $query_args = array_filter( $query_args );
+
+ return $this->request( 'GET', $query_args );
+ }
+}
diff --git a/src/services/content-api/endpoints/class-endpoint-related.php b/src/services/content-api/endpoints/class-endpoint-related.php
new file mode 100644
index 000000000..cd547ae84
--- /dev/null
+++ b/src/services/content-api/endpoints/class-endpoint-related.php
@@ -0,0 +1,53 @@
+ $args The arguments to pass to the API request.
+ * @return WP_Error|array The response from the API request.
+ */
+ public function call( array $args = array() ) {
+ // Filter out the empty values.
+ $args = array_filter( $args );
+
+ // When the URL is provided, the UUID cannot be provided.
+ if ( isset( $args['uuid'] ) && isset( $args['url'] ) ) {
+ unset( $args['uuid'] );
+ }
+
+ return $this->request( 'GET', $args );
+ }
+}
diff --git a/src/services/content-api/endpoints/class-endpoint-validate.php b/src/services/content-api/endpoints/class-endpoint-validate.php
new file mode 100644
index 000000000..8167c837d
--- /dev/null
+++ b/src/services/content-api/endpoints/class-endpoint-validate.php
@@ -0,0 +1,112 @@
+ $args The query arguments to send to the remote API.
+ * @return array The query arguments for the API request.
+ */
+ public function get_query_args( array $args = array() ): array {
+ return $args;
+ }
+
+ /**
+ * Queries the Parse.ly API credentials validation endpoint.
+ *
+ * The API will return a 200 response if the credentials are valid and a 403
+ * response if they are not.
+ *
+ * @since 3.17.0
+ *
+ * @param string $api_key The API key to validate.
+ * @param string $secret_key The secret key to validate.
+ * @return array|WP_Error The response from the remote API, or a WP_Error object if the response is an error.
+ */
+ private function api_validate_credentials( string $api_key, string $secret_key ) {
+ $query = array(
+ 'apikey' => $api_key,
+ 'secret' => $secret_key,
+ );
+
+ $response = $this->request( 'GET', $query );
+
+ if ( is_wp_error( $response ) ) {
+ return $response;
+ }
+
+ if ( false === $response['success'] ) {
+ return new WP_Error(
+ $response['code'] ?? 403,
+ $response['message'] ?? __( 'Unable to validate the API credentials', 'wp-parsely' )
+ );
+ }
+
+ return $response;
+ }
+
+ /**
+ * Processes the response from the remote API.
+ *
+ * @since 3.17.0
+ *
+ * @param WP_HTTP_Response|WP_Error $response The response from the remote API.
+ * @return array|WP_Error The processed response.
+ */
+ protected function process_response( $response ) {
+ return Base_Service_Endpoint::process_response( $response );
+ }
+
+ /**
+ * Executes the API request.
+ *
+ * @since 3.17.0
+ *
+ * @param array $args The arguments to pass to the API request.
+ * @return WP_Error|array The response from the API request.
+ */
+ public function call( array $args = array() ) {
+ /** @var string $api_key */
+ $api_key = $args['apikey'];
+ /** @var string $secret */
+ $secret = $args['secret'];
+
+ return $this->api_validate_credentials( $api_key, $secret );
+ }
+}
diff --git a/src/services/suggestions-api/class-suggestions-api-service.php b/src/services/suggestions-api/class-suggestions-api-service.php
new file mode 100644
index 000000000..8f7245467
--- /dev/null
+++ b/src/services/suggestions-api/class-suggestions-api-service.php
@@ -0,0 +1,115 @@
+register_endpoint( $endpoint );
+ }
+ }
+
+ /**
+ * Gets the first brief (meta description) for a given content using the
+ * Parse.ly Content Suggestion API.
+ *
+ * @since 3.13.0
+ * @since 3.17.0 Updated to use the new API service.
+ *
+ * @param string $title The title of the content.
+ * @param string $content The query arguments to send to the remote API.
+ * @param Endpoint_Suggest_Brief_Options $options The options to pass to the API request.
+ * @return array|WP_Error The response from the remote API, or a WP_Error
+ * object if the response is an error.
+ */
+ public function get_brief_suggestions( string $title, string $content, $options = array() ) {
+ /** @var Endpoints\Endpoint_Suggest_Brief $endpoint */
+ $endpoint = $this->get_endpoint( '/suggest-brief' );
+
+ return $endpoint->get_suggestion( $title, $content, $options );
+ }
+
+ /**
+ * Gets titles (headlines) for a given content using the Parse.ly Content
+ * Suggestion API.
+ *
+ * @since 3.13.0
+ * @since 3.17.0 Updated to use the new API service.
+ *
+ * @param string $content The query arguments to send to the remote API.
+ * @param Endpoint_Suggest_Headline_Options $options The options to pass to the API request.
+ * @return array|WP_Error The response from the remote API, or a WP_Error
+ * object if the response is an error.
+ */
+ public function get_title_suggestions( string $content, $options = array() ) {
+ /** @var Endpoints\Endpoint_Suggest_Headline $endpoint */
+ $endpoint = $this->get_endpoint( '/suggest-headline' );
+
+ return $endpoint->get_headlines( $content, $options );
+ }
+
+ /**
+ * Gets suggested smart links for the given content.
+ *
+ * @since 3.14.0
+ * @since 3.17.0 Updated to use the new API service.
+ *
+ * @param string $content The content to generate links for.
+ * @param Endpoint_Suggest_Linked_Reference_Options $options The options to pass to the API request.
+ * @param array $url_exclusion_list A list of URLs to exclude from the suggestions.
+ *
+ * @return array|WP_Error The response from the remote API, or a WP_Error
+ * object if the response is an error.
+ */
+ public function get_smart_links( string $content, $options = array(), array $url_exclusion_list = array() ) {
+ /** @var Endpoints\Endpoint_Suggest_Linked_Reference $endpoint */
+ $endpoint = $this->get_endpoint( '/suggest-linked-reference' );
+
+ return $endpoint->get_links( $content, $options, $url_exclusion_list );
+ }
+}
diff --git a/src/services/suggestions-api/endpoints/class-endpoint-suggest-brief.php b/src/services/suggestions-api/endpoints/class-endpoint-suggest-brief.php
new file mode 100644
index 000000000..8e0d3a20c
--- /dev/null
+++ b/src/services/suggestions-api/endpoints/class-endpoint-suggest-brief.php
@@ -0,0 +1,93 @@
+|WP_Error The response from the remote API, or a WP_Error
+ * object if the response is an error.
+ */
+ public function get_suggestion(
+ string $title,
+ string $content,
+ $options = array()
+ ) {
+ $request_body = array(
+ 'output_config' => array(
+ 'persona' => $options['persona'] ?? 'journalist',
+ 'style' => $options['style'] ?? 'neutral',
+ 'max_characters' => $options['max_characters'] ?? 160,
+ 'max_items' => $options['max_items'] ?? 1,
+ ),
+ 'title' => $title,
+ 'text' => wp_strip_all_tags( $content ),
+ );
+
+ /** @var array|WP_Error $response */
+ $response = $this->request( 'POST', array(), $request_body );
+
+ return $response;
+ }
+
+ /**
+ * Executes the API request.
+ *
+ * @since 3.17.0
+ *
+ * @param array $args The arguments to pass to the API request.
+ * @return WP_Error|array The response from the API.
+ */
+ public function call( array $args = array() ) {
+ /** @var string $title */
+ $title = $args['title'] ?? '';
+ /** @var string $content */
+ $content = $args['content'] ?? '';
+ /** @var Endpoint_Suggest_Brief_Options $options */
+ $options = $args['options'] ?? array();
+
+ return $this->get_suggestion( $title, $content, $options );
+ }
+}
diff --git a/src/services/suggestions-api/endpoints/class-endpoint-suggest-headline.php b/src/services/suggestions-api/endpoints/class-endpoint-suggest-headline.php
new file mode 100644
index 000000000..09b3e7c27
--- /dev/null
+++ b/src/services/suggestions-api/endpoints/class-endpoint-suggest-headline.php
@@ -0,0 +1,96 @@
+
+ * }
+ */
+class Endpoint_Suggest_Headline extends Suggestions_API_Base_Endpoint {
+ /**
+ * Returns the endpoint for the API request.
+ *
+ * @since 3.17.0
+ *
+ * @return string The endpoint for the API request.
+ */
+ public function get_endpoint(): string {
+ return '/suggest-headline';
+ }
+
+ /**
+ * Gets titles (headlines) for a given content using the Parse.ly Content
+ * Suggestion API.
+ *
+ * @since 3.13.0
+ * @since 3.17.0 Updated to use the new API service.
+ *
+ * @param string $content The query arguments to send to the remote API.
+ * @param Endpoint_Suggest_Headline_Options $options The options to pass to the API request.
+ * @return array|WP_Error The response from the remote API, or a WP_Error
+ * object if the response is an error.
+ */
+ public function get_headlines(
+ string $content,
+ $options = array()
+ ) {
+ $request_body = array(
+ 'output_config' => array(
+ 'persona' => $options['persona'] ?? 'journalist',
+ 'style' => $options['style'] ?? 'neutral',
+ 'max_items' => $options['max_items'] ?? 1,
+ ),
+ 'text' => wp_strip_all_tags( $content ),
+ );
+
+ /** @var array|WP_Error $response */
+ $response = $this->request( 'POST', array(), $request_body );
+
+ return $response;
+ }
+
+ /**
+ * Executes the API request.
+ *
+ * @since 3.17.0
+ *
+ * @param array $args The arguments to pass to the API request.
+ * @return WP_Error|array The response from the API.
+ */
+ public function call( array $args = array() ) {
+ /** @var string $title */
+ $title = $args['title'] ?? '';
+ /** @var string $content */
+ $content = $args['content'] ?? '';
+ /** @var Endpoint_Suggest_Headline_Options $options */
+ $options = $args['options'] ?? array();
+
+ return $this->get_headlines( $content, $options );
+ }
+}
diff --git a/src/services/suggestions-api/endpoints/class-endpoint-suggest-linked-reference.php b/src/services/suggestions-api/endpoints/class-endpoint-suggest-linked-reference.php
new file mode 100644
index 000000000..cb27e628b
--- /dev/null
+++ b/src/services/suggestions-api/endpoints/class-endpoint-suggest-linked-reference.php
@@ -0,0 +1,117 @@
+
+ * }
+ */
+class Endpoint_Suggest_Linked_Reference extends Suggestions_API_Base_Endpoint {
+ /**
+ * Returns the endpoint for the API request.
+ *
+ * @since 3.17.0
+ *
+ * @return string The endpoint for the API request.
+ */
+ public function get_endpoint(): string {
+ return '/suggest-linked-reference';
+ }
+
+ /**
+ * Gets suggested smart links for the given content using the Parse.ly
+ * Content Suggestion API.
+ *
+ * @since 3.14.0
+ * @since 3.17.0 Updated to use the new API service.
+ *
+ * @param string $content The content to generate links for.
+ * @param Endpoint_Suggest_Linked_Reference_Options $options The options to pass to the API request.
+ * @param array $url_exclusion_list A list of URLs to exclude from the suggestions.
+ * @return array|WP_Error The response from the remote API, or a WP_Error
+ * object if the response is an error.
+ */
+ public function get_links(
+ string $content,
+ $options = array(),
+ array $url_exclusion_list = array()
+ ) {
+ $request_body = array(
+ 'output_config' => array(
+ 'max_link_words' => $options['max_link_words'] ?? 4,
+ 'max_items' => $options['max_items'] ?? 10,
+ ),
+ 'text' => wp_strip_all_tags( $content ),
+ );
+
+ if ( count( $url_exclusion_list ) > 0 ) {
+ $request_body['url_exclusion_list'] = $url_exclusion_list;
+ }
+
+ $response = $this->request( 'POST', array(), $request_body );
+
+ if ( is_wp_error( $response ) ) {
+ return $response;
+ }
+
+ // Convert the links to Smart_Link objects.
+ $links = array();
+ foreach ( $response as $link ) {
+ $link = apply_filters( 'wp_parsely_suggest_linked_reference_link', $link );
+ $link_obj = new Smart_Link(
+ esc_url( $link['canonical_url'] ),
+ esc_attr( $link['title'] ),
+ wp_kses_post( $link['text'] ),
+ $link['offset']
+ );
+ $links[] = $link_obj;
+ }
+
+ return $links;
+ }
+
+ /**
+ * Executes the API request.
+ *
+ * @since 3.17.0
+ *
+ * @param array $args The arguments to pass to the API request.
+ * @return WP_Error|array The response from the API.
+ */
+ public function call( array $args = array() ) {
+ /** @var string $content */
+ $content = $args['content'] ?? '';
+ /** @var Endpoint_Suggest_Linked_Reference_Options $options */
+ $options = $args['options'] ?? array();
+ /** @var string[] $url_exclusion_list */
+ $url_exclusion_list = $args['url_exclusion_list'] ?? array();
+
+ return $this->get_links( $content, $options, $url_exclusion_list );
+ }
+}
diff --git a/src/services/suggestions-api/endpoints/class-suggestions-api-base-endpoint.php b/src/services/suggestions-api/endpoints/class-suggestions-api-base-endpoint.php
new file mode 100644
index 000000000..79c7e2cb3
--- /dev/null
+++ b/src/services/suggestions-api/endpoints/class-suggestions-api-base-endpoint.php
@@ -0,0 +1,123 @@
+ $method,
+ 'headers' => array( 'Content-Type' => 'application/json; charset=utf-8' ),
+ 'data_format' => 'body',
+ 'timeout' => 60, //phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout
+ 'body' => '{}',
+ );
+
+ // Add API key to request headers.
+ if ( $this->get_parsely()->api_secret_is_set() ) {
+ $options['headers']['X-APIKEY-SECRET'] = $this->get_parsely()->get_api_secret();
+ }
+
+ return $options;
+ }
+
+ /**
+ * Returns the common query arguments to send to the remote API.
+ *
+ * This method appends the API key and secret to the query arguments.
+ *
+ * @since 3.17.0
+ *
+ * @param array $args Additional query arguments to send to the remote API.
+ * @return array The query arguments to send to the remote API.
+ */
+ protected function get_query_args( array $args = array() ): array {
+ $query_args = parent::get_query_args( $args );
+
+ // Set up the API key and secret.
+ $query_args['apikey'] = $this->get_parsely()->get_site_id();
+
+ return $query_args;
+ }
+
+ /**
+ * Processes the response from the remote API.
+ *
+ * @since 3.17.0
+ *
+ * @param WP_HTTP_Response|WP_Error $response The response from the remote API.
+ * @return array|WP_Error The processed response.
+ */
+ protected function process_response( $response ) {
+ if ( is_wp_error( $response ) ) {
+ /** @var WP_Error $response */
+ return $response;
+ }
+
+ // Handle any errors returned by the API.
+ if ( 200 !== $response['response']['code'] ) {
+ $error = json_decode( wp_remote_retrieve_body( $response ), true );
+
+ if ( ! is_array( $error ) ) {
+ return new WP_Error(
+ 400,
+ __( 'Unable to decode upstream API error', 'wp-parsely' )
+ );
+ }
+
+ return new WP_Error( $error['error'], $error['detail'] );
+ }
+
+ $body = wp_remote_retrieve_body( $response );
+ $decoded = json_decode( $body, true );
+
+ if ( ! is_array( $decoded ) ) {
+ return new WP_Error( 400, __( 'Unable to decode upstream API response', 'wp-parsely' ) );
+ }
+
+ if ( ! is_array( $decoded['result'] ) ) {
+ return new WP_Error( 400, __( 'Unable to parse data from upstream API', 'wp-parsely' ) );
+ }
+
+ return $decoded['result'];
+ }
+}
diff --git a/tests/Integration/ContentHelper/ContentHelperDashboardWidgetTest.php b/tests/Integration/ContentHelper/ContentHelperDashboardWidgetTest.php
index cfb9f5d8e..7109879c3 100644
--- a/tests/Integration/ContentHelper/ContentHelperDashboardWidgetTest.php
+++ b/tests/Integration/ContentHelper/ContentHelperDashboardWidgetTest.php
@@ -11,16 +11,48 @@
use Parsely\Content_Helper\Dashboard_Widget;
use Parsely\Parsely;
+use Parsely\Tests\Integration\TestCase;
/**
* Integration Tests for the PCH Dashboard Widget.
*/
final class ContentHelperDashboardWidgetTest extends ContentHelperFeatureTest {
+ /**
+ * Internal variable.
+ *
+ * @since 3.17.0
+ *
+ * @var Parsely $parsely Holds the Parsely object.
+ */
+ private static $parsely;
+
/**
* Setup method called before each test.
+ *
+ * @since 3.17.0
*/
public function set_up(): void {
- $GLOBALS['parsely'] = new Parsely();
+ parent::set_up();
+
+ self::$parsely = new Parsely();
+ self::$parsely->get_rest_api_controller()->init();
+
+ TestCase::set_options(
+ array(
+ 'apikey' => 'test_apikey',
+ 'api_secret' => 'test_secret',
+ )
+ );
+ }
+
+ /**
+ * Teardown method called after each test.
+ *
+ * @since 3.17.0
+ */
+ public function tear_down(): void {
+ parent::tear_down();
+ TestCase::set_options();
}
/**
@@ -44,8 +76,8 @@ protected function assert_enqueued_status(
string $user_role,
array $additional_args = array()
): void {
- $feature = new Dashboard_Widget( $GLOBALS['parsely'] );
- $this->set_current_user_to( $user_login, $user_role );
+ $feature = new Dashboard_Widget( self::$parsely );
+ self::set_current_user_to( $user_login, $user_role );
parent::set_filters(
$feature::get_feature_filter_name(),
@@ -85,11 +117,8 @@ protected function assert_enqueued_status(
* @covers \Parsely\Content_Helper\Dashboard_Widget::get_script_id
* @covers \Parsely\Content_Helper\Dashboard_Widget::get_style_id
* @covers \Parsely\Content_Helper\Dashboard_Widget::run
- * @covers \Parsely\RemoteAPI\Analytics_Posts_API::is_available_to_current_user
* @uses \Parsely\Parsely::__construct
- * @uses \Parsely\Endpoints\Base_Endpoint::__construct
* @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
- * @uses \Parsely\Utils::get_asset_info
*
* @group content-helper
*/
@@ -112,11 +141,9 @@ public function test_assets_do_not_get_enqueued_when_user_has_not_enough_capabil
* @covers \Parsely\Content_Helper\Dashboard_Widget::get_script_id
* @covers \Parsely\Content_Helper\Dashboard_Widget::get_style_id
* @covers \Parsely\Content_Helper\Dashboard_Widget::run
- * @covers \Parsely\RemoteAPI\Analytics_Posts_API::is_available_to_current_user
* @uses \Parsely\Parsely::__construct
- * @uses \Parsely\Endpoints\Base_Endpoint::__construct
* @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
- * @uses \Parsely\Utils::get_asset_info
+ * @uses \Parsely\Utils\Utils::get_asset_info
*
* @group content-helper
*/
diff --git a/tests/Integration/ContentHelper/ContentHelperFeatureTest.php b/tests/Integration/ContentHelper/ContentHelperFeatureTest.php
index 977a968ba..45f4700c5 100644
--- a/tests/Integration/ContentHelper/ContentHelperFeatureTest.php
+++ b/tests/Integration/ContentHelper/ContentHelperFeatureTest.php
@@ -61,7 +61,7 @@ protected function assert_enqueued_status_default(
string $user_login,
string $user_role
): void {
- $this->set_current_user_to( $user_login, $user_role );
+ self::set_current_user_to( $user_login, $user_role );
self::set_filters(
$feature::get_feature_filter_name(),
@@ -172,7 +172,6 @@ protected static function deregister_feature_assets_and_run(
* @covers \Parsely\Content_Helper\Post_List_Stats::set_current_screen
* @uses \Parsely\Content_Helper\Content_Helper_Feature::get_credentials_not_set_message
* @uses \Parsely\Content_Helper\Content_Helper_Feature::inject_inline_scripts
- * @uses \Parsely\Endpoints\Base_Endpoint::__construct
* @uses \Parsely\Parsely::__construct
* @uses \Parsely\Parsely::allow_parsely_remote_requests
* @uses \Parsely\Parsely::api_secret_is_set
@@ -184,7 +183,7 @@ protected static function deregister_feature_assets_and_run(
* @uses \Parsely\Parsely::set_managed_options
* @uses \Parsely\Parsely::site_id_is_set
* @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
- * @uses \Parsely\Utils::get_asset_info
+ * @uses \Parsely\Utils\Utils::get_asset_info
*
* @group content-helper
*/
@@ -229,7 +228,6 @@ public function test_assets_get_enqueued_by_default(): void {
* @covers \Parsely\Content_Helper\Post_List_Stats::set_current_screen
* @uses \Parsely\Content_Helper\Content_Helper_Feature::get_credentials_not_set_message
* @uses \Parsely\Content_Helper\Content_Helper_Feature::inject_inline_scripts
- * @uses \Parsely\Endpoints\Base_Endpoint::__construct
* @uses \Parsely\Parsely::__construct
* @uses \Parsely\Parsely::allow_parsely_remote_requests
* @uses \Parsely\Parsely::api_secret_is_set
@@ -241,7 +239,7 @@ public function test_assets_get_enqueued_by_default(): void {
* @uses \Parsely\Parsely::set_managed_options
* @uses \Parsely\Parsely::site_id_is_set
* @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
- * @uses \Parsely\Utils::get_asset_info
+ * @uses \Parsely\Utils\Utils::get_asset_info
*
* @group content-helper
*/
@@ -277,7 +275,6 @@ public function test_assets_get_enqueued_when_global_filter_is_true(): void {
* @covers \Parsely\Content_Helper\Post_List_Stats::__construct
* @covers \Parsely\Content_Helper\Post_List_Stats::get_feature_filter_name
* @covers \Parsely\Content_Helper\Post_List_Stats::run
- * @uses \Parsely\Endpoints\Base_Endpoint::__construct
* @uses \Parsely\Parsely::__construct
* @uses \Parsely\Parsely::allow_parsely_remote_requests
* @uses \Parsely\Parsely::api_secret_is_set
@@ -286,7 +283,7 @@ public function test_assets_get_enqueued_when_global_filter_is_true(): void {
* @uses \Parsely\Parsely::set_managed_options
* @uses \Parsely\Parsely::site_id_is_set
* @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
- * @uses \Parsely\Utils::get_asset_info
+ * @uses \Parsely\Utils\Utils::get_asset_info
*
* @group content-helper
*/
@@ -322,7 +319,6 @@ public function test_assets_do_not_get_enqueued_when_global_filter_is_false(): v
* @covers \Parsely\Content_Helper\Post_List_Stats::__construct
* @covers \Parsely\Content_Helper\Post_List_Stats::get_feature_filter_name
* @covers \Parsely\Content_Helper\Post_List_Stats::run
- * @uses \Parsely\Endpoints\Base_Endpoint::__construct
* @uses \Parsely\Parsely::__construct
* @uses \Parsely\Parsely::allow_parsely_remote_requests
* @uses \Parsely\Parsely::api_secret_is_set
@@ -331,7 +327,7 @@ public function test_assets_do_not_get_enqueued_when_global_filter_is_false(): v
* @uses \Parsely\Parsely::set_managed_options
* @uses \Parsely\Parsely::site_id_is_set
* @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
- * @uses \Parsely\Utils::get_asset_info
+ * @uses \Parsely\Utils\Utils::get_asset_info
*
* @group content-helper
*/
@@ -377,7 +373,6 @@ public function test_assets_do_not_get_enqueued_when_global_filter_is_invalid():
* @covers \Parsely\Content_Helper\Post_List_Stats::set_current_screen
* @uses \Parsely\Content_Helper\Content_Helper_Feature::get_credentials_not_set_message
* @uses \Parsely\Content_Helper\Content_Helper_Feature::inject_inline_scripts
- * @uses \Parsely\Endpoints\Base_Endpoint::__construct
* @uses \Parsely\Parsely::__construct
* @uses \Parsely\Parsely::allow_parsely_remote_requests
* @uses \Parsely\Parsely::api_secret_is_set
@@ -389,7 +384,7 @@ public function test_assets_do_not_get_enqueued_when_global_filter_is_invalid():
* @uses \Parsely\Parsely::set_managed_options
* @uses \Parsely\Parsely::site_id_is_set
* @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
- * @uses \Parsely\Utils::get_asset_info
+ * @uses \Parsely\Utils\Utils::get_asset_info
*
* @group content-helper
*/
@@ -425,7 +420,6 @@ public function test_assets_get_enqueued_when_feature_filter_is_true(): void {
* @covers \Parsely\Content_Helper\Post_List_Stats::__construct
* @covers \Parsely\Content_Helper\Post_List_Stats::get_feature_filter_name
* @covers \Parsely\Content_Helper\Post_List_Stats::run
- * @uses \Parsely\Endpoints\Base_Endpoint::__construct
* @uses \Parsely\Parsely::__construct
* @uses \Parsely\Parsely::allow_parsely_remote_requests
* @uses \Parsely\Parsely::api_secret_is_set
@@ -434,7 +428,7 @@ public function test_assets_get_enqueued_when_feature_filter_is_true(): void {
* @uses \Parsely\Parsely::set_managed_options
* @uses \Parsely\Parsely::site_id_is_set
* @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
- * @uses \Parsely\Utils::get_asset_info
+ * @uses \Parsely\Utils\Utils::get_asset_info
*
* @group content-helper
*/
@@ -470,7 +464,6 @@ public function test_assets_do_not_get_enqueued_when_feature_filter_is_false():
* @covers \Parsely\Content_Helper\Post_List_Stats::__construct
* @covers \Parsely\Content_Helper\Post_List_Stats::get_feature_filter_name
* @covers \Parsely\Content_Helper\Post_List_Stats::run
- * @uses \Parsely\Endpoints\Base_Endpoint::__construct
* @uses \Parsely\Parsely::__construct
* @uses \Parsely\Parsely::allow_parsely_remote_requests
* @uses \Parsely\Parsely::api_secret_is_set
@@ -479,7 +472,7 @@ public function test_assets_do_not_get_enqueued_when_feature_filter_is_false():
* @uses \Parsely\Parsely::set_managed_options
* @uses \Parsely\Parsely::site_id_is_set
* @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
- * @uses \Parsely\Utils::get_asset_info
+ * @uses \Parsely\Utils\Utils::get_asset_info
*
* @group content-helper
*/
@@ -524,7 +517,6 @@ public function test_assets_do_not_get_enqueued_when_feature_filter_is_invalid()
* @covers \Parsely\Content_Helper\Post_List_Stats::set_current_screen
* @uses \Parsely\Content_Helper\Content_Helper_Feature::get_credentials_not_set_message
* @uses \Parsely\Content_Helper\Content_Helper_Feature::inject_inline_scripts
- * @uses \Parsely\Endpoints\Base_Endpoint::__construct
* @uses \Parsely\Parsely::__construct
* @uses \Parsely\Parsely::allow_parsely_remote_requests
* @uses \Parsely\Parsely::api_secret_is_set
@@ -536,7 +528,7 @@ public function test_assets_do_not_get_enqueued_when_feature_filter_is_invalid()
* @uses \Parsely\Parsely::set_managed_options
* @uses \Parsely\Parsely::site_id_is_set
* @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
- * @uses \Parsely\Utils::get_asset_info
+ * @uses \Parsely\Utils\Utils::get_asset_info
*
* @group content-helper
*/
@@ -579,7 +571,6 @@ public function test_assets_get_enqueued_when_both_filters_are_true(): void {
* @covers \Parsely\Content_Helper\Post_List_Stats::is_tracked_as_post_type
* @covers \Parsely\Content_Helper\Post_List_Stats::run
* @covers \Parsely\Content_Helper\Post_List_Stats::set_current_screen
- * @uses \Parsely\Endpoints\Base_Endpoint::__construct
* @uses \Parsely\Parsely::__construct
* @uses \Parsely\Parsely::allow_parsely_remote_requests
* @uses \Parsely\Parsely::api_secret_is_set
@@ -588,7 +579,7 @@ public function test_assets_get_enqueued_when_both_filters_are_true(): void {
* @uses \Parsely\Parsely::set_managed_options
* @uses \Parsely\Parsely::site_id_is_set
* @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
- * @uses \Parsely\Utils::get_asset_info
+ * @uses \Parsely\Utils\Utils::get_asset_info
*
* @group content-helper
*/
@@ -624,7 +615,6 @@ public function test_assets_do_not_get_enqueued_when_both_filters_are_false(): v
* @covers \Parsely\Content_Helper\Post_List_Stats::__construct
* @covers \Parsely\Content_Helper\Post_List_Stats::get_feature_filter_name
* @covers \Parsely\Content_Helper\Post_List_Stats::run
- * @uses \Parsely\Endpoints\Base_Endpoint::__construct
* @uses \Parsely\Parsely::__construct
* @uses \Parsely\Parsely::allow_parsely_remote_requests
* @uses \Parsely\Parsely::api_secret_is_set
@@ -633,7 +623,7 @@ public function test_assets_do_not_get_enqueued_when_both_filters_are_false(): v
* @uses \Parsely\Parsely::set_managed_options
* @uses \Parsely\Parsely::site_id_is_set
* @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
- * @uses \Parsely\Utils::get_asset_info
+ * @uses \Parsely\Utils\Utils::get_asset_info
*
* @group content-helper
*/
@@ -669,7 +659,6 @@ public function test_assets_do_not_get_enqueued_when_both_filters_are_invalid():
* @covers \Parsely\Content_Helper\Post_List_Stats::__construct
* @covers \Parsely\Content_Helper\Post_List_Stats::get_feature_filter_name
* @covers \Parsely\Content_Helper\Post_List_Stats::run
- * @uses \Parsely\Endpoints\Base_Endpoint::__construct
* @uses \Parsely\Parsely::__construct
* @uses \Parsely\Parsely::allow_parsely_remote_requests
* @uses \Parsely\Parsely::api_secret_is_set
@@ -678,7 +667,7 @@ public function test_assets_do_not_get_enqueued_when_both_filters_are_invalid():
* @uses \Parsely\Parsely::set_managed_options
* @uses \Parsely\Parsely::site_id_is_set
* @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
- * @uses \Parsely\Utils::get_asset_info
+ * @uses \Parsely\Utils\Utils::get_asset_info
*
* @group content-helper
*/
@@ -723,7 +712,6 @@ public function test_assets_do_not_get_enqueued_when_global_filter_is_true_and_f
* @covers \Parsely\Content_Helper\Post_List_Stats::set_current_screen
* @uses \Parsely\Content_Helper\Content_Helper_Feature::get_credentials_not_set_message
* @uses \Parsely\Content_Helper\Content_Helper_Feature::inject_inline_scripts
- * @uses \Parsely\Endpoints\Base_Endpoint::__construct
* @uses \Parsely\Parsely::__construct
* @uses \Parsely\Parsely::allow_parsely_remote_requests
* @uses \Parsely\Parsely::api_secret_is_set
@@ -735,7 +723,7 @@ public function test_assets_do_not_get_enqueued_when_global_filter_is_true_and_f
* @uses \Parsely\Parsely::set_managed_options
* @uses \Parsely\Parsely::site_id_is_set
* @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
- * @uses \Parsely\Utils::get_asset_info
+ * @uses \Parsely\Utils\Utils::get_asset_info
*
* @group content-helper
*/
@@ -772,7 +760,6 @@ public function test_assets_get_enqueued_when_global_filter_is_false_and_feature
* @covers \Parsely\Content_Helper\Post_List_Stats::__construct
* @covers \Parsely\Content_Helper\Post_List_Stats::get_feature_filter_name
* @covers \Parsely\Content_Helper\Post_List_Stats::run
- * @uses \Parsely\Endpoints\Base_Endpoint::__construct
* @uses \Parsely\Parsely::__construct
* @uses \Parsely\Parsely::allow_parsely_remote_requests
* @uses \Parsely\Parsely::api_secret_is_set
@@ -781,7 +768,7 @@ public function test_assets_get_enqueued_when_global_filter_is_false_and_feature
* @uses \Parsely\Parsely::set_managed_options
* @uses \Parsely\Parsely::site_id_is_set
* @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
- * @uses \Parsely\Utils::get_asset_info
+ * @uses \Parsely\Utils\Utils::get_asset_info
*
* @group content-helper
*/
@@ -826,7 +813,6 @@ public function test_assets_do_not_get_enqueued_when_global_filter_is_true_and_f
* @covers \Parsely\Content_Helper\Post_List_Stats::set_current_screen
* @uses \Parsely\Content_Helper\Content_Helper_Feature::get_credentials_not_set_message
* @uses \Parsely\Content_Helper\Content_Helper_Feature::inject_inline_scripts
- * @uses \Parsely\Endpoints\Base_Endpoint::__construct
* @uses \Parsely\Parsely::__construct
* @uses \Parsely\Parsely::allow_parsely_remote_requests
* @uses \Parsely\Parsely::api_secret_is_set
@@ -838,7 +824,7 @@ public function test_assets_do_not_get_enqueued_when_global_filter_is_true_and_f
* @uses \Parsely\Parsely::set_managed_options
* @uses \Parsely\Parsely::site_id_is_set
* @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
- * @uses \Parsely\Utils::get_asset_info
+ * @uses \Parsely\Utils\Utils::get_asset_info
*
* @group content-helper
*/
diff --git a/tests/Integration/ContentHelper/ContentHelperPostListStatsTest.php b/tests/Integration/ContentHelper/ContentHelperPostListStatsTest.php
index 7ea57fc8c..276d27c37 100644
--- a/tests/Integration/ContentHelper/ContentHelperPostListStatsTest.php
+++ b/tests/Integration/ContentHelper/ContentHelperPostListStatsTest.php
@@ -12,8 +12,10 @@
use Mockery;
use Parsely\Content_Helper\Post_List_Stats;
use Parsely\Parsely;
-use Parsely\RemoteAPI\Analytics_Posts_API;
+use Parsely\Services\Content_API\Content_API_Service;
+use Parsely\Services\Content_API\Endpoints\Endpoint_Analytics_Posts;
use Parsely\Tests\Integration\TestCase;
+use ReflectionProperty;
use WP_Error;
use WP_Post;
use WP_Scripts;
@@ -23,11 +25,17 @@
*
* @since 3.7.0
*
- * @phpstan-import-type Analytics_Post_API_Params from Analytics_Posts_API
- * @phpstan-import-type Analytics_Post from Analytics_Posts_API
+ * @phpstan-import-type Analytics_Post from Endpoint_Analytics_Posts
* @phpstan-import-type Parsely_Posts_Stats_Response from Post_List_Stats
*/
final class ContentHelperPostListStatsTest extends ContentHelperFeatureTest {
+ /**
+ * The Parsely instance.
+ *
+ * @var Parsely
+ */
+ private static $parsely;
+
/**
* Internal variable.
*
@@ -51,6 +59,9 @@ final class ContentHelperPostListStatsTest extends ContentHelperFeatureTest {
public function set_up(): void {
parent::set_up();
+ self::$parsely = new Parsely();
+ self::$parsely->get_rest_api_controller()->init();
+
$this->set_permalink_structure( '/%year%/%monthnum%/%day%/%postname%' );
$this->set_current_user_to_admin();
}
@@ -85,7 +96,7 @@ protected function assert_enqueued_status(
string $user_role,
array $additional_args = array()
): void {
- $this->set_current_user_to( $user_login, $user_role );
+ self::set_current_user_to( $user_login, $user_role );
parent::set_filters(
Post_List_Stats::get_feature_filter_name(),
@@ -121,13 +132,11 @@ protected function assert_enqueued_status(
* @covers \Parsely\Content_Helper\Post_List_Stats::__construct
* @covers \Parsely\Content_Helper\Post_List_Stats::get_feature_filter_name
* @covers \Parsely\Content_Helper\Post_List_Stats::run
- * @covers \Parsely\RemoteAPI\Analytics_Posts_API::is_available_to_current_user
* @covers \Parsely\Utils\Utils::convert_endpoint_to_filter_key
* @uses \Parsely\Parsely::__construct
* @uses \Parsely\Parsely::api_secret_is_set
* @uses \Parsely\Parsely::get_options
* @uses \Parsely\Parsely::site_id_is_set
- * @uses \Parsely\Endpoints\Base_Endpoint::__construct
*
* @group content-helper
*/
@@ -150,13 +159,11 @@ public function test_assets_do_not_get_enqueued_when_user_has_not_enough_capabil
* @covers \Parsely\Content_Helper\Post_List_Stats::is_tracked_as_post_type
* @covers \Parsely\Content_Helper\Post_List_Stats::run
* @covers \Parsely\Content_Helper\Post_List_Stats::set_current_screen
- * @covers \Parsely\RemoteAPI\Analytics_Posts_API::is_available_to_current_user
* @covers \Parsely\Utils\Utils::convert_endpoint_to_filter_key
* @uses \Parsely\Parsely::__construct
* @uses \Parsely\Parsely::api_secret_is_set
* @uses \Parsely\Parsely::get_options
* @uses \Parsely\Parsely::site_id_is_set
- * @uses \Parsely\Endpoints\Base_Endpoint::__construct
*
* @group content-helper
*/
@@ -754,7 +761,7 @@ public function test_script_of_parsely_stats_admin_column_on_valid_posts_and_val
* @return Post_List_Stats
*/
private function mock_parsely_stats_response( ?array $return_value ): Post_List_Stats {
- $obj = Mockery::mock( Post_List_Stats::class, array( new Parsely() ) )->makePartial();
+ $obj = Mockery::mock( Post_List_Stats::class, array( self::$parsely ) )->makePartial();
$obj->shouldReceive( 'get_parsely_stats_response' )->once()->andReturn( $return_value );
$obj->run();
@@ -785,7 +792,7 @@ public function test_should_not_call_parsely_api_on_empty_api_secret_and_hidden_
* @return Post_List_Stats
*/
private function mock_is_parsely_stats_column_hidden( bool $return_value = false ): Post_List_Stats {
- $obj = Mockery::mock( Post_List_Stats::class, array( new Parsely() ) )->makePartial();
+ $obj = Mockery::mock( Post_List_Stats::class, array( self::$parsely ) )->makePartial();
$obj->shouldReceive( 'is_parsely_stats_column_hidden' )->once()->andReturn( $return_value );
$obj->run();
@@ -1251,7 +1258,7 @@ public function test_parsely_stats_response_on_valid_hierarchal_post_type_and_ha
* @param array $posts Available Posts.
* @param string $post_type Type of the post.
* @param array|WP_Error|null $api_response Mocked response that we return on calling API.
- * @param Analytics_Post_API_Params|null $api_params API Parameters.
+ * @param array|null $api_params API Parameters.
* @return Parsely_Posts_Stats_Response|null
*/
private function get_parsely_stats_response(
@@ -1266,9 +1273,13 @@ private function get_parsely_stats_response(
$this->show_content_on_parsely_stats_column( $posts, $post_type );
ob_get_clean(); // Discard output to keep console clean while running tests.
- $api = Mockery::mock( Analytics_Posts_API::class, array( new Parsely() ) )->makePartial();
+ if ( null === $api_response ) {
+ $api_response = self::$parsely_api_empty_response;
+ }
+
+ $api = Mockery::mock( Content_API_Service::class, array( self::$parsely ) )->makePartial();
if ( ! is_null( $api_params ) ) {
- $api->shouldReceive( 'get_posts_analytics' )
+ $api->shouldReceive( 'get_posts' )
->once()
->withArgs(
array(
@@ -1276,8 +1287,8 @@ private function get_parsely_stats_response(
$api_params,
// Params which will not change.
array(
- 'period_start' => Analytics_Posts_API::ANALYTICS_API_DAYS_LIMIT . 'd',
- 'limit' => 2000,
+ 'period_start' => Endpoint_Analytics_Posts::MAX_PERIOD,
+ 'limit' => Endpoint_Analytics_Posts::MAX_LIMIT,
'sort' => 'avg_engaged',
)
),
@@ -1285,10 +1296,15 @@ private function get_parsely_stats_response(
)
->andReturn( $api_response );
} else {
- $api->shouldReceive( 'get_posts_analytics' )->once()->andReturn( $api_response );
+ $api->shouldReceive( 'get_posts' )->once()->andReturn( $api_response );
}
- return $obj->get_parsely_stats_response( $api );
+ // Replace the original API with the mock, using reflection.
+ $api_reflection = new ReflectionProperty( $obj, 'content_api' );
+ $api_reflection->setAccessible( true );
+ $api_reflection->setValue( $obj, $api );
+
+ return $obj->get_parsely_stats_response();
}
/**
@@ -1309,7 +1325,7 @@ private function assert_hooks_for_parsely_stats_response( $assert_type = true ):
* @return Post_List_Stats
*/
private function init_post_list_stats(): Post_List_Stats {
- $obj = new Post_List_Stats( new Parsely() );
+ $obj = new Post_List_Stats( self::$parsely );
$obj->run();
return $obj;
diff --git a/tests/Integration/Endpoints/Proxy/AnalyticsPostsProxyEndpointTest.php b/tests/Integration/Endpoints/Proxy/AnalyticsPostsProxyEndpointTest.php
deleted file mode 100644
index b0a7c366b..000000000
--- a/tests/Integration/Endpoints/Proxy/AnalyticsPostsProxyEndpointTest.php
+++ /dev/null
@@ -1,264 +0,0 @@
-dispatch(
- new WP_REST_Request( 'GET', self::$route )
- );
- /**
- * Variable.
- *
- * @var WP_Error
- */
- $error = $response->as_error();
-
- self::assertSame( 401, $response->get_status() );
- self::assertSame( 'rest_forbidden', $error->get_error_code() );
- self::assertSame(
- 'Sorry, you are not allowed to do that.',
- $error->get_error_message()
- );
- }
-
- /**
- * Verifies that calling `GET /wp-parsely/v1/stats/posts` returns an
- * error and does not perform a remote call when the Site ID is not populated
- * in site options.
- *
- * @covers \Parsely\Endpoints\Analytics_Posts_API_Proxy::get_items
- * @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::__construct
- * @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::is_available_to_current_user
- * @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::run
- * @uses \Parsely\Parsely::site_id_is_missing
- * @uses \Parsely\Parsely::site_id_is_set
- * @uses \Parsely\Parsely::get_options
- * @uses \Parsely\Endpoints\Base_Endpoint::__construct
- * @uses \Parsely\Endpoints\Base_API_Proxy::get_data
- * @uses \Parsely\Endpoints\Base_API_Proxy::register_endpoint
- */
- public function test_get_items_fails_when_site_id_is_not_set(): void {
- $this->set_current_user_to_admin();
- parent::run_test_get_items_fails_without_site_id_set();
- }
-
- /**
- * Verifies that calling `GET /wp-parsely/v1/stats/posts` returns an
- * error and does not perform a remote call when the API Secret is not
- * populated in site options.
- *
- * @covers \Parsely\Endpoints\Analytics_Posts_API_Proxy::get_items
- * @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::__construct
- * @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::is_available_to_current_user
- * @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::run
- * @uses \Parsely\Parsely::site_id_is_missing
- * @uses \Parsely\Parsely::site_id_is_set
- * @uses \Parsely\Parsely::api_secret_is_set
- * @uses \Parsely\Parsely::get_options
- * @uses \Parsely\Endpoints\Base_Endpoint::__construct
- * @uses \Parsely\Endpoints\Base_API_Proxy::get_data
- * @uses \Parsely\Endpoints\Base_API_Proxy::register_endpoint
- */
- public function test_get_items_fails_when_api_secret_is_not_set(): void {
- $this->set_current_user_to_admin();
- parent::run_test_get_items_fails_without_api_secret_set();
- }
-
- /**
- * Verifies default user capability filter.
- *
- * @covers \Parsely\Endpoints\Analytics_Posts_API_Proxy::is_available_to_current_user
- * @uses \Parsely\Endpoints\Base_Endpoint::__construct
- * @uses \Parsely\RemoteAPI\Analytics_Posts_API::is_available_to_current_user
- */
- public function test_user_is_allowed_to_make_proxy_api_call_if_default_user_capability_is_changed(): void {
- parent::run_test_user_is_allowed_to_make_proxy_api_call_if_default_user_capability_is_changed();
- }
-
- /**
- * Verifies endpoint specific user capability filter.
- *
- * @covers \Parsely\Endpoints\Analytics_Posts_API_Proxy::is_available_to_current_user
- * @uses \Parsely\Endpoints\Base_Endpoint::__construct
- * @uses \Parsely\RemoteAPI\Analytics_Posts_API::is_available_to_current_user
- */
- public function test_user_is_allowed_to_make_proxy_api_call_if_endpoint_specific_user_capability_is_changed(): void {
- parent::run_test_user_is_allowed_to_make_proxy_api_call_if_endpoint_specific_user_capability_is_changed( 'analytics_posts' );
- }
-
- /**
- * Verifies that calls to `GET /wp-parsely/v1/stats/posts` return
- * results in the expected format.
- *
- * @covers \Parsely\Endpoints\Analytics_Posts_API_Proxy::get_items
- * @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::__construct
- * @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::generate_data
- * @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::is_available_to_current_user
- * @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::run
- * @uses \Parsely\Endpoints\Base_API_Proxy::get_data
- * @uses \Parsely\Endpoints\Base_API_Proxy::register_endpoint
- * @uses \Parsely\Parsely::site_id_is_missing
- * @uses \Parsely\Parsely::site_id_is_set
- * @uses \Parsely\Parsely::api_secret_is_set
- * @uses \Parsely\Parsely::get_site_id
- * @uses \Parsely\Parsely::get_api_secret
- * @uses \Parsely\Parsely::get_options
- * @uses \Parsely\Endpoints\Base_Endpoint::__construct
- * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_api_url
- * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_items
- * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_request_options
- */
- public function test_get_items(): void {
- $this->set_current_user_to_admin();
- TestCase::set_options(
- array(
- 'apikey' => 'example.com',
- 'api_secret' => 'test',
- )
- );
-
- $dispatched = 0;
- $date_format = Utils::get_date_format();
-
- add_filter(
- 'pre_http_request',
- function () use ( &$dispatched ): array {
- $dispatched++;
- return array(
- 'body' => '{"data":[
- {
- "author": "Aakash Shah",
- "metrics": {"views": 142},
- "pub_date": "2020-04-06T13:30:58",
- "thumb_url_medium": "https://images.parsely.com/XCmTXuOf8yVbUYTxj2abQ4RSDkM=/85x85/smart/https%3A//blog.parse.ly/wp-content/uploads/2021/06/Web-Analytics-Tool.png%3Fw%3D150%26h%3D150%26crop%3D1",
- "title": "9 Types of Web Analytics Tools \u2014 And How to Know Which Ones You Really Need",
- "url": "https://blog.parse.ly/web-analytics-software-tools/?itm_source=parsely-api"
- },
- {
- "author": "Stephanie Schwartz and Andrew Butler",
- "metrics": {"views": 40},
- "pub_date": "2021-04-30T20:30:24",
- "thumb_url_medium": "https://images.parsely.com/ap3YSufqxnLpz6zzQshoks3snXI=/85x85/smart/https%3A//blog.parse.ly/wp-content/uploads/2021/05/pexels-brett-jordan-998501-1024x768-2.jpeg%3Fw%3D150%26h%3D150%26crop%3D1",
- "title": "5 Tagging Best Practices For Getting the Most Out of Your Content Strategy",
- "url": "https://blog.parse.ly/5-tagging-best-practices-content-strategy/?itm_source=parsely-api"
- }
- ]}',
- );
- }
- );
-
- $rest_request = new WP_REST_Request( 'GET', '/wp-parsely/v1/stats/posts' );
- $rest_request->set_param( 'itm_source', 'wp-parsely-content-helper' );
-
- $response = rest_get_server()->dispatch( $rest_request );
-
- self::assertSame( 1, $dispatched );
- self::assertSame( 200, $response->get_status() );
- self::assertEquals(
- (object) array(
- 'data' => array(
- (object) array(
- 'author' => 'Aakash Shah',
- 'date' => wp_date( $date_format, strtotime( '2020-04-06T13:30:58' ) ),
- 'id' => 'https://blog.parse.ly/web-analytics-software-tools/',
- 'dashUrl' => PARSELY::DASHBOARD_BASE_URL . '/example.com/find?url=https%3A%2F%2Fblog.parse.ly%2Fweb-analytics-software-tools%2F',
- 'thumbnailUrl' => 'https://images.parsely.com/XCmTXuOf8yVbUYTxj2abQ4RSDkM=/85x85/smart/https%3A//blog.parse.ly/wp-content/uploads/2021/06/Web-Analytics-Tool.png%3Fw%3D150%26h%3D150%26crop%3D1',
- 'title' => '9 Types of Web Analytics Tools — And How to Know Which Ones You Really Need',
- 'url' => 'https://blog.parse.ly/web-analytics-software-tools/?itm_source=wp-parsely-content-helper',
- 'views' => '142',
- 'postId' => 0,
- 'rawUrl' => 'https://blog.parse.ly/web-analytics-software-tools/',
- ),
- (object) array(
- 'author' => 'Stephanie Schwartz and Andrew Butler',
- 'date' => wp_date( $date_format, strtotime( '2021-04-30T20:30:24' ) ),
- 'id' => 'https://blog.parse.ly/5-tagging-best-practices-content-strategy/',
- 'dashUrl' => PARSELY::DASHBOARD_BASE_URL . '/example.com/find?url=https%3A%2F%2Fblog.parse.ly%2F5-tagging-best-practices-content-strategy%2F',
- 'thumbnailUrl' => 'https://images.parsely.com/ap3YSufqxnLpz6zzQshoks3snXI=/85x85/smart/https%3A//blog.parse.ly/wp-content/uploads/2021/05/pexels-brett-jordan-998501-1024x768-2.jpeg%3Fw%3D150%26h%3D150%26crop%3D1',
- 'title' => '5 Tagging Best Practices For Getting the Most Out of Your Content Strategy',
- 'url' => 'https://blog.parse.ly/5-tagging-best-practices-content-strategy/?itm_source=wp-parsely-content-helper',
- 'views' => '40',
- 'postId' => 0,
- 'rawUrl' => 'https://blog.parse.ly/5-tagging-best-practices-content-strategy/',
- ),
- ),
- ),
- $response->get_data()
- );
- }
-}
diff --git a/tests/Integration/Endpoints/Proxy/BaseProxyEndpointTest.php b/tests/Integration/Endpoints/Proxy/BaseProxyEndpointTest.php
deleted file mode 100644
index 208e0abaa..000000000
--- a/tests/Integration/Endpoints/Proxy/BaseProxyEndpointTest.php
+++ /dev/null
@@ -1,245 +0,0 @@
-wp_rest_server_global_backup = $GLOBALS['wp_rest_server'] ?? null;
- $endpoint = $this->get_endpoint();
- $this->rest_api_init_proxy = static function () use ( $endpoint ) {
- $endpoint->run();
- };
- add_action( 'rest_api_init', $this->rest_api_init_proxy );
- }
-
- /**
- * Teardown method called after each test.
- *
- * Resets globals.
- */
- public function tear_down(): void {
- parent::tear_down();
- remove_action( 'rest_api_init', $this->rest_api_init_proxy );
-
- // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound
- $GLOBALS['wp_rest_server'] = $this->wp_rest_server_global_backup;
- }
-
- /**
- * Verifies that the route is registered.
- *
- * @param array $methods The methods supported by the route.
- */
- public function run_test_register_routes_by_default(
- array $methods = array( 'GET' => true )
- ): void {
- $routes = rest_get_server()->get_routes();
- self::assertArrayHasKey( self::$route, $routes );
- self::assertCount( 1, $routes[ self::$route ] );
- self::assertSame( $methods, $routes[ self::$route ][0]['methods'] );
- }
-
- /**
- * Verifies that the route is not registered when the respective filter is
- * set to false.
- */
- public function run_test_do_not_register_route_when_proxy_is_disabled(): void {
- // Override some setup steps in order to set the filter to false.
- remove_action( 'rest_api_init', $this->rest_api_init_proxy );
- $endpoint = $this->get_endpoint();
- $this->rest_api_init_proxy = static function () use ( $endpoint ) {
- add_filter( 'wp_parsely_enable_' . self::$filter_key . '_api_proxy', '__return_false' );
- $endpoint->run();
- };
- add_action( 'rest_api_init', $this->rest_api_init_proxy );
-
- $routes = rest_get_server()->get_routes();
- self::assertFalse( array_key_exists( self::$route, $routes ) );
- }
-
- /**
- * Verifies that calls return an error and do not perform a remote call when
- * the Site ID is not populated in site options.
- *
- * @param WP_REST_Request|null $request The request object to be used.
- */
- public function run_test_get_items_fails_without_site_id_set(
- ?WP_REST_Request $request = null
- ): void {
- $this->run_test_get_items_fails(
- array( 'apikey' => '' ),
- 'parsely_site_id_not_set',
- 'A Parse.ly Site ID must be set in site options to use this endpoint',
- $request
- );
- }
-
- /**
- * Verifies that calls return an error and do not perform a remote call when
- * the API Secret is not populated in site options.
- *
- * @param WP_REST_Request|null $request The request object to be used.
- */
- public function run_test_get_items_fails_without_api_secret_set(
- ?WP_REST_Request $request = null
- ): void {
- $this->run_test_get_items_fails(
- array(
- 'apikey' => 'example.com',
- 'api_secret' => '',
- ),
- 'parsely_api_secret_not_set',
- 'A Parse.ly API Secret must be set in site options to use this endpoint',
- $request
- );
- }
-
- /**
- * Verifies that attempting to get items under the given conditions will
- * fail.
- *
- * @param array $options The WordPress options to be set.
- * @param string $expected_error_code The expected error code.
- * @param string $expected_error_message The expected error message.
- * @param WP_REST_Request|null $request The request object to be used.
- */
- private function run_test_get_items_fails(
- array $options,
- string $expected_error_code,
- string $expected_error_message,
- ?WP_REST_Request $request = null
- ): void {
- TestCase::set_options( $options );
- if ( null === $request ) {
- $request = new WP_REST_Request( 'GET', self::$route );
- }
-
- $response = rest_get_server()->dispatch( $request );
- /**
- * Variable.
- *
- * @var WP_Error
- */
- $error = $response->as_error();
- self::assertSame( 403, $response->get_status() );
- self::assertSame( $expected_error_code, $error->get_error_code() );
- self::assertSame( $expected_error_message, $error->get_error_message() );
- }
-
- /**
- * Verifies default user capability filter.
- */
- public function run_test_user_is_allowed_to_make_proxy_api_call_if_default_user_capability_is_changed(): void {
- $this->set_current_user_to_contributor();
- add_filter(
- 'wp_parsely_user_capability_for_all_private_apis',
- function () {
- return 'edit_posts';
- }
- );
-
- self::assertTrue( static::get_endpoint()->is_available_to_current_user() );
- }
-
- /**
- * Verifies endpoint specific user capability filter.
- *
- * @param string|null $filter_key The key to use for the filter.
- */
- public function run_test_user_is_allowed_to_make_proxy_api_call_if_endpoint_specific_user_capability_is_changed(
- $filter_key = null
- ): void {
- $this->set_current_user_to_contributor();
- $filter_key = $filter_key ?? static::$filter_key;
-
- add_filter(
- 'wp_parsely_user_capability_for_' . $filter_key . '_api',
- function () {
- return 'edit_posts';
- }
- );
-
- self::assertTrue( static::get_endpoint()->is_available_to_current_user() );
- }
-}
diff --git a/tests/Integration/Endpoints/Proxy/ReferrersPostDetailProxyEndpointTest.php b/tests/Integration/Endpoints/Proxy/ReferrersPostDetailProxyEndpointTest.php
deleted file mode 100644
index 38ec499a4..000000000
--- a/tests/Integration/Endpoints/Proxy/ReferrersPostDetailProxyEndpointTest.php
+++ /dev/null
@@ -1,305 +0,0 @@
-set_current_user_to_admin();
- parent::run_test_get_items_fails_without_site_id_set();
- }
-
- /**
- * Verifies that calling `GET /wp-parsely/v1/referrers/post/detail` returns
- * an error and does not perform a remote call when the Site ID is not
- * populated in site options.
- *
- * @covers \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::get_items
- * @uses \Parsely\Endpoints\Base_API_Proxy::get_data
- * @uses \Parsely\Endpoints\Base_API_Proxy::register_endpoint
- * @uses \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::__construct
- * @uses \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::is_available_to_current_user
- * @uses \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::run
- * @uses \Parsely\Parsely::site_id_is_missing
- * @uses \Parsely\Parsely::site_id_is_set
- * @uses \Parsely\Parsely::api_secret_is_set
- * @uses \Parsely\Parsely::get_options
- * @uses \Parsely\Endpoints\Base_Endpoint::__construct
- */
- public function test_get_items_fails_when_api_secret_is_not_set(): void {
- $this->set_current_user_to_admin();
- parent::run_test_get_items_fails_without_api_secret_set();
- }
-
- /**
- * Verifies default user capability filter.
- *
- * @covers \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::is_available_to_current_user
- * @uses \Parsely\Endpoints\Base_Endpoint::__construct
- * @uses \Parsely\RemoteAPI\Referrers_Post_Detail_API::is_available_to_current_user
- */
- public function test_user_is_allowed_to_make_proxy_api_call_if_default_user_capability_is_changed(): void {
- parent::run_test_user_is_allowed_to_make_proxy_api_call_if_default_user_capability_is_changed();
- }
-
- /**
- * Verifies endpoint specific user capability filter.
- *
- * @covers \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::is_available_to_current_user
- * @uses \Parsely\Endpoints\Base_Endpoint::__construct
- * @uses \Parsely\RemoteAPI\Referrers_Post_Detail_API::is_available_to_current_user
- */
- public function test_user_is_allowed_to_make_proxy_api_call_if_endpoint_specific_user_capability_is_changed(): void {
- parent::run_test_user_is_allowed_to_make_proxy_api_call_if_endpoint_specific_user_capability_is_changed();
- }
-
- /**
- * Verifies that calls to `GET /wp-parsely/v1/referrers/post/detail` return
- * results in the expected format.
- *
- * @covers \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::get_items
- * @uses \Parsely\Endpoints\Base_API_Proxy::get_data
- * @uses \Parsely\Endpoints\Base_API_Proxy::register_endpoint
- * @uses \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::__construct
- * @uses \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::generate_data
- * @uses \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::is_available_to_current_user
- * @uses \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::run
- * @uses \Parsely\Parsely::site_id_is_missing
- * @uses \Parsely\Parsely::site_id_is_set
- * @uses \Parsely\Parsely::api_secret_is_set
- * @uses \Parsely\Parsely::get_site_id
- * @uses \Parsely\Parsely::get_options
- * @uses \Parsely\Endpoints\Base_Endpoint::__construct
- * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_api_url
- * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_items
- * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_request_options
- */
- public function test_get_items(): void {
- $this->set_current_user_to_admin();
- TestCase::set_options(
- array(
- 'apikey' => 'example.com',
- 'api_secret' => 'test',
- )
- );
-
- $dispatched = 0;
-
- add_filter(
- 'pre_http_request',
- function () use ( &$dispatched ): array {
- $dispatched++;
- return array(
- 'body' => '{"data":[
- {
- "metrics": {"referrers_views": 1500},
- "name": "google",
- "type": "search"
- },
- {
- "metrics": {"referrers_views": 100},
- "name": "blog.parse.ly",
- "type": "internal"
- },
- {
- "metrics": {"referrers_views": 50},
- "name": "bing",
- "type": "search"
- },
- {
- "metrics": {"referrers_views": 30},
- "name": "facebook.com",
- "type": "social"
- },
- {
- "metrics": {"referrers_views": 10},
- "name": "okt.to",
- "type": "other"
- },
- {
- "metrics": {"referrers_views": 10},
- "name": "yandex",
- "type": "search"
- },
- {
- "metrics": {"referrers_views": 10},
- "name": "parse.ly",
- "type": "internal"
- },
- {
- "metrics": {"referrers_views": 10},
- "name": "yahoo!",
- "type": "search"
- },
- {
- "metrics": {"referrers_views": 5},
- "name": "site1.com",
- "type": "other"
- },
- {
- "metrics": {"referrers_views": 5},
- "name": "link.site2.com",
- "type": "other"
- }
- ]}',
- );
- }
- );
-
- $expected_top = (object) array(
- 'direct' => (object) array(
- 'views' => '770',
- 'viewsPercentage' => '30.80',
- 'datasetViewsPercentage' => '31.43',
- ),
- 'google' => (object) array(
- 'views' => '1,500',
- 'viewsPercentage' => '60.00',
- 'datasetViewsPercentage' => '61.22',
- ),
- 'blog.parse.ly' => (object) array(
- 'views' => '100',
- 'viewsPercentage' => '4.00',
- 'datasetViewsPercentage' => '4.08',
- ),
- 'bing' => (object) array(
- 'views' => '50',
- 'viewsPercentage' => '2.00',
- 'datasetViewsPercentage' => '2.04',
- ),
- 'facebook.com' => (object) array(
- 'views' => '30',
- 'viewsPercentage' => '1.20',
- 'datasetViewsPercentage' => '1.22',
- ),
- 'totals' => (object) array(
- 'views' => '2,450',
- 'viewsPercentage' => '98.00',
- 'datasetViewsPercentage' => '100.00',
- ),
- );
-
- $expected_types = (object) array(
- 'social' => (object) array(
- 'views' => '30',
- 'viewsPercentage' => '1.20',
- ),
- 'search' => (object) array(
- 'views' => '1,570',
- 'viewsPercentage' => '62.80',
- ),
- 'other' => (object) array(
- 'views' => '20',
- 'viewsPercentage' => '0.80',
- ),
- 'internal' => (object) array(
- 'views' => '110',
- 'viewsPercentage' => '4.40',
- ),
- 'direct' => (object) array(
- 'views' => '770',
- 'viewsPercentage' => '30.80',
- ),
- 'totals' => (object) array(
- 'views' => '2,500',
- 'viewsPercentage' => '100.00',
- ),
- );
-
- $request = new WP_REST_Request( 'GET', self::$route );
- $request->set_param( 'total_views', '2,500' );
-
- $response = rest_get_server()->dispatch( $request );
-
- self::assertSame( 1, $dispatched );
- self::assertSame( 200, $response->get_status() );
- self::assertEquals(
- (object) array(
- 'data' => array(
- 'top' => $expected_top,
- 'types' => $expected_types,
- ),
- ),
- $response->get_data()
- );
- }
-}
diff --git a/tests/Integration/Endpoints/Proxy/RelatedProxyEndpointTest.php b/tests/Integration/Endpoints/Proxy/RelatedProxyEndpointTest.php
deleted file mode 100644
index 7584fa597..000000000
--- a/tests/Integration/Endpoints/Proxy/RelatedProxyEndpointTest.php
+++ /dev/null
@@ -1,162 +0,0 @@
- 'example.com' ) );
-
- $dispatched = 0;
-
- add_filter(
- 'pre_http_request',
- function () use ( &$dispatched ): array {
- $dispatched++;
- return array(
- 'body' => '{"data":[
- {
- "image_url":"https:\/\/example.com\/img.png",
- "thumb_url_medium":"https:\/\/example.com\/thumb.png",
- "title":"something",
- "url":"https:\/\/example.com"
- },
- {
- "image_url":"https:\/\/example.com\/img2.png",
- "thumb_url_medium":"https:\/\/example.com\/thumb2.png",
- "title":"something2",
- "url":"https:\/\/example.com\/2"
- }
- ]}',
- );
- }
- );
-
- $response = rest_get_server()->dispatch( new WP_REST_Request( 'GET', '/wp-parsely/v1/related' ) );
-
- self::assertSame( 1, $dispatched );
- self::assertSame( 200, $response->get_status() );
- self::assertEquals(
- (object) array(
- 'data' => array(
- (object) array(
- 'image_url' => 'https://example.com/img.png',
- 'thumb_url_medium' => 'https://example.com/thumb.png',
- 'title' => 'something',
- 'url' => 'https://example.com',
- ),
- (object) array(
- 'image_url' => 'https://example.com/img2.png',
- 'thumb_url_medium' => 'https://example.com/thumb2.png',
- 'title' => 'something2',
- 'url' => 'https://example.com/2',
- ),
- ),
- ),
- $response->get_data()
- );
- }
-}
diff --git a/tests/Integration/Endpoints/Proxy/StatsPostDetailProxyEndpointTest.php b/tests/Integration/Endpoints/Proxy/StatsPostDetailProxyEndpointTest.php
deleted file mode 100644
index c442d098a..000000000
--- a/tests/Integration/Endpoints/Proxy/StatsPostDetailProxyEndpointTest.php
+++ /dev/null
@@ -1,206 +0,0 @@
-set_current_user_to_admin();
- parent::run_test_get_items_fails_without_site_id_set();
- }
-
- /**
- * Verifies that calling `GET /wp-parsely/v1/analytics/post/detail` returns
- * an error and does not perform a remote call when the Site ID is not
- * populated in site options.
- *
- * @covers \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::get_items
- * @uses \Parsely\Endpoints\Base_API_Proxy::get_data
- * @uses \Parsely\Endpoints\Base_API_Proxy::register_endpoint
- * @uses \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::__construct
- * @uses \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::is_available_to_current_user
- * @uses \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::run
- * @uses \Parsely\Parsely::site_id_is_missing
- * @uses \Parsely\Parsely::site_id_is_set
- * @uses \Parsely\Parsely::api_secret_is_set
- * @uses \Parsely\Parsely::get_options
- * @uses \Parsely\Endpoints\Base_Endpoint::__construct
- */
- public function test_get_items_fails_when_api_secret_is_not_set(): void {
- $this->set_current_user_to_admin();
- parent::run_test_get_items_fails_without_api_secret_set();
- }
-
- /**
- * Verifies default user capability filter.
- *
- * @covers \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::is_available_to_current_user
- * @uses \Parsely\Endpoints\Base_Endpoint::__construct
- */
- public function test_user_is_allowed_to_make_proxy_api_call_if_default_user_capability_is_changed(): void {
- parent::run_test_user_is_allowed_to_make_proxy_api_call_if_default_user_capability_is_changed();
- }
-
- /**
- * Verifies endpoint specific user capability filter.
- *
- * @covers \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::is_available_to_current_user
- * @uses \Parsely\Endpoints\Base_Endpoint::__construct
- */
- public function test_user_is_allowed_to_make_proxy_api_call_if_endpoint_specific_user_capability_is_changed(): void {
- parent::run_test_user_is_allowed_to_make_proxy_api_call_if_endpoint_specific_user_capability_is_changed( 'analytics_post_detail' );
- }
-
- /**
- * Verifies that calls to `GET /wp-parsely/v1/analytics/post/detail` return
- * results in the expected format.
- *
- * @covers \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::get_items
- * @uses \Parsely\Endpoints\Base_API_Proxy::get_data
- * @uses \Parsely\Endpoints\Base_API_Proxy::register_endpoint
- * @uses \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::__construct
- * @uses \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::generate_data
- * @uses \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::is_available_to_current_user
- * @uses \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::run
- * @uses \Parsely\Parsely::site_id_is_missing
- * @uses \Parsely\Parsely::site_id_is_set
- * @uses \Parsely\Parsely::api_secret_is_set
- * @uses \Parsely\Parsely::get_site_id
- * @uses \Parsely\Parsely::get_options
- * @uses \Parsely\Endpoints\Base_Endpoint::__construct
- * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_api_url
- * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_items
- * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_request_options
- */
- public function test_get_items(): void {
- $this->set_current_user_to_admin();
- TestCase::set_options(
- array(
- 'apikey' => 'example.com',
- 'api_secret' => 'test',
- )
- );
-
- $dispatched = 0;
-
- add_filter(
- 'pre_http_request',
- function () use ( &$dispatched ): array {
- $dispatched++;
- return array(
- 'body' => '
- {"data":[{
- "avg_engaged": 1.911,
- "metrics": {
- "views": 2158,
- "visitors": 1537
- },
- "url": "https://example.com"
- }]}
- ',
- );
- }
- );
-
- $response = rest_get_server()->dispatch( new WP_REST_Request( 'GET', '/wp-parsely/v1/stats/post/detail' ) );
-
- self::assertSame( 1, $dispatched );
- self::assertSame( 200, $response->get_status() );
- self::assertEquals(
- (object) array(
- 'data' => array(
- (object) array(
- 'avgEngaged' => '1:55',
- 'dashUrl' => Parsely::DASHBOARD_BASE_URL . '/example.com/find?url=https%3A%2F%2Fexample.com',
- 'id' => 'https://example.com',
- 'postId' => 0,
- 'url' => 'https://example.com',
- 'views' => '2,158',
- 'visitors' => '1,537',
- 'rawUrl' => 'https://example.com',
- ),
- ),
- ),
- $response->get_data()
- );
- }
-}
diff --git a/tests/Integration/Endpoints/RestMetadataTest.php b/tests/Integration/Endpoints/RestMetadataTest.php
index 226e1071c..23854b34c 100644
--- a/tests/Integration/Endpoints/RestMetadataTest.php
+++ b/tests/Integration/Endpoints/RestMetadataTest.php
@@ -14,7 +14,6 @@
use Parsely\Endpoints\Rest_Metadata;
use Parsely\Tests\Integration\TestCase;
-
/**
* Integration Tests for the REST API Metadata Endpoint.
*/
diff --git a/tests/Integration/Endpoints/UserMeta/BaseUserMetaEndpointTest.php b/tests/Integration/Endpoints/UserMeta/BaseUserMetaEndpointTest.php
deleted file mode 100644
index c2ad8ec40..000000000
--- a/tests/Integration/Endpoints/UserMeta/BaseUserMetaEndpointTest.php
+++ /dev/null
@@ -1,169 +0,0 @@
-
- */
- protected $default_value = array();
-
- /**
- * Generates a JSON string for the passed period, metric, and extra data.
- *
- * @since 3.13.0
- *
- * @param string|null $metric The Metric value.
- * @param string|null $period The Period value.
- * @param array $extra_data Any Extra key/value pairs to add.
- * @return string The generated JSON string.
- */
- abstract protected function generate_json(
- ?string $metric = null,
- ?string $period = null,
- array $extra_data = array()
- ): string;
-
- /**
- * Verifies that the endpoint returns the correct default value.
- *
- * @since 3.13.0
- */
- public function run_test_endpoint_returns_value_on_get_request(): void {
- $this->set_current_user_to_admin();
-
- $value = rest_do_request(
- new WP_REST_Request(
- 'GET',
- self::$route
- )
- )->get_data();
-
- $expected = $this->wp_json_encode(
- $this->default_value
- );
-
- self::assertSame( $expected, $value );
- }
-
- /**
- * Provides data for testing PUT requests.
- *
- * @since 3.13.0
- * @return iterable
- */
- public function provide_put_requests_data(): iterable {
- $default_value = $this->generate_json( 'views', '7d' );
- $valid_value = $this->generate_json( 'avg_engaged', '1h' );
-
- // Valid non-default value. It should be returned unmodified.
- yield 'valid period and metric values' => array(
- 'test_data' => $valid_value,
- 'expected' => $valid_value,
- );
-
- // Missing or problematic keys. Defaults should be used for the missing or problematic keys.
- yield 'valid period value, no metric value' => array(
- 'test_data' => $this->generate_json( null, '1h' ),
- 'expected' => $this->generate_json( 'views', '1h' ),
- );
- yield 'valid metric value, no period value' => array(
- 'test_data' => $this->generate_json( 'avg_engaged' ),
- 'expected' => $this->generate_json( 'avg_engaged', '7d' ),
- );
- yield 'no values' => array(
- 'test_data' => $this->generate_json(),
- 'expected' => $default_value,
- );
-
- // Invalid values. They should be adjusted to their defaults.
- yield 'invalid period value' => array(
- 'test_data' => $this->generate_json( 'avg_engaged', 'invalid' ),
- 'expected' => $this->generate_json( 'avg_engaged', '7d' ),
- );
- yield 'invalid metric value' => array(
- 'test_data' => $this->generate_json( 'invalid', '1h' ),
- 'expected' => $this->generate_json( 'views', '1h' ),
- );
- yield 'invalid period and metric values' => array(
- 'test_data' => $this->generate_json( 'invalid', 'invalid' ),
- 'expected' => $default_value,
- );
-
- // Invalid extra data passed. Any such data should be discarded.
- yield 'invalid additional value' => array(
- 'test_data' => $this->generate_json(
- 'avg_engaged',
- '1h',
- array( 'invalid' )
- ),
- 'expected' => $valid_value,
- );
- yield 'invalid additional key/value pair' => array(
- 'test_data' => $this->generate_json(
- 'avg_engaged',
- '1h',
- array( 'invalid_key' => 'invalid_value' )
- ),
- 'expected' => $valid_value,
- );
- }
-
- /**
- * Sends a PUT request to the endpoint.
- *
- * @since 3.13.0
- *
- * @param string $data The data to be sent in the request.
- * @return string The response returned by the endpoint.
- */
- protected function send_put_request( string $data ): string {
- $this->set_current_user_to_admin();
- $result = $this->send_wp_rest_request( 'PUT', self::$route, $data );
-
- if ( ! is_string( $result ) ) {
- return '';
- }
-
- return $result;
- }
-
- /**
- * Verifies that the route is not registered when the respective filter is
- * set to false.
- *
- * @since 3.16.0
- */
- public function run_test_do_not_register_route_when_proxy_is_disabled(): void {
- // Override some setup steps in order to set the filter to false.
- remove_action( 'rest_api_init', $this->rest_api_init_proxy );
- $endpoint = $this->get_endpoint();
- $this->rest_api_init_proxy = static function () use ( $endpoint ) {
- add_filter( 'wp_parsely_enable_' . self::$filter_key . '_api_proxy', '__return_false' );
- $endpoint->run();
- };
- add_action( 'rest_api_init', $this->rest_api_init_proxy );
-
- $routes = rest_get_server()->get_routes();
- self::assertFalse( array_key_exists( self::$route, $routes ) );
- }
-}
diff --git a/tests/Integration/Endpoints/UserMeta/DashboardWidgetSettingsEndpointTest.php b/tests/Integration/Endpoints/UserMeta/DashboardWidgetSettingsEndpointTest.php
deleted file mode 100644
index 7af087282..000000000
--- a/tests/Integration/Endpoints/UserMeta/DashboardWidgetSettingsEndpointTest.php
+++ /dev/null
@@ -1,204 +0,0 @@
-
- */
- protected $default_value = array(
- 'Metric' => 'views',
- 'Period' => '7d',
- );
-
- /**
- * Initializes all required values for the test.
- *
- * @since 3.13.0
- */
- public static function initialize(): void {
- $route = Dashboard_Widget_Settings_Endpoint::get_route();
-
- self::$route = '/wp-parsely/v1' . $route;
- self::$filter_key = Utils::convert_endpoint_to_filter_key( $route );
- }
-
- /**
- * Returns the endpoint to be used in tests.
- *
- * @since 3.13.0
- *
- * @return Base_Endpoint_User_Meta The endpoint to be used in tests.
- */
- public function get_endpoint(): Base_Endpoint_User_Meta {
- return new Dashboard_Widget_Settings_Endpoint( new Parsely() );
- }
-
- /**
- * Verifies that the route is registered.
- *
- * @since 3.13.0
- *
- * @covers \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::get_route
- * @covers \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::run
- * @uses \Parsely\Endpoints\Base_Endpoint::__construct
- * @uses \Parsely\Endpoints\Base_Endpoint::register_endpoint
- * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::__construct
- * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::get_subvalues_specs
- * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::is_available_to_current_user
- * @uses \Parsely\Endpoints\User_Meta\Dashboard_Widget_Settings_Endpoint::get_subvalues_specs
- * @uses \Parsely\Parsely::__construct
- * @uses \Parsely\Parsely::allow_parsely_remote_requests
- * @uses \Parsely\Parsely::are_credentials_managed
- * @uses \Parsely\Parsely::set_managed_options
- * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
- */
- public function test_register_routes_by_default(): void {
- parent::run_test_register_routes_by_default(
- array(
- 'GET' => true,
- 'PUT' => true,
- )
- );
- }
-
- /**
- * Verifies that the route is not registered when the endpoint filter is set
- * to false.
- *
- * @since 3.13.0
- *
- * @covers \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::run
- * @uses \Parsely\Endpoints\Base_Endpoint::__construct
- * @uses \Parsely\Endpoints\Base_Endpoint::register_endpoint
- * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::__construct
- * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::get_route
- * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::get_subvalues_specs
- * @uses \Parsely\Endpoints\User_Meta\Dashboard_Widget_Settings_Endpoint::get_subvalues_specs
- * @uses \Parsely\Parsely::__construct
- * @uses \Parsely\Parsely::allow_parsely_remote_requests
- * @uses \Parsely\Parsely::are_credentials_managed
- * @uses \Parsely\Parsely::set_managed_options
- * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
- */
- public function test_verify_that_route_is_not_registered_when_endpoint_is_disabled(): void {
- parent::run_test_do_not_register_route_when_proxy_is_disabled();
- }
-
- /**
- * Verifies that the endpoint returns the correct default settings.
- *
- * @since 3.13.0
- *
- * @covers \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::__construct
- * @covers \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::get_subvalues_specs
- * @covers \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::get_value
- * @covers \Parsely\Endpoints\User_Meta\Dashboard_Widget_Settings_Endpoint::get_subvalues_specs
- * @covers \Parsely\Endpoints\User_Meta\Dashboard_Widget_Settings_Endpoint::process_request
- * @uses \Parsely\Endpoints\Base_Endpoint::__construct
- * @uses \Parsely\Endpoints\Base_Endpoint::register_endpoint
- * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::is_available_to_current_user
- * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::run
- * @uses \Parsely\Endpoints\User_Meta\Dashboard_Widget_Settings_Endpoint::get_meta_key
- * @uses \Parsely\Endpoints\User_Meta\Dashboard_Widget_Settings_Endpoint::get_route
- * @uses \Parsely\Parsely::__construct
- * @uses \Parsely\Parsely::allow_parsely_remote_requests
- * @uses \Parsely\Parsely::are_credentials_managed
- * @uses \Parsely\Parsely::set_managed_options
- * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
- */
- public function test_endpoint_returns_value_on_get_request(): void {
- parent::run_test_endpoint_returns_value_on_get_request();
- }
-
- /**
- * Verifies that the endpoint can correctly handle PUT requests.
- *
- * @since 3.13.0
- *
- * @param string $test_data The data to send in the PUT request.
- * @param string $expected The expected value of the setting after the PUT request.
- *
- * @covers \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::get_subvalues_specs
- * @covers \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::get_value
- * @covers \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::sanitize_subvalue
- * @covers \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::sanitize_value
- * @covers \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::set_value
- * @covers \Parsely\Endpoints\User_Meta\Dashboard_Widget_Settings_Endpoint::get_subvalues_specs
- * @covers \Parsely\Endpoints\User_Meta\Dashboard_Widget_Settings_Endpoint::process_request
- * @uses \Parsely\Endpoints\Base_Endpoint::__construct
- * @uses \Parsely\Endpoints\Base_Endpoint::register_endpoint
- * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::__construct
- * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::is_available_to_current_user
- * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::run
- * @uses \Parsely\Endpoints\User_Meta\Dashboard_Widget_Settings_Endpoint::get_meta_key
- * @uses \Parsely\Endpoints\User_Meta\Dashboard_Widget_Settings_Endpoint::get_route
- * @uses \Parsely\Parsely::__construct
- * @uses \Parsely\Parsely::allow_parsely_remote_requests
- * @uses \Parsely\Parsely::are_credentials_managed
- * @uses \Parsely\Parsely::set_managed_options
- * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
- *
- * @dataProvider provide_put_requests_data
- */
- public function test_endpoint_correctly_handles_put_requests(
- string $test_data,
- string $expected
- ): void {
- $value = $this->send_put_request( $test_data );
- self::assertSame( $expected, $value );
- }
-
- /**
- * Generates a JSON string for the passed period, metric, and extra data.
- *
- * @since 3.13.0
- *
- * @param string|null $metric The Metric value.
- * @param string|null $period The Period value.
- * @param array $extra_data Any Extra key/value pairs to add.
- * @return string The generated JSON string.
- */
- protected function generate_json(
- ?string $metric = null,
- ?string $period = null,
- array $extra_data = array()
- ): string {
- $array = $this->default_value;
- unset( $array['Metric'], $array['Period'] );
-
- if ( null !== $metric ) {
- $array['Metric'] = $metric;
- }
-
- if ( null !== $period ) {
- $array['Period'] = $period;
- }
-
- ksort( $array );
-
- return $this->wp_json_encode( array_merge( $array, $extra_data ) );
- }
-}
diff --git a/tests/Integration/Endpoints/UserMeta/EditorSidebarSettingsEndpointTest.php b/tests/Integration/Endpoints/UserMeta/EditorSidebarSettingsEndpointTest.php
deleted file mode 100644
index 00071489b..000000000
--- a/tests/Integration/Endpoints/UserMeta/EditorSidebarSettingsEndpointTest.php
+++ /dev/null
@@ -1,309 +0,0 @@
-
- */
- protected $default_value = array(
- 'InitialTabName' => 'tools',
- 'PerformanceStats' => array(
- 'Period' => '7d',
- 'VisibleDataPoints' => array( 'views', 'visitors', 'avgEngaged', 'recirculation' ),
- 'VisiblePanels' => array( 'overview', 'categories', 'referrers' ),
- ),
- 'RelatedPosts' => array(
- 'FilterBy' => 'unavailable',
- 'FilterValue' => '',
- 'Metric' => 'views',
- 'Open' => false,
- 'Period' => '7d',
- ),
- 'SmartLinking' => array(
- 'MaxLinks' => 10,
- 'MaxLinkWords' => 4,
- 'Open' => false,
- ),
- 'TitleSuggestions' => array(
- 'Open' => false,
- 'Persona' => 'journalist',
- 'Tone' => 'neutral',
- ),
- );
-
- /**
- * Initializes all required values for the test.
- *
- * @since 3.13.0
- */
- public static function initialize(): void {
- $route = Editor_Sidebar_Settings_Endpoint::get_route();
-
- self::$route = '/wp-parsely/v1' . $route;
- self::$filter_key = Utils::convert_endpoint_to_filter_key( $route );
- }
-
- /**
- * Returns the endpoint to be used in tests.
- *
- * @since 3.13.0
- *
- * @return Base_Endpoint_User_Meta The endpoint to be used in tests.
- */
- public function get_endpoint(): Base_Endpoint_User_Meta {
- return new Editor_Sidebar_Settings_Endpoint( new Parsely() );
- }
-
- /**
- * Verifies that the route is registered.
- *
- * @since 3.13.0
- *
- * @covers \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::get_route
- * @covers \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::run
- * @uses \Parsely\Endpoints\Base_Endpoint::__construct
- * @uses \Parsely\Endpoints\Base_Endpoint::register_endpoint
- * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::__construct
- * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::get_subvalues_specs
- * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::is_available_to_current_user
- * @uses \Parsely\Endpoints\User_Meta\Editor_Sidebar_Settings_Endpoint::get_subvalues_specs
- * @uses \Parsely\Parsely::__construct
- * @uses \Parsely\Parsely::allow_parsely_remote_requests
- * @uses \Parsely\Parsely::are_credentials_managed
- * @uses \Parsely\Parsely::set_managed_options
- * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
- */
- public function test_register_routes_by_default(): void {
- parent::run_test_register_routes_by_default(
- array(
- 'GET' => true,
- 'PUT' => true,
- )
- );
- }
-
- /**
- * Verifies that the route is not registered when the endpoint filter is set
- * to false.
- *
- * @since 3.13.0
- *
- * @covers \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::run
- * @uses \Parsely\Endpoints\Base_Endpoint::__construct
- * @uses \Parsely\Endpoints\Base_Endpoint::register_endpoint
- * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::__construct
- * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::get_route
- * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::get_subvalues_specs
- * @uses \Parsely\Endpoints\User_Meta\Editor_Sidebar_Settings_Endpoint::get_subvalues_specs
- * @uses \Parsely\Parsely::__construct
- * @uses \Parsely\Parsely::allow_parsely_remote_requests
- * @uses \Parsely\Parsely::are_credentials_managed
- * @uses \Parsely\Parsely::set_managed_options
- * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
- */
- public function test_verify_that_route_is_not_registered_when_endpoint_is_disabled(): void {
- parent::run_test_do_not_register_route_when_proxy_is_disabled();
- }
-
- /**
- * Verifies that the endpoint returns the correct default settings.
- *
- * @since 3.13.0
- *
- * @covers \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::__construct
- * @covers \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::get_subvalues_specs
- * @covers \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::get_value
- * @covers \Parsely\Endpoints\User_Meta\Editor_Sidebar_Settings_Endpoint::get_subvalues_specs
- * @covers \Parsely\Endpoints\User_Meta\Editor_Sidebar_Settings_Endpoint::process_request
- * @uses \Parsely\Endpoints\Base_Endpoint::__construct
- * @uses \Parsely\Endpoints\Base_Endpoint::register_endpoint
- * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::is_available_to_current_user
- * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::run
- * @uses \Parsely\Endpoints\User_Meta\Editor_Sidebar_Settings_Endpoint::get_meta_key
- * @uses \Parsely\Endpoints\User_Meta\Editor_Sidebar_Settings_Endpoint::get_route
- * @uses \Parsely\Parsely::__construct
- * @uses \Parsely\Parsely::allow_parsely_remote_requests
- * @uses \Parsely\Parsely::are_credentials_managed
- * @uses \Parsely\Parsely::set_managed_options
- * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
- */
- public function test_endpoint_returns_value_on_get_request(): void {
- parent::run_test_endpoint_returns_value_on_get_request();
- }
-
- /**
- * Verifies that the endpoint can correctly handle PUT requests.
- *
- * @since 3.13.0
- *
- * @param string $test_data The data to send in the PUT request.
- * @param string $expected The expected value of the setting after the PUT request.
- *
- * @covers \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::get_subvalues_specs
- * @covers \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::get_value
- * @covers \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::sanitize_subvalue
- * @covers \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::sanitize_value
- * @covers \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::set_value
- * @covers \Parsely\Endpoints\User_Meta\Editor_Sidebar_Settings_Endpoint::get_subvalues_specs
- * @covers \Parsely\Endpoints\User_Meta\Editor_Sidebar_Settings_Endpoint::process_request
- * @uses \Parsely\Endpoints\Base_Endpoint::__construct
- * @uses \Parsely\Endpoints\Base_Endpoint::register_endpoint
- * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::__construct
- * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::get_default
- * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::get_nested_specs
- * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::get_valid_values
- * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::is_available_to_current_user
- * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::is_valid_key
- * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::run
- * @uses \Parsely\Endpoints\User_Meta\Editor_Sidebar_Settings_Endpoint::get_meta_key
- * @uses \Parsely\Endpoints\User_Meta\Editor_Sidebar_Settings_Endpoint::get_route
- * @uses \Parsely\Parsely::__construct
- * @uses \Parsely\Parsely::allow_parsely_remote_requests
- * @uses \Parsely\Parsely::are_credentials_managed
- * @uses \Parsely\Parsely::set_managed_options
- * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
- * @dataProvider provide_put_requests_data
- */
- public function test_endpoint_correctly_handles_put_requests(
- string $test_data,
- string $expected
- ): void {
- $value = $this->send_put_request( $test_data );
- self::assertSame( $expected, $value );
- }
-
- /**
- * Tests that the endpoint can correctly handle PUT requests with valid
- * nested PerformanceStats values.
- *
- * @since 3.14.0
- *
- * @covers \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::sanitize_subvalue
- * @uses \Parsely\Endpoints\Base_Endpoint::__construct()
- * @uses \Parsely\Endpoints\Base_Endpoint::register_endpoint()
- * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::__construct()
- * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::get_route()
- * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::get_value()
- * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::is_available_to_current_user()
- * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::process_request()
- * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::run()
- * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::sanitize_value()
- * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::set_value()
- * @uses \Parsely\Endpoints\User_Meta\Editor_Sidebar_Settings_Endpoint::get_meta_key()
- * @uses \Parsely\Endpoints\User_Meta\Editor_Sidebar_Settings_Endpoint::get_subvalues_specs()
- * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::get_nested_specs
- * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::get_valid_values
- * @uses \Parsely\Parsely::__construct()
- * @uses \Parsely\Parsely::allow_parsely_remote_requests()
- * @uses \Parsely\Parsely::are_credentials_managed()
- * @uses \Parsely\Parsely::set_managed_options()
- * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key()
- */
- public function test_valid_nested_performance_stats_settings_period(): void {
- $this->set_current_user_to_admin();
-
- $value = $this->send_put_request(
- $this->generate_json(
- 'views',
- '7d',
- array(
- 'PerformanceStats' => array(
- 'Period' => '1h',
- 'VisibleDataPoints' => array( 'views', 'avgEngaged', 'recirculation' ),
- 'VisiblePanels' => array( 'overview', 'referrers' ),
- ),
- )
- )
- );
-
- $expected = $this->wp_json_encode(
- array_merge(
- $this->default_value,
- array(
- 'PerformanceStats' => array(
- 'Period' => '1h',
- 'VisibleDataPoints' => array( 'views', 'avgEngaged', 'recirculation' ),
- 'VisiblePanels' => array( 'overview', 'referrers' ),
- ),
- )
- )
- );
-
- self::assertSame( $expected, $value );
- }
-
- /**
- * Generates a JSON string for the passed period, metric, and extra data.
- *
- * @since 3.13.0
- *
- * @param string|null $metric The RelatedPostsMetric value.
- * @param string|null $period The RelatedPostsPeriod value.
- * @param array $extra_data Any Extra key/value pairs to add.
- * @return string The generated JSON string.
- */
- protected function generate_json(
- ?string $metric = null,
- ?string $period = null,
- array $extra_data = array()
- ): string {
- $array = $this->default_value;
- assert( is_array( $array['RelatedPosts'] ) );
-
- unset( $array['RelatedPosts']['Metric'], $array['RelatedPosts']['Period'] );
-
- if ( null !== $metric ) {
- $array['RelatedPosts']['Metric'] = $metric;
- }
-
- if ( null !== $period ) {
- $array['RelatedPosts']['Period'] = $period;
- }
-
- $merged_array = array_merge( $array, $extra_data );
-
- $this->ksortRecursive( $merged_array, SORT_NATURAL | SORT_FLAG_CASE );
-
- return $this->wp_json_encode( $merged_array );
- }
-
- /**
- * Recursively sorts an array by key using a specified sort flag.
- *
- * @since 3.14.3
- *
- * @param array &$unsorted_array The array to be sorted, passed by reference.
- * @param int $sort_flags Optional sorting flags. Defaults to SORT_REGULAR.
- */
- private function ksortRecursive( array &$unsorted_array, int $sort_flags = SORT_REGULAR ): void {
- ksort( $unsorted_array, $sort_flags );
- foreach ( $unsorted_array as &$value ) {
- if ( is_array( $value ) ) {
- $this->ksortRecursive( $value, $sort_flags );
- }
- }
- }
-}
diff --git a/tests/Integration/RemoteAPI/AnalyticsPostsRemoteAPITest.php b/tests/Integration/RemoteAPI/AnalyticsPostsRemoteAPITest.php
deleted file mode 100644
index 8eb7dc105..000000000
--- a/tests/Integration/RemoteAPI/AnalyticsPostsRemoteAPITest.php
+++ /dev/null
@@ -1,128 +0,0 @@
-
- */
- public function data_api_url(): iterable {
- yield 'Basic (Expected data)' => array(
- array(
- 'apikey' => 'my-key',
- 'limit' => 5,
- ),
- Parsely::PUBLIC_API_BASE_URL . '/analytics/posts?apikey=my-key&limit=5',
- );
- }
-
- /**
- * Verifies default user capability filter.
- *
- * @covers \Parsely\RemoteAPI\Analytics_Posts_API::is_available_to_current_user
- * @uses \Parsely\Endpoints\Base_Endpoint::__construct
- * @uses \Parsely\Endpoints\Base_Endpoint::apply_capability_filters
- * @uses \Parsely\Parsely::__construct
- * @uses \Parsely\Parsely::allow_parsely_remote_requests
- * @uses \Parsely\Parsely::are_credentials_managed
- * @uses \Parsely\Parsely::set_managed_options
- * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
- */
- public function test_user_is_allowed_to_make_api_call_if_default_user_capability_is_changed(): void {
- $this->set_current_user_to_contributor();
- add_filter(
- 'wp_parsely_user_capability_for_all_private_apis',
- function () {
- return 'edit_posts';
- }
- );
-
- $api = new Analytics_Posts_API( new Parsely() );
-
- self::assertTrue( $api->is_available_to_current_user() );
- }
-
- /**
- * Verifies endpoint specific user capability filter.
- *
- * @covers \Parsely\RemoteAPI\Analytics_Posts_API::is_available_to_current_user
- * @uses \Parsely\Endpoints\Base_Endpoint::__construct
- * @uses \Parsely\Endpoints\Base_Endpoint::apply_capability_filters
- * @uses \Parsely\Parsely::__construct
- * @uses \Parsely\Parsely::allow_parsely_remote_requests
- * @uses \Parsely\Parsely::are_credentials_managed
- * @uses \Parsely\Parsely::set_managed_options
- * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
- */
- public function test_user_is_allowed_to_make_api_call_if_endpoint_specific_user_capability_is_changed(): void {
- $this->set_current_user_to_contributor();
- add_filter(
- 'wp_parsely_user_capability_for_analytics_posts_api',
- function () {
- return 'edit_posts';
- }
- );
-
- $api = new Analytics_Posts_API( new Parsely() );
-
- self::assertTrue( $api->is_available_to_current_user() );
- }
-
- /**
- * Verifies that the endpoint specific user capability filter has more priority than the default capability filter.
- *
- * @covers \Parsely\RemoteAPI\Analytics_Posts_API::is_available_to_current_user
- * @uses \Parsely\Endpoints\Base_Endpoint::__construct
- * @uses \Parsely\Endpoints\Base_Endpoint::apply_capability_filters
- * @uses \Parsely\Parsely::__construct
- * @uses \Parsely\Parsely::allow_parsely_remote_requests
- * @uses \Parsely\Parsely::are_credentials_managed
- * @uses \Parsely\Parsely::set_managed_options
- * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
- */
- public function test_endpoint_specific_user_capability_filter_have_more_priority_than_default(): void {
- $this->set_current_user_to_contributor();
-
- add_filter(
- 'wp_parsely_user_capability_for_all_private_apis',
- function () {
- return 'publish_posts';
- }
- );
-
- add_filter(
- 'wp_parsely_user_capability_for_analytics_posts_api',
- function () {
- return 'edit_posts';
- }
- );
-
- $api = new Analytics_Posts_API( new Parsely() );
-
- self::assertTrue( $api->is_available_to_current_user() );
- }
-}
diff --git a/tests/Integration/RemoteAPI/BaseRemoteAPITest.php b/tests/Integration/RemoteAPI/BaseRemoteAPITest.php
deleted file mode 100644
index 6f978d11c..000000000
--- a/tests/Integration/RemoteAPI/BaseRemoteAPITest.php
+++ /dev/null
@@ -1,163 +0,0 @@
-
- */
- abstract public function data_api_url(): iterable;
-
- /**
- * Runs once before all tests.
- */
- public static function set_up_before_class(): void {
- static::initialize();
- }
-
- /**
- * Verifies the basic generation of the API URL.
- *
- * @dataProvider data_api_url
- * @covers \Parsely\RemoteAPI\Related_API::get_api_url
- * @covers \Parsely\RemoteAPI\Analytics_Posts_API::get_api_url
- * @uses \Parsely\Endpoints\Base_Endpoint::__construct
- * @uses \Parsely\Parsely::api_secret_is_set
- * @uses \Parsely\Parsely::get_managed_credentials
- * @uses \Parsely\Parsely::get_options
- * @uses \Parsely\Parsely::get_site_id
- * @uses \Parsely\Parsely::site_id_is_set
- * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::validate_required_constraints
- * @uses \Parsely\RemoteAPI\ContentSuggestions\Content_Suggestions_Base_API::get_api_url
- *
- * @param array $query Test query arguments.
- * @param string $url Expected generated URL.
- */
- public function test_api_url( array $query, string $url ): void {
- self::set_options( array( 'apikey' => 'my-key' ) );
- self::assertSame( $url, self::$remote_api->get_api_url( $query ) );
- }
-
- /**
- * Verifies that the cache is used instead of the api when there's a cache
- * hit.
- *
- * @covers \Parsely\RemoteAPI\Remote_API_Cache::get_items
- * @covers \Parsely\RemoteAPI\Remote_API_Cache::__construct
- * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_endpoint
- */
- public function test_remote_api_cache_returns_cached_value(): void {
- $api_mock = $this->getMockBuilder( get_class( self::$remote_api ) )
- ->disableOriginalConstructor()
- ->getMock();
-
- // If this method is called, that means our cache did not hit as expected.
- $api_mock->expects( self::never() )->method( 'get_items' );
- $api_mock->method( 'get_endpoint' )->willReturn( self::$remote_api->get_endpoint() ); // Passing call to non-mock method.
-
- $cache_key = 'parsely_api_' . wp_hash( self::$remote_api->get_endpoint() ) . '_' . wp_hash( $this->wp_json_encode( array() ) );
-
- $object_cache = $this->createMock( Cache::class );
- $object_cache->method( 'get' )
- ->willReturn( (object) array( 'cache_hit' => true ) );
-
- $object_cache->expects( self::once() )
- ->method( 'get' )
- ->with(
- self::equalTo( $cache_key ),
- self::equalTo( 'wp-parsely' ),
- self::equalTo( false ),
- self::isNull()
- );
-
- /**
- * Variable.
- *
- * @var Remote_API_Cache
- */
- $remote_api_cache = $this->getMockBuilder( Remote_API_Cache::class )
- ->setConstructorArgs( array( $api_mock, $object_cache ) )
- ->setMethodsExcept( array( 'get_items' ) )
- ->getMock();
-
- self::assertEquals( (object) array( 'cache_hit' => true ), $remote_api_cache->get_items( array() ) );
- }
-
- /**
- * Verifies that when the cache misses, the api is used instead and the
- * resultant value is cached.
- *
- * @covers \Parsely\RemoteAPI\Remote_API_Cache::get_items
- * @covers \Parsely\RemoteAPI\Remote_API_Cache::__construct
- * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_endpoint
- */
- public function test_caching_decorator_returns_uncached_value(): void {
- $api_mock = $this->getMockBuilder( get_class( self::$remote_api ) )
- ->disableOriginalConstructor()
- ->getMock();
-
- $api_mock->method( 'get_items' )
- ->willReturn( (object) array( 'cache_hit' => false ) );
-
- // If this method is _NOT_ called, that means our cache did not miss as expected.
- $api_mock->expects( self::once() )->method( 'get_items' );
- $api_mock->method( 'get_endpoint' )->willReturn( self::$remote_api->get_endpoint() ); // Passing call to non-mock method.
-
- $cache_key = 'parsely_api_' . wp_hash( self::$remote_api->get_endpoint() ) . '_' . wp_hash( $this->wp_json_encode( array() ) );
-
- $object_cache = $this->createMock( Cache::class );
- $object_cache->method( 'get' )
- ->willReturn( false );
-
- $object_cache->expects( self::once() )
- ->method( 'get' )
- ->with(
- self::equalTo( $cache_key ),
- self::equalTo( 'wp-parsely' ),
- self::equalTo( false ),
- self::isNull()
- );
-
- /**
- * Variable.
- *
- * @var Remote_API_Cache
- */
- $remote_api_cache = $this->getMockBuilder( Remote_API_Cache::class )
- ->setConstructorArgs( array( $api_mock, $object_cache ) )
- ->setMethodsExcept( array( 'get_items' ) )
- ->getMock();
-
- self::assertEquals( (object) array( 'cache_hit' => false ), $remote_api_cache->get_items( array() ) );
- }
-}
diff --git a/tests/Integration/RemoteAPI/ContentSuggestions/BaseContentSuggestionsAPITest.php b/tests/Integration/RemoteAPI/ContentSuggestions/BaseContentSuggestionsAPITest.php
deleted file mode 100644
index 708b08a6c..000000000
--- a/tests/Integration/RemoteAPI/ContentSuggestions/BaseContentSuggestionsAPITest.php
+++ /dev/null
@@ -1,129 +0,0 @@
- 'my-key',
- 'api_secret' => 'my-secret',
- )
- );
-
- $request_options = self::$remote_api->get_request_options();
-
- // Ensure that $request_options is an array and 'headers' key exists.
- self::assertIsArray( $request_options );
- self::assertArrayHasKey( 'headers', $request_options );
-
- $headers = $request_options['headers'];
- self::assertIsArray( $headers ); // Ensures $headers is indeed an array.
-
- // Verify the Content-Type header is present and its value is application/json.
- self::assertArrayHasKey( 'Content-Type', $headers );
- self::assertEquals( 'application/json; charset=utf-8', $headers['Content-Type'] );
-
- // Verify the API key is present in the headers and its value matches the one set in the options.
- self::assertArrayHasKey( 'X-APIKEY-SECRET', $headers );
- self::assertEquals( 'my-secret', $headers['X-APIKEY-SECRET'] );
- }
-
- /**
- * Verifies that the truncate function is properly truncated long content on the body array.
- *
- * @since 3.14.1
- *
- * @covers \Parsely\RemoteAPI\ContentSuggestions\Content_Suggestions_Base_API::truncate_array_content
- */
- public function test_truncate_body_content(): void {
- /**
- * @var Content_Suggestions_Base_API $remote_api
- */
- $remote_api = self::$remote_api;
-
- $body = array(
- 'output_params' => array(
- 'some_param' => true,
- 'other_param' => 'Hello',
- 'recursive' => array(
- 'key' => 'value',
- ),
- ),
- 'text' => $this->generate_content_with_length( 30000 ),
- 'something' => 'else',
- );
-
- $truncated_array = $remote_api->truncate_array_content( $body );
-
- self::assertIsArray( $truncated_array );
- self::assertArrayHasKey( 'output_params', $truncated_array );
- self::assertArrayHasKey( 'text', $truncated_array );
- self::assertLessThanOrEqual( 25000, strlen( $truncated_array['text'] ) );
-
- // Assert that the truncated text is the beginning of the original text.
- self::assertStringStartsWith( $truncated_array['text'], $body['text'] );
-
- // Assert that the other keys are the same in both arrays.
- self::assertEquals( $body['output_params'], $truncated_array['output_params'] );
- self::assertEquals( $body['something'], $truncated_array['something'] );
- }
-
- /**
- * Generate content with a specific length.
- *
- * @since 3.14.1
- *
- * @param int $length Length of the generated content.
- *
- * @return string The generated content.
- */
- private function generate_content_with_length( int $length ): string {
- $words = array( 'lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing', 'elit' );
- $string = '';
- $current_length = 0;
- while ( $current_length < $length ) {
- $word = $words[ array_rand( $words ) ];
- if ( $current_length > 0 ) {
- if ( $current_length + strlen( $word ) + 1 > $length ) {
- break;
- }
- $string .= ' ';
- ++$current_length;
- }
- $string .= $word;
- $current_length += strlen( $word );
- }
- return $string;
- }
-}
diff --git a/tests/Integration/RemoteAPI/ContentSuggestions/SuggestBriefAPITest.php b/tests/Integration/RemoteAPI/ContentSuggestions/SuggestBriefAPITest.php
deleted file mode 100644
index 6c3ab62e1..000000000
--- a/tests/Integration/RemoteAPI/ContentSuggestions/SuggestBriefAPITest.php
+++ /dev/null
@@ -1,137 +0,0 @@
-
- */
- public function data_api_url(): iterable {
- yield 'Basic (Expected data)' => array(
- array(
- 'apikey' => 'my-key',
- ),
- Parsely::PUBLIC_SUGGESTIONS_API_BASE_URL .
- '/suggest-brief?apikey=my-key',
- );
- }
-
-
- /**
- * Mocks a successful HTTP response to the Content Suggestion suggest-brief
- * API endpoint.
- *
- * @since 3.13.0
- *
- * @param string $response The response to mock.
- * @param array $args The arguments passed to the HTTP request.
- * @param string $url The URL of the HTTP request.
- * @return array|false The mocked response.
- *
- * @phpstan-ignore-next-line
- */
- public function mock_successful_suggest_brief_response(
- string $response,
- array $args,
- string $url
- ) {
- if ( ! str_contains( $url, 'suggest-brief' ) ) {
- return false;
- }
-
- $response = array(
- 'result' => array(
- 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
- ),
- );
-
- return array(
- 'headers' => array(),
- 'cookies' => array(),
- 'filename' => null,
- 'response' => array(
- 'code' => 200,
- 'message' => 'OK',
- ),
- 'status_code' => 200,
- 'success' => true,
- 'body' => $this->wp_json_encode( $response ),
- );
- }
-
- /**
- * Tests getting meta description from the API with some generic content.
- *
- * @since 3.13.0
- *
- * @covers \Parsely\RemoteAPI\ContentSuggestions\Suggest_Brief_API::get_suggestion
- * @uses \Parsely\Parsely::api_secret_is_set()
- * @uses \Parsely\Parsely::get_managed_credentials()
- * @uses \Parsely\Parsely::get_options()
- * @uses \Parsely\Parsely::get_site_id()
- * @uses \Parsely\Parsely::set_default_track_as_values()
- * @uses \Parsely\Parsely::site_id_is_set()
- * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::validate_required_constraints()
- * @uses \Parsely\RemoteAPI\ContentSuggestions\Content_Suggestions_Base_API::get_api_url()
- * @uses \Parsely\RemoteAPI\ContentSuggestions\Content_Suggestions_Base_API::get_request_options()
- * @uses \Parsely\RemoteAPI\ContentSuggestions\Content_Suggestions_Base_API::post_request()
- */
- public function test_get_suggestion(): void {
- $title = 'Lorem Ipsum is a random title';
- $content = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.
';
- $persona = 'journalist';
- $style = 'neutral';
-
- // Mock API result.
- add_filter( 'pre_http_request', array( $this, 'mock_successful_suggest_brief_response' ), 10, 3 );
-
- // Test getting meta description.
- $brief = self::$suggest_brief_api->get_suggestion( $title, $content, $persona, $style );
-
- self::assertIsString( $brief );
- self::assertEquals( 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', $brief );
-
- // Remove mock.
- remove_filter( 'pre_http_request', array( $this, 'mock_successful_suggest_brief_response' ) );
- }
-}
diff --git a/tests/Integration/RemoteAPI/RelatedRemoteAPITest.php b/tests/Integration/RemoteAPI/RelatedRemoteAPITest.php
deleted file mode 100644
index 52c4ab80d..000000000
--- a/tests/Integration/RemoteAPI/RelatedRemoteAPITest.php
+++ /dev/null
@@ -1,94 +0,0 @@
-
- */
- public function data_api_url(): iterable {
- yield 'Basic (Expected data)' => array(
- array(
- 'apikey' => 'my-key',
- 'pub_date_start' => '7d',
- 'sort' => 'score',
- 'limit' => 5,
- ),
- Parsely::PUBLIC_API_BASE_URL . '/related?apikey=my-key&limit=5&pub_date_start=7d&sort=score',
- );
-
- yield 'published_within value of 0' => array(
- array(
- 'apikey' => 'my-key',
- 'sort' => 'score',
- 'limit' => 5,
- ),
- Parsely::PUBLIC_API_BASE_URL . '/related?apikey=my-key&limit=5&sort=score',
- );
-
- yield 'Sort on publish date' => array(
- array(
- 'apikey' => 'my-key',
- 'sort' => 'pub_date',
- 'limit' => 5,
- ),
- Parsely::PUBLIC_API_BASE_URL . '/related?apikey=my-key&limit=5&sort=pub_date',
- );
-
- yield 'Rank by relevance only' => array(
- array(
- 'apikey' => 'my-key',
- 'sort' => 'score',
- 'limit' => 5,
- ),
- Parsely::PUBLIC_API_BASE_URL . '/related?apikey=my-key&limit=5&sort=score',
- );
- }
-
- /**
- * Verifies that the endpoint does not have filters that check user capability.
- *
- * @covers \Parsely\RemoteAPI\Related_API::is_available_to_current_user
- * @uses \Parsely\Endpoints\Base_Endpoint::__construct
- * @uses \Parsely\Parsely::get_managed_credentials
- * @uses \Parsely\Parsely::__construct
- * @uses \Parsely\Parsely::allow_parsely_remote_requests
- * @uses \Parsely\Parsely::are_credentials_managed
- * @uses \Parsely\Parsely::set_managed_options
- */
- public function test_related_endpoint_does_not_have_user_capability_filters(): void {
- $api = new Related_API( new Parsely() );
-
- self::assertTrue( $api->is_available_to_current_user() );
- $this->assert_wp_hooks_availability(
- array(
- 'wp_parsely_user_capability_for_all_private_apis',
- 'wp_parsely_user_capability_for_related_api',
- ),
- false
- );
- }
-}
diff --git a/tests/Integration/RestAPI/BaseAPIControllerTest.php b/tests/Integration/RestAPI/BaseAPIControllerTest.php
new file mode 100644
index 000000000..4211b10bb
--- /dev/null
+++ b/tests/Integration/RestAPI/BaseAPIControllerTest.php
@@ -0,0 +1,270 @@
+test_controller = new class($parsely) extends Base_API_Controller {
+ /**
+ * Gets the namespace for the API.
+ *
+ * @since 3.17.0
+ *
+ * @return string The namespace.
+ */
+ protected function get_namespace(): string {
+ return 'test';
+ }
+
+ /**
+ * Gets the route prefix, which acts as a namespace for the endpoints.
+ *
+ * @since 3.17.0
+ *
+ * @return string The route prefix.
+ */
+ public static function get_route_prefix(): string {
+ return 'test';
+ }
+
+ /**
+ * Gets the version for the API.
+ *
+ * @since 3.17.0
+ *
+ * @return string The version.
+ */
+ protected function get_version(): string {
+ return 'v1';
+ }
+
+ /**
+ * Initializes the test controller.
+ *
+ * @since 3.17.0
+ */
+ protected function init(): void {}
+
+ /**
+ * Exposes the protected method for testing.
+ *
+ * @since 3.17.0
+ *
+ * @param Base_Endpoint[] $endpoints The endpoints to register.
+ */
+ public function testable_register_endpoints( array $endpoints ): void {
+ $this->register_endpoints( $endpoints );
+ }
+
+ /**
+ * Exposes the protected method for testing.
+ *
+ * @since 3.17.0
+ *
+ * @param Base_Endpoint $endpoint The endpoint to register.
+ */
+ public function testable_register_endpoint( Base_Endpoint $endpoint ): void {
+ $this->register_endpoint( $endpoint );
+ }
+
+ /**
+ * Checks if a specific endpoint is available to the current user.
+ *
+ * @since 3.17.0
+ *
+ * @param string $endpoint The endpoint to check.
+ * @return bool True if the controller is available to the current user, false otherwise.
+ */
+ public function is_available_to_current_user( string $endpoint ): bool {
+ return true;
+ }
+ };
+ }
+
+ /**
+ * Tests the get_namespace method.
+ *
+ * @since 3.17.0
+ *
+ * @covers \Parsely\REST_API\Base_API_Controller::get_full_namespace
+ * @uses \Parsely\REST_API\Base_API_Controller::__construct
+ */
+ public function test_get_namespace(): void {
+ self::assertEquals( 'test/v1', $this->test_controller->get_full_namespace() );
+ }
+
+ /**
+ * Tests the prefix_route method.
+ *
+ * @since 3.17.0
+ *
+ * @covers \Parsely\REST_API\Base_API_Controller::prefix_route
+ * @uses \Parsely\REST_API\Base_API_Controller::__construct
+ */
+ public function test_prefix_route(): void {
+ self::assertEquals( 'test/my-route', $this->test_controller->prefix_route( 'my-route' ) );
+ $parsely = self::createMock( Parsely::class );
+
+ $controller_without_prefix = new class($parsely) extends Base_API_Controller {
+ /**
+ * Initialize the test controller.
+ *
+ * @since 3.17.0
+ */
+ protected function init(): void {}
+
+ /**
+ * Get the namespace for the API.
+ *
+ * @since 3.17.0
+ *
+ * @return string The namespace.
+ */
+ protected function get_namespace(): string {
+ return 'test';
+ }
+
+ /**
+ * Checks if a specific endpoint is available to the current user.
+ *
+ * @since 3.17.0
+ *
+ * @param string $endpoint The endpoint to check.
+ * @return bool True if the controller is available to the current user, false otherwise.
+ */
+ public function is_available_to_current_user( string $endpoint ): bool {
+ return true;
+ }
+ };
+
+ self::assertEquals( 'my-route', $controller_without_prefix->prefix_route( 'my-route' ) );
+ }
+
+ /**
+ * Tests that endpoints are registered correctly.
+ *
+ * @since 3.17.0
+ *
+ * @covers \Parsely\REST_API\Base_API_Controller::register_endpoint
+ * @uses \Parsely\REST_API\Base_API_Controller::__construct
+ * @uses \Parsely\REST_API\Base_API_Controller::register_endpoint
+ */
+ public function test_register_endpoint(): void {
+ $endpoint = self::createMock( Base_Endpoint::class );
+ $endpoint->expects( self::once() )
+ ->method( 'get_endpoint_slug' )
+ ->willReturn( 'test' );
+
+ $this->test_controller->testable_register_endpoint( $endpoint ); // @phpstan-ignore-line
+
+ $endpoints = $this->test_controller->get_endpoints();
+ self::assertCount( 1, $endpoints );
+ self::assertSame( $endpoint, $endpoints['test'] );
+ }
+
+ /**
+ * Tests that multiple endpoints are registered correctly using a helper method.
+ *
+ * @since 3.17.0
+ *
+ * @covers \Parsely\REST_API\Base_API_Controller::register_endpoints
+ * @uses \Parsely\REST_API\Base_API_Controller::__construct
+ * @uses \Parsely\REST_API\Base_API_Controller::register_endpoint
+ * @uses \Parsely\REST_API\Base_API_Controller::register_endpoints
+ */
+ public function test_register_multiple_endpoints(): void {
+ $endpoint1 = self::createMock( Base_Endpoint::class );
+ $endpoint1->expects( self::once() )
+ ->method( 'get_endpoint_slug' )
+ ->willReturn( 'test1' );
+
+ $endpoint2 = self::createMock( Base_Endpoint::class );
+ $endpoint2->expects( self::once() )
+ ->method( 'get_endpoint_slug' )
+ ->willReturn( 'test2' );
+
+
+ $this->test_controller->testable_register_endpoints( array( $endpoint1, $endpoint2 ) ); // @phpstan-ignore-line
+
+ $endpoints = $this->test_controller->get_endpoints();
+
+ self::assertCount( 2, $endpoints );
+ self::assertSame( $endpoint1, $endpoints['test1'] );
+ self::assertSame( $endpoint2, $endpoints['test2'] );
+ }
+
+ /**
+ * Tests that the get_endpoint_slug method returns the correct value.
+ *
+ * @since 3.17.0
+ *
+ * @covers \Parsely\REST_API\Base_Endpoint::get_endpoint_slug
+ * @uses \Parsely\REST_API\Base_Endpoint::__construct
+ * @uses \Parsely\REST_API\Base_Endpoint::get_endpoint_slug
+ * @uses \Parsely\REST_API\Base_Endpoint::init
+ * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
+ */
+ public function test_get_endpoint_slug(): void {
+ // Create a mocked endpoint.
+ $endpoint = new class( $this->test_controller ) extends Base_Endpoint {
+ /**
+ * Get the endpoint name.
+ *
+ * @since 3.17.0
+ *
+ * @return string The endpoint name.
+ */
+ public static function get_endpoint_name(): string {
+ return 'test-endpoint';
+ }
+
+ /**
+ * Register the routes for the endpoints.
+ *
+ * @since 3.17.0
+ */
+ public function register_routes(): void {}
+ };
+
+ $this->test_controller->testable_register_endpoint( $endpoint ); // @phpstan-ignore-line
+
+ self::assertEquals( 'test/test-endpoint', $endpoint->get_endpoint_slug() );
+ }
+}
diff --git a/tests/Integration/RestAPI/BaseEndpointTest.php b/tests/Integration/RestAPI/BaseEndpointTest.php
new file mode 100644
index 000000000..29544e51a
--- /dev/null
+++ b/tests/Integration/RestAPI/BaseEndpointTest.php
@@ -0,0 +1,447 @@
+ $data The data for the test.
+ * @param string $data_name The name of the data.
+ */
+ public function __construct( $name = null, array $data = array(), $data_name = '' ) {
+ // Create Parsely class, if not already created (by an inherited class).
+ if ( null === $this->parsely ) {
+ $this->parsely = new Parsely();
+ }
+
+ // Create API controller, if not already created (by an inherited class).
+ if ( null === $this->api_controller ) {
+ $this->api_controller = new REST_API_Controller( $this->parsely );
+ }
+
+ parent::__construct( $name, $data, $data_name );
+ }
+
+ /**
+ * Setup method called before each test.
+ *
+ * @since 3.17.0
+ */
+ public function set_up(): void {
+ parent::set_up();
+ TestCase::set_options();
+ $this->set_current_user_to_admin();
+
+ $this->wp_rest_server_global_backup = $GLOBALS['wp_rest_server'] ?? null;
+
+ // Create a concrete class for testing purposes.
+ $this->test_endpoint = new class($this->api_controller) extends Base_Endpoint {
+
+ /**
+ * Gets the endpoint name.
+ *
+ * @since 3.17.0
+ *
+ * @return string
+ */
+ public static function get_endpoint_name(): string {
+ return 'test';
+ }
+
+ /**
+ * Registers the test route.
+ *
+ * @since 3.17.0
+ */
+ public function register_routes(): void {
+ $this->register_rest_route(
+ '/test-route',
+ array( 'GET' ),
+ array( $this, 'get_test_data' )
+ );
+ }
+
+ /**
+ * Gets test data.
+ *
+ * @since 3.17.0
+ *
+ * @return array
+ */
+ public function get_test_data(): array {
+ return array( 'data' => 'test' );
+ }
+ };
+
+ $this->initialize_rest_endpoint();
+ }
+
+ /**
+ * Tears down the test environment.
+ *
+ * @since 3.17.0
+ */
+ public function tear_down(): void {
+ remove_action( 'plugins_loaded', $this->rest_api_init_proxy );
+ // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound
+ $GLOBALS['wp_rest_server'] = $this->wp_rest_server_global_backup;
+
+ parent::tear_down();
+ }
+
+ /**
+ * Returns the test endpoint instance.
+ *
+ * @since 3.17.0
+ *
+ * @return Base_Endpoint
+ */
+ public function get_endpoint(): Base_Endpoint {
+ return $this->test_endpoint;
+ }
+
+ /**
+ * Tests that the route is correctly registered in WordPress.
+ *
+ * @since 3.17.0
+ *
+ * @covers \Parsely\REST_API\Base_Endpoint::init
+ * @covers \Parsely\REST_API\Base_Endpoint::register_routes
+ * @covers \Parsely\REST_API\Base_Endpoint::get_full_endpoint
+ * @uses \Parsely\REST_API\Base_API_Controller::__construct
+ * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace
+ * @uses \Parsely\REST_API\Base_API_Controller::prefix_route
+ * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
+ * @uses \Parsely\Parsely::__construct
+ * @uses \Parsely\Parsely::allow_parsely_remote_requests
+ * @uses \Parsely\Parsely::api_secret_is_set
+ * @uses \Parsely\Parsely::are_credentials_managed
+ * @uses \Parsely\Parsely::get_managed_credentials
+ * @uses \Parsely\Parsely::get_options
+ * @uses \Parsely\Parsely::set_default_content_helper_settings_values
+ * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts
+ * @uses \Parsely\Parsely::set_managed_options
+ * @uses \Parsely\Parsely::site_id_is_set
+ * @uses \Parsely\Permissions::build_pch_permissions_settings_array
+ * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap
+ */
+ public function test_route_is_registered(): void {
+ $routes = rest_get_server()->get_routes();
+
+ // Check that the namespace route is registered.
+ $expected_namespace = '/' . $this->api_controller->get_full_namespace();
+ self::assertArrayHasKey( $expected_namespace, $routes );
+
+ // Check that the test route is registered.
+ $expected_route = $this->get_endpoint()->get_full_endpoint( 'test-route' );
+ self::assertArrayHasKey( $expected_route, $routes );
+
+ // Check that the route is associated with the correct method.
+ $route_data = $routes[ $expected_route ];
+ self::assertArrayHasKey( 'GET', $route_data[0]['methods'] );
+ }
+
+ /**
+ * Tests that the route is correctly registered in WordPress, depending on the filter.
+ *
+ * @since 3.17.0
+ *
+ * @covers \Parsely\REST_API\Base_Endpoint::init
+ * @covers \Parsely\REST_API\Base_Endpoint::register_routes
+ * @covers \Parsely\REST_API\Base_Endpoint::get_full_endpoint
+ * @covers \Parsely\REST_API\Base_Endpoint::get_registered_routes
+ * @uses \Parsely\Parsely::__construct
+ * @uses \Parsely\Parsely::allow_parsely_remote_requests
+ * @uses \Parsely\Parsely::api_secret_is_set
+ * @uses \Parsely\Parsely::are_credentials_managed
+ * @uses \Parsely\Parsely::get_managed_credentials
+ * @uses \Parsely\Parsely::get_options
+ * @uses \Parsely\Parsely::set_default_content_helper_settings_values
+ * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts
+ * @uses \Parsely\Parsely::set_managed_options
+ * @uses \Parsely\Parsely::site_id_is_set
+ * @uses \Parsely\Permissions::build_pch_permissions_settings_array
+ * @uses \Parsely\Permissions::current_user_can_use_pch_feature
+ * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap
+ * @uses \Parsely\REST_API\Base_API_Controller::__construct
+ * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace
+ * @uses \Parsely\REST_API\Base_API_Controller::prefix_route
+ * @uses \Parsely\REST_API\Base_API_Controller::get_parsely
+ * @uses \Parsely\REST_API\REST_API_Controller::get_namespace
+ * @uses \Parsely\REST_API\REST_API_Controller::get_version
+ * @uses \Parsely\REST_API\Base_API_Controller::get_route_prefix
+ * @uses \Parsely\REST_API\Base_Endpoint::__construct
+ * @uses \Parsely\REST_API\Base_Endpoint::get_endpoint_name
+ * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user
+ * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route
+ * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret
+ * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters
+ * @uses \Parsely\REST_API\Base_Endpoint::get_default_access_capability
+ * @uses \Parsely\REST_API\Content_Helper\Content_Helper_Controller::get_route_prefix
+ * @uses \Parsely\REST_API\Stats\Stats_Controller::get_route_prefix
+ * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
+ */
+ public function test_endpoint_is_registered_based_on_filter(): void {
+ $filter_name = 'wp_parsely_api_' .
+ \Parsely\Utils\Utils::convert_endpoint_to_filter_key( $this->get_endpoint()::get_endpoint_name() ) .
+ '_endpoint_enabled';
+
+ // Test when the filter allows the endpoint to be enabled.
+ add_filter( $filter_name, '__return_true' );
+ $this->get_endpoint()->init();
+ $routes = rest_get_server()->get_routes();
+ $registered_routes = $this->get_endpoint()->get_registered_routes();
+
+ // Assert that the routes are registered when the filter returns true.
+ foreach ( $registered_routes as $route ) {
+ self::assertArrayHasKey( $this->get_endpoint()->get_full_endpoint( $route ), $routes );
+ }
+
+ // Reset the environment.
+ $this->tear_down();
+
+ // Now test when the filter disables the endpoint.
+ remove_all_filters( $filter_name );
+ add_filter( $filter_name, '__return_false' );
+ $this->get_endpoint()->init();
+ $routes = rest_get_server()->get_routes();
+ $registered_routes = $this->get_endpoint()->get_registered_routes();
+
+ // Assert that the route is NOT registered when the filter returns false.
+ foreach ( $registered_routes as $route ) {
+ self::assertArrayNotHasKey( $this->get_endpoint()->get_full_endpoint( $route ), $routes );
+ }
+ }
+
+ /**
+ * Tests is_available_to_current_user returns WP_Error if API key or secret is not set.
+ *
+ * @since 3.17.0
+ *
+ * @covers \Parsely\REST_API\Base_Endpoint::is_available_to_current_user
+ * @covers \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret
+ * @uses \Parsely\REST_API\Base_API_Controller::__construct
+ * @uses \Parsely\REST_API\Base_API_Controller::get_parsely
+ * @uses \Parsely\Parsely::__construct
+ * @uses \Parsely\Parsely::allow_parsely_remote_requests
+ * @uses \Parsely\Parsely::are_credentials_managed
+ * @uses \Parsely\Parsely::get_managed_credentials
+ * @uses \Parsely\Parsely::get_options
+ * @uses \Parsely\Parsely::set_default_content_helper_settings_values
+ * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts
+ * @uses \Parsely\Parsely::set_managed_options
+ * @uses \Parsely\Parsely::site_id_is_set
+ * @uses \Parsely\Permissions::build_pch_permissions_settings_array
+ * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap
+ * @uses \Parsely\Permissions::current_user_can_use_pch_feature
+ * @uses \Parsely\REST_API\Base_Endpoint::__construct
+ * @uses \Parsely\REST_API\Base_Endpoint::init
+ * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
+ */
+ public function test_is_available_to_current_user_returns_error_site_id_not_set(): void {
+ TestCase::set_options(
+ array(
+ 'apikey' => '',
+ 'api_secret' => '',
+ )
+ );
+
+ $result = $this->get_endpoint()->is_available_to_current_user();
+
+ self::assertInstanceOf( WP_Error::class, $result );
+ self::assertEquals( 'parsely_site_id_not_set', $result->get_error_code() );
+ }
+
+ /**
+ * Tests is_available_to_current_user returns WP_Error if API key or secret is not set.
+ *
+ * @since 3.17.0
+ *
+ * @covers \Parsely\REST_API\Base_Endpoint::is_available_to_current_user
+ * @covers \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret
+ * @uses \Parsely\REST_API\Base_API_Controller::__construct
+ * @uses \Parsely\REST_API\Base_API_Controller::get_parsely
+ * @uses \Parsely\Parsely::__construct
+ * @uses \Parsely\Parsely::allow_parsely_remote_requests
+ * @uses \Parsely\Parsely::api_secret_is_set
+ * @uses \Parsely\Parsely::are_credentials_managed
+ * @uses \Parsely\Parsely::get_managed_credentials
+ * @uses \Parsely\Parsely::get_options
+ * @uses \Parsely\Parsely::set_default_content_helper_settings_values
+ * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts
+ * @uses \Parsely\Parsely::set_managed_options
+ * @uses \Parsely\Parsely::site_id_is_set
+ * @uses \Parsely\Permissions::build_pch_permissions_settings_array
+ * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap
+ * @uses \Parsely\Permissions::current_user_can_use_pch_feature
+ * @uses \Parsely\REST_API\Base_Endpoint::__construct
+ * @uses \Parsely\REST_API\Base_Endpoint::init
+ * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
+ */
+ public function test_is_available_to_current_user_returns_error_api_secret_not_set(): void {
+ TestCase::set_options(
+ array(
+ 'apikey' => 'test-apikey',
+ 'api_secret' => '',
+ )
+ );
+
+ $result = $this->get_endpoint()->is_available_to_current_user();
+
+ self::assertInstanceOf( WP_Error::class, $result );
+ self::assertEquals( 'parsely_api_secret_not_set', $result->get_error_code() );
+ }
+
+ /**
+ * Tests apply_capability_filters method.
+ *
+ * @covers \Parsely\REST_API\Base_Endpoint::apply_capability_filters
+ * @uses \Parsely\REST_API\Base_API_Controller::__construct
+ * @uses \Parsely\REST_API\Base_API_Controller::get_parsely
+ * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
+ * @uses \Parsely\Parsely::__construct
+ * @uses \Parsely\Parsely::allow_parsely_remote_requests
+ * @uses \Parsely\Parsely::are_credentials_managed
+ * @uses \Parsely\Parsely::set_managed_options
+ * @uses \Parsely\REST_API\Base_Endpoint::__construct
+ * @uses \Parsely\REST_API\Base_Endpoint::init
+ */
+ public function test_apply_capability_filters(): void {
+ add_filter(
+ 'wp_parsely_user_capability_for_all_private_apis',
+ function () {
+ return 'edit_posts';
+ }
+ );
+
+ $result = $this->get_endpoint()->apply_capability_filters( 'publish_posts' );
+ self::assertEquals( 'edit_posts', $result );
+ }
+
+ /**
+ * Tests validate_apikey_and_secret returns true when API key and secret are set.
+ *
+ * @covers \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret
+ * @uses \Parsely\REST_API\Base_API_Controller::__construct
+ * @uses \Parsely\REST_API\Base_API_Controller::get_parsely
+ * @uses \Parsely\Parsely::__construct
+ * @uses \Parsely\Parsely::allow_parsely_remote_requests
+ * @uses \Parsely\Parsely::api_secret_is_set
+ * @uses \Parsely\Parsely::are_credentials_managed
+ * @uses \Parsely\Parsely::get_managed_credentials
+ * @uses \Parsely\Parsely::get_options
+ * @uses \Parsely\Parsely::set_default_content_helper_settings_values
+ * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts
+ * @uses \Parsely\Parsely::set_managed_options
+ * @uses \Parsely\Parsely::site_id_is_set
+ * @uses \Parsely\Permissions::build_pch_permissions_settings_array
+ * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap
+ * @uses \Parsely\REST_API\Base_Endpoint::__construct
+ * @uses \Parsely\REST_API\Base_Endpoint::init
+ * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
+ */
+ public function test_validate_site_id_and_secret_returns_true(): void {
+ TestCase::set_options(
+ array(
+ 'apikey' => 'test-apikey',
+ 'api_secret' => 'test-secret',
+ )
+ );
+
+ $result = $this->get_endpoint()->validate_site_id_and_secret();
+
+ self::assertTrue( $result );
+ }
+
+ /**
+ * Initializes the REST endpoint.
+ *
+ * @since 3.17.0
+ */
+ protected function initialize_rest_endpoint(): void {
+ // Initialize the endpoint when the plugins are loaded.
+ $this->rest_api_init_proxy = function () {
+ $this->get_endpoint()->init();
+ };
+ add_action( 'plugins_loaded', $this->rest_api_init_proxy );
+
+ // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
+ do_action( 'plugins_loaded' );
+ }
+}
diff --git a/tests/Integration/RestAPI/ContentHelper/ContentHelperControllerTest.php b/tests/Integration/RestAPI/ContentHelper/ContentHelperControllerTest.php
new file mode 100644
index 000000000..29f4300f4
--- /dev/null
+++ b/tests/Integration/RestAPI/ContentHelper/ContentHelperControllerTest.php
@@ -0,0 +1,106 @@
+content_helper_controller = new Content_Helper_Controller( $parsely );
+ }
+
+ /**
+ * Tests the constructor sets up the correct namespace and version.
+ *
+ * @since 3.17.0
+ *
+ * @covers \Parsely\REST_API\Content_Helper\Content_Helper_Controller::__construct
+ * @uses \Parsely\REST_API\Content_Helper\Content_Helper_Controller::get_full_namespace
+ * @uses \Parsely\REST_API\REST_API_Controller::get_namespace
+ * @uses \Parsely\REST_API\REST_API_Controller::get_version
+ */
+ public function test_constructor_sets_up_namespace_and_version(): void {
+ self::assertEquals( 'wp-parsely/v2', $this->content_helper_controller->get_full_namespace() );
+ }
+
+ /**
+ * Tests that the route prefix is set correctly.
+ *
+ * @since 3.17.0
+ *
+ * @covers \Parsely\REST_API\Content_Helper\Content_Helper_Controller::ROUTE_PREFIX
+ */
+ public function test_route_prefix(): void {
+ self::assertEquals( 'content-helper', $this->content_helper_controller::get_route_prefix() );
+ }
+
+ /**
+ * Tests that the init method registers the correct endpoints.
+ *
+ * @since 3.17.0
+ *
+ * @covers \Parsely\REST_API\Content_Helper\Content_Helper_Controller::init
+ * @uses \Parsely\REST_API\Base_API_Controller::__construct
+ * @uses \Parsely\REST_API\Base_API_Controller::get_endpoints
+ * @uses \Parsely\REST_API\Base_API_Controller::get_parsely
+ * @uses \Parsely\REST_API\Base_API_Controller::register_endpoint
+ * @uses \Parsely\REST_API\Base_API_Controller::register_endpoints
+ * @uses \Parsely\REST_API\Base_Endpoint::__construct
+ * @uses \Parsely\REST_API\Base_Endpoint::init
+ * @uses \Parsely\REST_API\Content_Helper\Endpoint_Excerpt_Generator::__construct
+ * @uses \Parsely\REST_API\Content_Helper\Endpoint_Excerpt_Generator::get_endpoint_name
+ * @uses \Parsely\REST_API\Content_Helper\Endpoint_Smart_Linking::__construct
+ * @uses \Parsely\REST_API\Content_Helper\Endpoint_Smart_Linking::get_endpoint_name
+ * @uses \Parsely\REST_API\Content_Helper\Endpoint_Title_Suggestions::__construct
+ * @uses \Parsely\REST_API\Content_Helper\Endpoint_Title_Suggestions::get_endpoint_name
+ * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
+ */
+ public function test_init_registers_endpoints(): void {
+ $this->content_helper_controller->init();
+ $endpoints = $this->content_helper_controller->get_endpoints();
+
+ self::assertCount( 3, $endpoints );
+
+ self::assertInstanceOf( Endpoint_Smart_Linking::class, $endpoints['content-helper/smart-linking'] );
+ self::assertInstanceOf( Endpoint_Excerpt_Generator::class, $endpoints['content-helper/excerpt-generator'] );
+ self::assertInstanceOf( Endpoint_Title_Suggestions::class, $endpoints['content-helper/title-suggestions'] );
+ }
+}
diff --git a/tests/Integration/RestAPI/ContentHelper/ContentHelperFeatureTestTrait.php b/tests/Integration/RestAPI/ContentHelper/ContentHelperFeatureTestTrait.php
new file mode 100644
index 000000000..1493ee0ca
--- /dev/null
+++ b/tests/Integration/RestAPI/ContentHelper/ContentHelperFeatureTestTrait.php
@@ -0,0 +1,289 @@
+enable_feature();
+ $this->set_current_user_to_admin();
+
+ // Assert that the endpoint is available to the current user.
+ self::assertTrue( $this->get_endpoint()->is_available_to_current_user( new WP_REST_Request() ) );
+ }
+
+ /**
+ * Tests that the endpoint is not available to the current user.
+ *
+ * @since 3.17.0
+ *
+ * @covers \Parsely\REST_API\Content_Helper\Content_Helper_Feature::is_available_to_current_user
+ * @uses \Parsely\Parsely::__construct
+ * @uses \Parsely\Parsely::allow_parsely_remote_requests
+ * @uses \Parsely\Parsely::api_secret_is_set
+ * @uses \Parsely\Parsely::are_credentials_managed
+ * @uses \Parsely\Parsely::get_managed_credentials
+ * @uses \Parsely\Parsely::get_options
+ * @uses \Parsely\Parsely::set_default_content_helper_settings_values
+ * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts
+ * @uses \Parsely\Parsely::set_managed_options
+ * @uses \Parsely\Parsely::site_id_is_set
+ * @uses \Parsely\Permissions::build_pch_permissions_settings_array
+ * @uses \Parsely\Permissions::current_user_can_use_pch_feature
+ * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap
+ * @uses \Parsely\REST_API\Base_API_Controller::__construct
+ * @uses \Parsely\REST_API\Base_Endpoint::__construct
+ * @uses \Parsely\REST_API\Base_Endpoint::init
+ * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters
+ * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user
+ * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret
+ * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
+ */
+ public function test_is_available_to_current_user_returns_error_if_feature_disabled(): void {
+ $this->disable_feature();
+ $this->set_current_user_to_admin();
+
+ // Assert that the endpoint is not available to the current user.
+ self::assertInstanceOf( WP_Error::class, $this->get_endpoint()->is_available_to_current_user( new WP_REST_Request() ) );
+ }
+
+ /**
+ * Tests that the endpoint is available to the current user, since the user has the required role.
+ *
+ * @since 3.17.0
+ *
+ * @covers \Parsely\REST_API\Content_Helper\Content_Helper_Feature::is_available_to_current_user
+ * @uses \Parsely\Parsely::__construct
+ * @uses \Parsely\Parsely::allow_parsely_remote_requests
+ * @uses \Parsely\Parsely::api_secret_is_set
+ * @uses \Parsely\Parsely::are_credentials_managed
+ * @uses \Parsely\Parsely::get_managed_credentials
+ * @uses \Parsely\Parsely::get_options
+ * @uses \Parsely\Parsely::set_default_content_helper_settings_values
+ * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts
+ * @uses \Parsely\Parsely::set_managed_options
+ * @uses \Parsely\Parsely::site_id_is_set
+ * @uses \Parsely\Permissions::build_pch_permissions_settings_array
+ * @uses \Parsely\Permissions::current_user_can_use_pch_feature
+ * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap
+ * @uses \Parsely\REST_API\Base_API_Controller::__construct
+ * @uses \Parsely\REST_API\Base_Endpoint::__construct
+ * @uses \Parsely\REST_API\Base_Endpoint::init
+ * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters
+ * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user
+ * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret
+ * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
+ */
+ public function test_is_available_to_current_user_returns_true_if_has_permissions(): void {
+ $this->set_feature_options(
+ array(
+ 'enabled' => true,
+ 'allowed_user_roles' => array( 'administrator' ),
+ )
+ );
+
+ // Assert that the endpoint is available to the current user.
+ $this->set_current_user_to_admin();
+ self::assertTrue( $this->get_endpoint()->is_available_to_current_user( new WP_REST_Request() ) );
+
+ // Assert that the endpoint is not available to the current user.
+ $this->set_current_user_to_contributor();
+ self::assertInstanceOf( WP_Error::class, $this->get_endpoint()->is_available_to_current_user( new WP_REST_Request() ) );
+ }
+
+ /**
+ * Tests that the endpoint is not available to the current user, since the user does not have the
+ * required role.
+ *
+ * @since 3.17.0
+ *
+ * @covers \Parsely\REST_API\Content_Helper\Content_Helper_Feature::is_available_to_current_user
+ * @uses \Parsely\Parsely::__construct
+ * @uses \Parsely\Parsely::allow_parsely_remote_requests
+ * @uses \Parsely\Parsely::api_secret_is_set
+ * @uses \Parsely\Parsely::are_credentials_managed
+ * @uses \Parsely\Parsely::get_managed_credentials
+ * @uses \Parsely\Parsely::get_options
+ * @uses \Parsely\Parsely::set_default_content_helper_settings_values
+ * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts
+ * @uses \Parsely\Parsely::set_managed_options
+ * @uses \Parsely\Parsely::site_id_is_set
+ * @uses \Parsely\Permissions::build_pch_permissions_settings_array
+ * @uses \Parsely\Permissions::current_user_can_use_pch_feature
+ * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap
+ * @uses \Parsely\REST_API\Base_API_Controller::__construct
+ * @uses \Parsely\REST_API\Base_Endpoint::__construct
+ * @uses \Parsely\REST_API\Base_Endpoint::init
+ * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters
+ * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user
+ * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret
+ * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
+ */
+ public function test_is_available_to_current_user_returns_error_if_no_permissions(): void {
+ $this->set_current_user_to_contributor();
+
+ $this->set_feature_options(
+ array(
+ 'enabled' => true,
+ 'allowed_user_roles' => array( 'administrator' ),
+ )
+ );
+
+ // Assert that the endpoint is not available to the current user.
+ self::assertInstanceOf( WP_Error::class, $this->get_endpoint()->is_available_to_current_user( new WP_REST_Request() ) );
+ }
+
+
+ /**
+ * Tests that the endpoint is not available to the current user, since the user is not logged in.
+ *
+ * @since 3.17.0
+ *
+ * @covers \Parsely\REST_API\Content_Helper\Content_Helper_Feature::is_available_to_current_user
+ * @uses \Parsely\Parsely::api_secret_is_set
+ * @uses \Parsely\Parsely::get_managed_credentials
+ * @uses \Parsely\Parsely::get_options
+ * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts
+ * @uses \Parsely\Parsely::site_id_is_set
+ * @uses \Parsely\Permissions::current_user_can_use_pch_feature
+ * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap
+ * @uses \Parsely\REST_API\Base_API_Controller::__construct
+ * @uses \Parsely\REST_API\Base_API_Controller::get_parsely
+ * @uses \Parsely\REST_API\Base_Endpoint::__construct
+ * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters
+ * @uses \Parsely\REST_API\Base_Endpoint::get_default_access_capability
+ * @uses \Parsely\REST_API\Base_Endpoint::init
+ * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user
+ * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret
+ * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
+ */
+ public function test_is_available_to_current_user_returns_error_if_no_user(): void {
+ $this->enable_feature();
+ // Set the current user to a non-logged in user.
+ wp_set_current_user( 0 );
+
+ // Assert that the endpoint is not available to the current user.
+ self::assertInstanceOf( WP_Error::class, $this->get_endpoint()->is_available_to_current_user( new WP_REST_Request() ) );
+ }
+
+ /**
+ * You need to implement this method in your test class
+ * to return the endpoint instance being tested.
+ *
+ * @since 3.17.0
+ *
+ * @return Base_Endpoint
+ */
+ abstract protected function get_endpoint(): Base_Endpoint;
+
+ /**
+ * Sets the specific feature options.
+ *
+ * @since 3.17.0
+ *
+ * @param array $options The options to set.
+ */
+ private function set_feature_options( array $options ): void {
+ $feature_name = $this->get_endpoint()->get_pch_feature_name();
+
+ TestCase::set_options(
+ array(
+ 'apikey' => 'test',
+ 'api_secret' => 'test',
+ 'content_helper' => array(
+ 'ai_features_enabled' => true,
+ $feature_name => $options,
+ ),
+ )
+ );
+ }
+
+ /**
+ * Disables the specific feature.
+ *
+ * @since 3.17.0
+ */
+ private function disable_feature(): void {
+ $this->set_feature_options(
+ array(
+ 'enabled' => false,
+ 'allowed_user_roles' => array(),
+ )
+ );
+ }
+
+ /**
+ * Enables the specific feature.
+ *
+ * @since 3.17.0
+ */
+ private function enable_feature(): void {
+ $valid_roles = array_keys( Permissions::get_user_roles_with_edit_posts_cap() );
+
+ $this->set_feature_options(
+ array(
+ 'enabled' => true,
+ 'allowed_user_roles' => $valid_roles,
+ )
+ );
+ }
+
+ /**
+ * Sets the current user to an administrator.
+ *
+ * @since 3.17.0
+ */
+ abstract protected function set_current_user_to_admin(): void;
+
+ /**
+ * Sets the current user to a contributor.
+ *
+ * @since 3.17.0
+ */
+ abstract protected function set_current_user_to_contributor(): void;
+}
diff --git a/tests/Integration/RestAPI/ContentHelper/EndpointExcerptGeneratorTest.php b/tests/Integration/RestAPI/ContentHelper/EndpointExcerptGeneratorTest.php
new file mode 100644
index 000000000..17a12ebd3
--- /dev/null
+++ b/tests/Integration/RestAPI/ContentHelper/EndpointExcerptGeneratorTest.php
@@ -0,0 +1,203 @@
+api_controller = new Content_Helper_Controller( $this->parsely );
+ $this->endpoint = new Endpoint_Excerpt_Generator( $this->api_controller );
+
+ parent::set_up();
+ }
+
+ /**
+ * Gets the test endpoint instance.
+ *
+ * @since 3.17.0
+ *
+ * @return Endpoint_Excerpt_Generator
+ */
+ public function get_endpoint(): \Parsely\REST_API\Base_Endpoint {
+ return $this->endpoint;
+ }
+
+ /**
+ * Tests that the endpoint is correctly registered.
+ *
+ * @since 3.17.0
+ *
+ * @covers \Parsely\REST_API\Content_Helper\Endpoint_Excerpt_Generator::register_routes
+ * @uses \Parsely\Parsely::api_secret_is_set
+ * @uses \Parsely\Parsely::get_managed_credentials
+ * @uses \Parsely\Parsely::get_options
+ * @uses \Parsely\Parsely::set_default_content_helper_settings_values
+ * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts
+ * @uses \Parsely\Parsely::site_id_is_set
+ * @uses \Parsely\Permissions::build_pch_permissions_settings_array
+ * @uses \Parsely\Permissions::current_user_can_use_pch_feature
+ * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap
+ * @uses \Parsely\REST_API\Base_API_Controller::__construct
+ * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace
+ * @uses \Parsely\REST_API\Base_API_Controller::get_parsely
+ * @uses \Parsely\REST_API\Base_API_Controller::prefix_route
+ * @uses \Parsely\REST_API\Base_Endpoint::__construct
+ * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint
+ * @uses \Parsely\REST_API\Base_Endpoint::init
+ * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user
+ * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route
+ * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret
+ * @uses \Parsely\REST_API\Content_Helper\Content_Helper_Controller::get_route_prefix
+ * @uses \Parsely\REST_API\REST_API_Controller::get_namespace
+ * @uses \Parsely\REST_API\REST_API_Controller::get_version
+ * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
+ */
+ public function test_route_is_registered(): void {
+ // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
+ do_action( 'plugins_loaded' );
+ $routes = rest_get_server()->get_routes();
+
+ // Check that the excerpt-generator/generate route is registered.
+ $expected_route = $this->get_endpoint()->get_full_endpoint( 'generate' );
+ self::assertArrayHasKey( $expected_route, $routes );
+
+ // Check that the route is associated with the POST method.
+ $route_data = $routes[ $expected_route ];
+ self::assertArrayHasKey( 'POST', $route_data[0]['methods'] );
+ }
+
+ /**
+ * Tests that the generate_excerpt method returns a valid response.
+ *
+ * @since 3.17.0
+ *
+ * @covers \Parsely\REST_API\Content_Helper\Endpoint_Excerpt_Generator::generate_excerpt
+ * @uses \Parsely\Parsely::get_suggestions_api
+ * @uses \Parsely\REST_API\Base_API_Controller::__construct
+ * @uses \Parsely\REST_API\Base_API_Controller::get_parsely
+ * @uses \Parsely\REST_API\Base_Endpoint::__construct
+ * @uses \Parsely\REST_API\Base_Endpoint::init
+ * @uses \Parsely\Services\Base_API_Service::__construct
+ * @uses \Parsely\Services\Base_API_Service::register_endpoint
+ * @uses \Parsely\Services\Base_Service_Endpoint::__construct
+ * @uses \Parsely\Services\Suggestions_API\Endpoints\Endpoint_Suggest_Brief::get_endpoint
+ * @uses \Parsely\Services\Suggestions_API\Endpoints\Endpoint_Suggest_Headline::get_endpoint
+ * @uses \Parsely\Services\Suggestions_API\Endpoints\Endpoint_Suggest_Linked_Reference::get_endpoint
+ * @uses \Parsely\Services\Suggestions_API\Suggestions_API_Service::register_endpoints
+ * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
+ */
+ public function test_generate_excerpt_returns_valid_response(): void {
+ // Mock the Suggest_Brief_API to control the response.
+ $mock_suggestions_api = $this->createMock( Suggestions_API_Service::class );
+ $mock_suggestions_api->expects( self::once() )
+ ->method( 'get_brief_suggestions' )
+ ->willReturn( array( array( 'summary' => 'This is a test excerpt.' ) ) );
+
+ self::set_protected_property( $this->get_endpoint(), 'suggestions_api', $mock_suggestions_api );
+
+ // Create a mock request.
+ $request = new WP_REST_Request( 'POST', '/excerpt-generator/generate' );
+ $request->set_param( 'text', 'Test content' );
+ $request->set_param( 'title', 'Test title' );
+ $request->set_param( 'persona', 'journalist' );
+ $request->set_param( 'style', 'neutral' );
+
+ // Call the generate_excerpt method.
+ $response = $this->get_endpoint()->generate_excerpt( $request );
+
+ // Assert that the response is a WP_REST_Response and contains the correct data.
+ self::assertInstanceOf( WP_REST_Response::class, $response );
+
+ /**
+ * The response data.
+ *
+ * @var array> $data The response data.
+ */
+ $data = $response->get_data();
+ self::assertArrayHasKey( 'data', $data );
+ self::assertEquals( 'This is a test excerpt.', $data['data']['summary'] );
+ }
+
+ /**
+ * Tests that the generate_excerpt method returns an error if Suggest_Brief_API fails.
+ *
+ * @since 3.17.0
+ *
+ * @covers \Parsely\REST_API\Content_Helper\Endpoint_Excerpt_Generator::generate_excerpt
+ * @uses \Parsely\Parsely::get_suggestions_api
+ * @uses \Parsely\REST_API\Base_API_Controller::__construct
+ * @uses \Parsely\REST_API\Base_API_Controller::get_parsely
+ * @uses \Parsely\REST_API\Base_Endpoint::__construct
+ * @uses \Parsely\REST_API\Base_Endpoint::init
+ * @uses \Parsely\Services\Base_API_Service::__construct
+ * @uses \Parsely\Services\Base_API_Service::register_endpoint
+ * @uses \Parsely\Services\Base_Service_Endpoint::__construct
+ * @uses \Parsely\Services\Suggestions_API\Endpoints\Endpoint_Suggest_Brief::get_endpoint
+ * @uses \Parsely\Services\Suggestions_API\Endpoints\Endpoint_Suggest_Headline::get_endpoint
+ * @uses \Parsely\Services\Suggestions_API\Endpoints\Endpoint_Suggest_Linked_Reference::get_endpoint
+ * @uses \Parsely\Services\Suggestions_API\Suggestions_API_Service::register_endpoints
+ * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
+ */
+ public function test_generate_excerpt_returns_error_on_failure(): void {
+ // Mock the Suggest_Brief_API to simulate a failure.
+ $mock_suggestions_api = $this->createMock( Suggestions_API_Service::class );
+ $mock_suggestions_api->expects( self::once() )
+ ->method( 'get_brief_suggestions' )
+ ->willReturn( new WP_Error( 'api_error', 'API request failed' ) );
+
+ self::set_protected_property( $this->get_endpoint(), 'suggestions_api', $mock_suggestions_api );
+
+ // Create a mock request.
+ $request = new WP_REST_Request( 'POST', '/excerpt-generator/generate' );
+ $request->set_param( 'text', 'Test content' );
+ $request->set_param( 'title', 'Test title' );
+ $request->set_param( 'persona', 'journalist' );
+ $request->set_param( 'style', 'neutral' );
+
+ // Call the generate_excerpt method.
+ $response = $this->get_endpoint()->generate_excerpt( $request );
+
+ // Assert that the response is a WP_Error.
+ self::assertInstanceOf( WP_Error::class, $response );
+ self::assertEquals( 'api_error', $response->get_error_code() );
+ }
+}
diff --git a/tests/Integration/RestAPI/ContentHelper/EndpointSmartLinkingTest.php b/tests/Integration/RestAPI/ContentHelper/EndpointSmartLinkingTest.php
new file mode 100644
index 000000000..6e17109ee
--- /dev/null
+++ b/tests/Integration/RestAPI/ContentHelper/EndpointSmartLinkingTest.php
@@ -0,0 +1,429 @@
+api_controller = new Content_Helper_Controller( $this->parsely );
+ $this->endpoint = new Endpoint_Smart_Linking( $this->api_controller );
+
+ parent::set_up();
+
+ // Setup fake API key and secret.
+ TestCase::set_options(
+ array(
+ 'apikey' => 'test-apikey',
+ 'api_secret' => 'test-secret',
+ )
+ );
+ }
+
+ /**
+ * Gets the test endpoint instance.
+ *
+ * @since 3.17.0
+ *
+ * @return Endpoint_Smart_Linking
+ */
+ public function get_endpoint(): \Parsely\REST_API\Base_Endpoint {
+ return $this->endpoint;
+ }
+
+ /**
+ * Tests that the endpoint is correctly registered.
+ *
+ * @since 3.17.0
+ *
+ * @covers \Parsely\REST_API\Content_Helper\Endpoint_Smart_Linking::register_routes
+ * @uses \Parsely\Parsely::api_secret_is_set
+ * @uses \Parsely\Parsely::get_managed_credentials
+ * @uses \Parsely\Parsely::get_options
+ * @uses \Parsely\Parsely::set_default_content_helper_settings_values
+ * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts
+ * @uses \Parsely\Parsely::site_id_is_set
+ * @uses \Parsely\Permissions::build_pch_permissions_settings_array
+ * @uses \Parsely\Permissions::current_user_can_use_pch_feature
+ * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap
+ * @uses \Parsely\REST_API\Base_API_Controller::__construct
+ * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace
+ * @uses \Parsely\REST_API\Base_API_Controller::get_parsely
+ * @uses \Parsely\REST_API\Base_API_Controller::prefix_route
+ * @uses \Parsely\REST_API\Base_Endpoint::__construct
+ * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters
+ * @uses \Parsely\REST_API\Base_Endpoint::get_default_access_capability
+ * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint
+ * @uses \Parsely\REST_API\Base_Endpoint::init
+ * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user
+ * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route
+ * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret
+ * @uses \Parsely\REST_API\Content_Helper\Content_Helper_Controller::get_route_prefix
+ * @uses \Parsely\REST_API\REST_API_Controller::get_namespace
+ * @uses \Parsely\REST_API\REST_API_Controller::get_version
+ * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
+ */
+ public function test_route_is_registered(): void {
+ // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
+ do_action( 'plugins_loaded' );
+ $routes = rest_get_server()->get_routes();
+
+ // Check that the smart-linking/generate route is registered.
+ $expected_route = $this->get_endpoint()->get_full_endpoint( 'generate' );
+ self::assertArrayHasKey( $expected_route, $routes );
+
+ // Check that the route is associated with the POST method.
+ $route_data = $routes[ $expected_route ];
+ self::assertArrayHasKey( 'POST', $route_data[0]['methods'] );
+ }
+
+ /**
+ * Tests that the generate_smart_links method returns a valid response.
+ *
+ * @since 3.17.0
+ *
+ * @covers \Parsely\REST_API\Content_Helper\Endpoint_Smart_Linking::generate_smart_links
+ * @uses \Parsely\Models\Base_Model::__construct
+ * @uses \Parsely\Models\Smart_Link::__construct
+ * @uses \Parsely\Models\Smart_Link::generate_uid
+ * @uses \Parsely\Models\Smart_Link::get_post_id_by_url
+ * @uses \Parsely\Models\Smart_Link::set_href
+ * @uses \Parsely\Models\Smart_Link::to_array
+ * @uses \Parsely\REST_API\Base_API_Controller::__construct
+ * @uses \Parsely\REST_API\Base_API_Controller::get_parsely
+ * @uses \Parsely\REST_API\Base_Endpoint::__construct
+ * @uses \Parsely\REST_API\Base_Endpoint::init
+ * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
+ */
+ public function test_generate_smart_links_returns_valid_response(): void {
+ // Create a mocked Suggestions API that returns two smart links.
+ $mock_suggestions_api = $this->createMock( Suggestions_API_Service::class );
+ $mock_suggestions_api->expects( self::once() )
+ ->method( 'get_smart_links' )
+ ->willReturn(
+ array(
+ new Smart_Link( 'link1', 'http://example.com/1', 'Example 1', 0, 0 ),
+ new Smart_Link( 'link2', 'http://example.com/2', 'Example 2', 0, 1 ),
+ )
+ );
+
+ self::set_protected_property( $this->get_endpoint(), 'suggestions_api', $mock_suggestions_api );
+
+ // Create a mock request.
+ $request = new WP_REST_Request( 'POST', '/smart-linking/generate' );
+ $request->set_param( 'text', 'Test content' );
+ $request->set_param( 'max_links', 2 );
+ $request->set_param( 'url_exclusion_list', array( 'http://excluded.com' ) );
+
+ // Call the generate_smart_links method.
+ $response = $this->get_endpoint()->generate_smart_links( $request );
+
+ // Assert that the response is a WP_REST_Response and contains the correct data.
+ self::assertInstanceOf( WP_REST_Response::class, $response );
+
+ /**
+ * The response data.
+ *
+ * @var array> $data The response data.
+ */
+ $data = $response->get_data();
+ self::assertArrayHasKey( 'data', $data );
+ self::assertCount( 2, $data['data'] );
+ }
+
+ /**
+ * Tests that the generate_smart_links method returns an error if Suggest_Linked_Reference_API fails.
+ *
+ * @since 3.17.0
+ *
+ * @covers \Parsely\REST_API\Content_Helper\Endpoint_Smart_Linking::generate_smart_links
+ * @uses \Parsely\REST_API\Base_API_Controller::__construct
+ * @uses \Parsely\REST_API\Base_API_Controller::get_parsely
+ * @uses \Parsely\REST_API\Base_Endpoint::__construct
+ * @uses \Parsely\REST_API\Base_Endpoint::init
+ * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
+ */
+ public function test_generate_smart_links_returns_error_on_failure(): void {
+ // Mock the Suggestions API `get_smart_links` method to return a WP_Error.
+ $mock_suggestions_api = $this->createMock( Suggestions_API_Service::class );
+ $mock_suggestions_api->expects( self::once() )
+ ->method( 'get_smart_links' )
+ ->willReturn( new WP_Error( 'api_error', 'API request failed' ) );
+
+ self::set_protected_property( $this->get_endpoint(), 'suggestions_api', $mock_suggestions_api );
+
+ // Create a mock request.
+ $request = new WP_REST_Request( 'POST', '/smart-linking/generate' );
+ $request->set_param( 'text', 'Test content' );
+ $request->set_param( 'max_links', 2 );
+ $request->set_param( 'url_exclusion_list', array( 'http://excluded.com' ) );
+
+ // Call the generate_smart_links method.
+ $response = $this->get_endpoint()->generate_smart_links( $request );
+
+ // Assert that the response is a WP_Error.
+ self::assertInstanceOf( WP_Error::class, $response );
+ self::assertEquals( 'api_error', $response->get_error_code() );
+ }
+
+ /**
+ * Tests that the add_smart_link method returns a valid response when adding a new smart link.
+ *
+ * @since 3.17.0
+ *
+ * @covers \Parsely\REST_API\Content_Helper\Endpoint_Smart_Linking::add_smart_link
+ * @uses \Parsely\Models\Base_Model::__construct
+ * @uses \Parsely\Models\Base_Model::serialize
+ * @uses \Parsely\Models\Smart_Link::__construct
+ * @uses \Parsely\Models\Smart_Link::deserialize
+ * @uses \Parsely\Models\Smart_Link::exists
+ * @uses \Parsely\Models\Smart_Link::generate_uid
+ * @uses \Parsely\Models\Smart_Link::get_post_id_by_url
+ * @uses \Parsely\Models\Smart_Link::get_smart_link
+ * @uses \Parsely\Models\Smart_Link::get_smart_link_object_by_uid
+ * @uses \Parsely\Models\Smart_Link::load
+ * @uses \Parsely\Models\Smart_Link::save
+ * @uses \Parsely\Models\Smart_Link::set_href
+ * @uses \Parsely\Models\Smart_Link::set_source_post_id
+ * @uses \Parsely\Models\Smart_Link::set_uid
+ * @uses \Parsely\Models\Smart_Link::to_array
+ * @uses \Parsely\Parsely::api_secret_is_set
+ * @uses \Parsely\Parsely::get_managed_credentials
+ * @uses \Parsely\Parsely::get_options
+ * @uses \Parsely\Parsely::set_default_content_helper_settings_values
+ * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts
+ * @uses \Parsely\Parsely::site_id_is_set
+ * @uses \Parsely\Permissions::build_pch_permissions_settings_array
+ * @uses \Parsely\Permissions::current_user_can_use_pch_feature
+ * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap
+ * @uses \Parsely\REST_API\Base_API_Controller::__construct
+ * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace
+ * @uses \Parsely\REST_API\Base_API_Controller::get_parsely
+ * @uses \Parsely\REST_API\Base_API_Controller::prefix_route
+ * @uses \Parsely\REST_API\Base_Endpoint::__construct
+ * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters
+ * @uses \Parsely\REST_API\Base_Endpoint::get_default_access_capability
+ * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint
+ * @uses \Parsely\REST_API\Base_Endpoint::init
+ * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user
+ * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route
+ * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret
+ * @uses \Parsely\REST_API\Content_Helper\Content_Helper_Controller::get_route_prefix
+ * @uses \Parsely\REST_API\REST_API_Controller::get_namespace
+ * @uses \Parsely\REST_API\REST_API_Controller::get_version
+ * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
+ */
+ public function test_add_smart_link_returns_valid_response(): void {
+ // Create mocked post.
+ $post = WP_UnitTestCase_Base::factory()->post->create_and_get();
+ self::assertNotWPError( $post );
+ $post_id = $post->ID; // @phpstan-ignore-line
+
+ // Create a mock request.
+ $request = new WP_REST_Request( 'POST', $this->get_endpoint()->get_full_endpoint( $post_id . '/add' ) );
+
+ $smart_link_data = array(
+ 'uid' => md5( 'link1' ),
+ 'href' => 'http://example.com/1',
+ 'title' => 'Example 1',
+ 'text' => 'Example 1',
+ 'offset' => 0,
+ );
+ $request->set_param( 'link', $smart_link_data );
+
+ // Dispatch the request.
+ $response = rest_get_server()->dispatch( $request );
+
+ // Assert that the response is a WP_REST_Response and contains the correct data.
+ self::assertInstanceOf( WP_REST_Response::class, $response );
+
+ /**
+ * The response data.
+ *
+ * @var array> $data The response data.
+ */
+ $data = $response->get_data();
+
+ self::assertNotTrue( $response->is_error() );
+
+ self::assertArrayHasKey( 'data', $data );
+ self::assertIsObject( $data['data'] );
+
+ $smart_link_attributes = array(
+ 'smart_link_id',
+ 'uid',
+ 'href',
+ 'text',
+ 'offset',
+ 'applied',
+ 'source',
+ 'destination',
+ );
+
+ foreach ( $smart_link_attributes as $attribute ) {
+ self::assertObjectHasProperty( $attribute, $data['data'] );
+ }
+ }
+
+ /**
+ * Tests that the add_multiple_smart_links method returns a valid response when adding multiple smart links.
+ *
+ * @since 3.17.0
+ *
+ * @covers \Parsely\REST_API\Content_Helper\Endpoint_Smart_Linking::add_multiple_smart_links
+ * @uses \Parsely\Models\Base_Model::__construct
+ * @uses \Parsely\Models\Base_Model::serialize
+ * @uses \Parsely\Models\Smart_Link::__construct
+ * @uses \Parsely\Models\Smart_Link::deserialize
+ * @uses \Parsely\Models\Smart_Link::exists
+ * @uses \Parsely\Models\Smart_Link::generate_uid
+ * @uses \Parsely\Models\Smart_Link::get_post_id_by_url
+ * @uses \Parsely\Models\Smart_Link::get_smart_link
+ * @uses \Parsely\Models\Smart_Link::get_smart_link_object_by_uid
+ * @uses \Parsely\Models\Smart_Link::load
+ * @uses \Parsely\Models\Smart_Link::save
+ * @uses \Parsely\Models\Smart_Link::set_href
+ * @uses \Parsely\Models\Smart_Link::set_source_post_id
+ * @uses \Parsely\Models\Smart_Link::set_uid
+ * @uses \Parsely\Models\Smart_Link::to_array
+ * @uses \Parsely\Parsely::api_secret_is_set
+ * @uses \Parsely\Parsely::get_managed_credentials
+ * @uses \Parsely\Parsely::get_options
+ * @uses \Parsely\Parsely::set_default_content_helper_settings_values
+ * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts
+ * @uses \Parsely\Parsely::site_id_is_set
+ * @uses \Parsely\Permissions::build_pch_permissions_settings_array
+ * @uses \Parsely\Permissions::current_user_can_use_pch_feature
+ * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap
+ * @uses \Parsely\REST_API\Base_API_Controller::__construct
+ * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace
+ * @uses \Parsely\REST_API\Base_API_Controller::get_parsely
+ * @uses \Parsely\REST_API\Base_API_Controller::prefix_route
+ * @uses \Parsely\REST_API\Base_Endpoint::__construct
+ * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters
+ * @uses \Parsely\REST_API\Base_Endpoint::get_default_access_capability
+ * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint
+ * @uses \Parsely\REST_API\Base_Endpoint::init
+ * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user
+ * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route
+ * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret
+ * @uses \Parsely\REST_API\Content_Helper\Content_Helper_Controller::get_route_prefix
+ * @uses \Parsely\REST_API\REST_API_Controller::get_namespace
+ * @uses \Parsely\REST_API\REST_API_Controller::get_version
+ * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
+ */
+ public function test_add_multiple_smart_links_returns_valid_response(): void {
+ // Create mocked post.
+ $post = WP_UnitTestCase_Base::factory()->post->create_and_get();
+ self::assertNotWPError( $post );
+ $post_id = $post->ID; // @phpstan-ignore-line
+
+ // Create a mock request.
+ $request = new WP_REST_Request( 'POST', $this->get_endpoint()->get_full_endpoint( $post_id . '/add-multiple' ) );
+
+ $smart_links_data = array(
+ array(
+ 'uid' => md5( 'link1' ),
+ 'href' => 'http://example.com/1',
+ 'title' => 'Example 1',
+ 'text' => 'Example 1',
+ 'offset' => 0,
+ ),
+ array(
+ 'uid' => md5( 'link2' ),
+ 'href' => 'http://example.com/2',
+ 'title' => 'Example 2',
+ 'text' => 'Example 2',
+ 'offset' => 0,
+ ),
+ array(
+ 'uid' => md5( 'link3' ),
+ 'href' => 'http://example.com/3',
+ 'title' => 'Example 3',
+ 'text' => 'Example 3',
+ 'offset' => 0,
+ ),
+ );
+ $request->set_param( 'links', $smart_links_data );
+
+ // Dispatch the request.
+ $response = rest_get_server()->dispatch( $request );
+
+ // Assert that the response is a WP_REST_Response and contains the correct data.
+ self::assertInstanceOf( WP_REST_Response::class, $response );
+
+ /**
+ * The response data.
+ *
+ * @var array> $data The response data.
+ */
+ $data = $response->get_data();
+
+ self::assertNotTrue( $response->is_error() );
+
+ self::assertArrayHasKey( 'data', $data );
+ self::assertArrayHasKey( 'added', $data['data'] );
+ self::assertIsArray( $data['data']['added'] );
+ self::assertCount( 3, $data['data']['added'] );
+
+ $smart_link_attributes = array(
+ 'smart_link_id',
+ 'uid',
+ 'href',
+ 'text',
+ 'offset',
+ 'applied',
+ 'source',
+ 'destination',
+ );
+
+ foreach ( $data['data']['added'] as $smart_link ) {
+ foreach ( $smart_link_attributes as $attribute ) {
+ self::assertArrayHasKey( $attribute, $smart_link );
+ }
+ }
+ }
+}
diff --git a/tests/Integration/RestAPI/ContentHelper/EndpointTitleSuggestionsTest.php b/tests/Integration/RestAPI/ContentHelper/EndpointTitleSuggestionsTest.php
new file mode 100644
index 000000000..34969bd2f
--- /dev/null
+++ b/tests/Integration/RestAPI/ContentHelper/EndpointTitleSuggestionsTest.php
@@ -0,0 +1,180 @@
+api_controller = new Content_Helper_Controller( $this->parsely );
+ $this->endpoint = new Endpoint_Title_Suggestions( $this->api_controller );
+
+ parent::set_up();
+ }
+
+ /**
+ * Gets the test endpoint instance.
+ *
+ * @since 3.17.0
+ *
+ * @return Endpoint_Title_Suggestions
+ */
+ public function get_endpoint(): \Parsely\REST_API\Base_Endpoint {
+ return $this->endpoint;
+ }
+
+ /**
+ * Tests that the endpoint is correctly registered.
+ *
+ * @since 3.17.0
+ *
+ * @covers \Parsely\REST_API\Content_Helper\Endpoint_Title_Suggestions::register_routes
+ * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint
+ */
+ public function test_route_is_registered(): void {
+ $routes = rest_get_server()->get_routes();
+
+ // Check that the title-suggestions/generate route is registered.
+ $expected_route = $this->get_endpoint()->get_full_endpoint( 'generate' );
+ self::assertArrayHasKey( $expected_route, $routes );
+
+ // Check that the route is associated with the POST method.
+ $route_data = $routes[ $expected_route ];
+ self::assertArrayHasKey( 'POST', $route_data[0]['methods'] );
+ }
+
+ /**
+ * Tests that the generate_titles method returns a valid response.
+ *
+ * @since 3.17.0
+ *
+ * @covers \Parsely\REST_API\Content_Helper\Endpoint_Title_Suggestions::generate_titles
+ * @uses \Parsely\Parsely::get_suggestions_api
+ * @uses \Parsely\REST_API\Base_API_Controller::__construct
+ * @uses \Parsely\REST_API\Base_API_Controller::get_parsely
+ * @uses \Parsely\REST_API\Base_Endpoint::__construct
+ * @uses \Parsely\REST_API\Base_Endpoint::init
+ * @uses \Parsely\Services\Base_API_Service::__construct
+ * @uses \Parsely\Services\Base_API_Service::register_endpoint
+ * @uses \Parsely\Services\Base_Service_Endpoint::__construct
+ * @uses \Parsely\Services\Suggestions_API\Endpoints\Endpoint_Suggest_Brief::get_endpoint
+ * @uses \Parsely\Services\Suggestions_API\Endpoints\Endpoint_Suggest_Headline::get_endpoint
+ * @uses \Parsely\Services\Suggestions_API\Endpoints\Endpoint_Suggest_Linked_Reference::get_endpoint
+ * @uses \Parsely\Services\Suggestions_API\Suggestions_API_Service::register_endpoints
+ * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
+ */
+ public function test_generate_titles_returns_valid_response(): void {
+ // Mock the Suggestions API `get_title_suggestions` method to return a list of titles.
+ $mock_suggestions_api = $this->createMock( Suggestions_API_Service::class );
+ $mock_suggestions_api->expects( self::once() )
+ ->method( 'get_title_suggestions' )
+ ->willReturn( array( 'title1', 'title2', 'title3' ) );
+
+ self::set_protected_property( $this->get_endpoint(), 'suggestions_api', $mock_suggestions_api );
+
+ // Create a mock request.
+ $request = new WP_REST_Request( 'POST', '/title-suggestions/generate' );
+ $request->set_param( 'text', 'Test content' );
+ $request->set_param( 'limit', 3 );
+ $request->set_param( 'style', 'neutral' );
+ $request->set_param( 'persona', 'journalist' );
+
+ // Call the generate_titles method.
+ $response = $this->get_endpoint()->generate_titles( $request );
+
+ // Assert that the response is a WP_REST_Response and contains the correct data.
+ self::assertInstanceOf( WP_REST_Response::class, $response );
+
+ /**
+ * The response data.
+ *
+ * @var array $data The response data.
+ */
+ $data = $response->get_data();
+ self::assertArrayHasKey( 'data', $data );
+ self::assertEquals( array( 'title1', 'title2', 'title3' ), $data['data'] );
+ }
+
+ /**
+ * Tests that the generate_titles method returns an error if Suggest_Headline_API fails.
+ *
+ * @since 3.17.0
+ *
+ * @covers \Parsely\REST_API\Content_Helper\Endpoint_Title_Suggestions::generate_titles
+ * @uses \Parsely\Parsely::get_suggestions_api
+ * @uses \Parsely\REST_API\Base_API_Controller::__construct
+ * @uses \Parsely\REST_API\Base_API_Controller::get_parsely
+ * @uses \Parsely\REST_API\Base_Endpoint::__construct
+ * @uses \Parsely\REST_API\Base_Endpoint::init
+ * @uses \Parsely\Services\Base_API_Service::__construct
+ * @uses \Parsely\Services\Base_API_Service::register_endpoint
+ * @uses \Parsely\Services\Base_Service_Endpoint::__construct
+ * @uses \Parsely\Services\Suggestions_API\Endpoints\Endpoint_Suggest_Brief::get_endpoint
+ * @uses \Parsely\Services\Suggestions_API\Endpoints\Endpoint_Suggest_Headline::get_endpoint
+ * @uses \Parsely\Services\Suggestions_API\Endpoints\Endpoint_Suggest_Linked_Reference::get_endpoint
+ * @uses \Parsely\Services\Suggestions_API\Suggestions_API_Service::register_endpoints
+ * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key
+ */
+ public function test_generate_titles_returns_error_on_failure(): void {
+ // Mock the Suggestions API `get_title_suggestions` method to return an error.
+ $mock_suggestions_api = $this->createMock( Suggestions_API_Service::class );
+ $mock_suggestions_api->expects( self::once() )
+ ->method( 'get_title_suggestions' )
+ ->willReturn( new WP_Error( 'api_error', 'API request failed' ) );
+
+ self::set_protected_property( $this->get_endpoint(), 'suggestions_api', $mock_suggestions_api );
+
+ // Create a mock request.
+ $request = new WP_REST_Request( 'POST', '/title-suggestions/generate' );
+ $request->set_param( 'text', 'Test content' );
+ $request->set_param( 'limit', 3 );
+ $request->set_param( 'style', 'neutral' );
+ $request->set_param( 'persona', 'journalist' );
+
+ // Call the generate_titles method.
+ $response = $this->get_endpoint()->generate_titles( $request );
+
+ // Assert that the response is a WP_Error.
+ self::assertInstanceOf( WP_Error::class, $response );
+ self::assertEquals( 'api_error', $response->get_error_code() );
+ }
+}
diff --git a/tests/Integration/RestAPI/RestAPIControllerTest.php b/tests/Integration/RestAPI/RestAPIControllerTest.php
new file mode 100644
index 000000000..833d3600a
--- /dev/null
+++ b/tests/Integration/RestAPI/RestAPIControllerTest.php
@@ -0,0 +1,57 @@
+test_controller = new REST_API_Controller( $parsely );
+ }
+
+ /**
+ * Tests the constructor sets up the correct namespace and version.
+ *
+ * @since 3.17.0
+ *
+ * @covers \Parsely\REST_API\REST_API_Controller::__construct
+ * @uses \Parsely\REST_API\REST_API_Controller::get_full_namespace
+ */
+ public function test_constructor_sets_up_namespace_and_version(): void {
+ self::assertEquals( 'wp-parsely/v2', $this->test_controller->get_full_namespace() );
+ }
+}
diff --git a/tests/Integration/RestAPI/Settings/BaseSettingsEndpointTest.php b/tests/Integration/RestAPI/Settings/BaseSettingsEndpointTest.php
new file mode 100644
index 000000000..aa5a4d9e4
--- /dev/null
+++ b/tests/Integration/RestAPI/Settings/BaseSettingsEndpointTest.php
@@ -0,0 +1,221 @@
+ $extra_data Any Extra key/value pairs to add.
+ * @return array The generated JSON array.
+ */
+ abstract protected function generate_json(
+ ?string $metric = null,
+ ?string $period = null,
+ array $extra_data = array()
+ ): array;
+
+ /**
+ * Returns the default value for the endpoint.
+ *
+ * @since 3.17.0
+ *
+ * @return array The default value for the endpoint.
+ */
+ abstract protected function get_default_value(): array;
+
+ /**
+ * Verifies that the route is registered.
+ *
+ * @since 3.17.0
+ */
+ protected function run_test_route_is_registered(): void {
+ $routes = rest_get_server()->get_routes();
+
+ // Check that the main route is registered.
+ $expected_route = $this->get_endpoint()->get_full_endpoint( '/' );
+ self::assertArrayHasKey( $expected_route, $routes );
+
+ // Check that the route is associated with the GET and PUT methods.
+ $route_data = $routes[ $expected_route ];
+ self::assertArrayHasKey( 'GET', $route_data[0]['methods'] );
+ self::assertArrayHasKey( 'PUT', $route_data[0]['methods'] );
+
+ // Check the `/get` route.
+ $expected_route = $this->get_endpoint()->get_full_endpoint( '/get' );
+ self::assertArrayHasKey( $expected_route, $routes );
+
+ // Check that the route is associated with the GET method.
+ $route_data = $routes[ $expected_route ];
+ self::assertArrayHasKey( 'GET', $route_data[0]['methods'] );
+
+ // Check the `/set` route.
+ $expected_route = $this->get_endpoint()->get_full_endpoint( '/set' );
+ self::assertArrayHasKey( $expected_route, $routes );
+
+ // Check that the route is associated with the PUT method.
+ $route_data = $routes[ $expected_route ];
+ self::assertArrayHasKey( 'PUT', $route_data[0]['methods'] );
+ }
+
+ /**
+ * Verifies that the endpoint returns the correct default value.
+ *
+ * @since 3.13.0
+ * @since 3.17.0 Moved from the old BaseUserMetaEndpointTest class.
+ */
+ public function run_test_endpoint_returns_value_on_get_request(): void {
+ $this->set_current_user_to_admin();
+
+ $value = rest_do_request(
+ new WP_REST_Request(
+ 'GET',
+ $this->get_endpoint()->get_full_endpoint( '/get' )
+ )
+ )->get_data();
+ $value = $this->wp_json_encode( $value );
+
+ $expected = $this->wp_json_encode(
+ $this->get_default_value()
+ );
+
+ self::assertSame( $expected, $value );
+ }
+
+ /**
+ * Provides data for testing PUT requests.
+ *
+ * @since 3.13.0
+ * @since 3.17.0 Moved from the old BaseUserMetaEndpointTest class.
+ *
+ * @return iterable
+ */
+ public function provide_put_requests_data(): iterable {
+ $default_value = $this->generate_json( 'views', '7d' );
+ $valid_value = $this->generate_json( 'avg_engaged', '1h' );
+
+ // Valid non-default value. It should be returned unmodified.
+ yield 'valid period and metric values' => array(
+ 'test_data' => $valid_value,
+ 'expected' => $valid_value,
+ );
+
+ // Missing or problematic keys. Defaults should be used for the missing or problematic keys.
+ yield 'valid period value, no metric value' => array(
+ 'test_data' => $this->generate_json( null, '1h' ),
+ 'expected' => $this->generate_json( 'views', '1h' ),
+ );
+ yield 'valid metric value, no period value' => array(
+ 'test_data' => $this->generate_json( 'avg_engaged' ),
+ 'expected' => $this->generate_json( 'avg_engaged', '7d' ),
+ );
+ yield 'no values' => array(
+ 'test_data' => $this->generate_json(),
+ 'expected' => $default_value,
+ );
+
+ // Invalid values. They should be adjusted to their defaults.
+ yield 'invalid period value' => array(
+ 'test_data' => $this->generate_json( 'avg_engaged', 'invalid' ),
+ 'expected' => $this->generate_json( 'avg_engaged', '7d' ),
+ );
+ yield 'invalid metric value' => array(
+ 'test_data' => $this->generate_json( 'invalid', '1h' ),
+ 'expected' => $this->generate_json( 'views', '1h' ),
+ );
+ yield 'invalid period and metric values' => array(
+ 'test_data' => $this->generate_json( 'invalid', 'invalid' ),
+ 'expected' => $default_value,
+ );
+
+ // Invalid extra data passed. Any such data should be discarded.
+ yield 'invalid additional value' => array(
+ 'test_data' => $this->generate_json(
+ 'avg_engaged',
+ '1h',
+ array( 'invalid' )
+ ),
+ 'expected' => $valid_value,
+ );
+ yield 'invalid additional key/value pair' => array(
+ 'test_data' => $this->generate_json(
+ 'avg_engaged',
+ '1h',
+ array( 'invalid_key' => 'invalid_value' )
+ ),
+ 'expected' => $valid_value,
+ );
+ }
+
+ /**
+ * Sends a PUT request to the endpoint.
+ *
+ * @since 3.13.0
+ * @since 3.17.0 Moved from the old BaseUserMetaEndpointTest class.
+ *
+ * @param array