Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add WPGraphQL Extensions Page to the WordPress admin #3188

Merged
merged 55 commits into from
Jan 16, 2025

Conversation

josephfusco
Copy link
Member

@josephfusco josephfusco commented Aug 12, 2024

This is the 1st pass at #304

Features

This page is registered as a submenu of WPGraphQL: /wp-admin/admin.php?page=wpgraphql-extensions

UI Scenarios

State Button Label Description
Uninstalled Install & Activate Indicates the plugin is not installed and can be installed and activated.
Installed Activate Indicates the plugin is installed but not active.
Active Active (subdued) Indicates the plugin is currently active.

Demo

extensions.page.mov

Key Points

  • Extensions/plugins cannot be deactivated or deleted on this screen by design.
  • Plugin data is hardcoded now with the intent to fetch from WordPress.org

Possible Additions

  • Add plugin thumbnail to card (similar to WordPress.org)
  • Allow new platforms to be registered (Bitbucket, Gitlab, etc)

TODO

  • Setup @wordpress/data in order to use @wordpress/notice store

@josephfusco josephfusco mentioned this pull request Aug 12, 2024
12 tasks
build/extensions.js Fixed Show fixed Hide fixed
build/extensions.js Fixed Show fixed Hide fixed
build/extensions.js Fixed Show fixed Hide fixed
build/extensions.js Fixed Show fixed Hide fixed
build/extensions.js Fixed Show fixed Hide fixed
build/extensions.js Fixed Show fixed Hide fixed
</div>
<div className="action-links">
<ul className="plugin-action-buttons">
{host.includes('wordpress.org') && (

Check failure

Code scanning / CodeQL

Incomplete URL substring sanitization High

'
wordpress.org
' can be anywhere in the URL, and arbitrary hosts may come before or after it.

Copilot Autofix AI about 1 month ago

To fix the problem, we need to ensure that the host of the URL is explicitly checked against a whitelist of allowed hosts. This involves parsing the URL to extract the host and then comparing it against a predefined list of allowed hosts. This approach prevents malicious URLs from bypassing the check by embedding the allowed host in unexpected locations.

  1. Parse the URL to extract the host.
  2. Define a whitelist of allowed hosts.
  3. Check if the parsed host is in the whitelist.
  4. Update the relevant code to use this new check.
Suggested changeset 1
packages/extensions/PluginCard.js

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/extensions/PluginCard.js b/packages/extensions/PluginCard.js
--- a/packages/extensions/PluginCard.js
+++ b/packages/extensions/PluginCard.js
@@ -41,3 +41,5 @@
 
-    const host = new URL(plugin.plugin_url).host;
+    const url = new URL(plugin.plugin_url);
+    const host = url.host;
+    const allowedHosts = ['wordpress.org', 'github.com'];
     const { buttonText, buttonDisabled } = getButtonDetails(host, plugin.plugin_url, isInstalled, isActive, installing, activating);
@@ -71,3 +73,3 @@
                     <ul className="plugin-action-buttons">
-                        {host.includes('wordpress.org') && (
+                        {allowedHosts.includes(host) && host === 'wordpress.org' && (
                             <li>
@@ -84,3 +86,3 @@
                         )}
-                        {host.includes('github.com') && (
+                        {allowedHosts.includes(host) && host === 'github.com' && (
                             <li>
EOF
@@ -41,3 +41,5 @@

const host = new URL(plugin.plugin_url).host;
const url = new URL(plugin.plugin_url);
const host = url.host;
const allowedHosts = ['wordpress.org', 'github.com'];
const { buttonText, buttonDisabled } = getButtonDetails(host, plugin.plugin_url, isInstalled, isActive, installing, activating);
@@ -71,3 +73,3 @@
<ul className="plugin-action-buttons">
{host.includes('wordpress.org') && (
{allowedHosts.includes(host) && host === 'wordpress.org' && (
<li>
@@ -84,3 +86,3 @@
)}
{host.includes('github.com') && (
{allowedHosts.includes(host) && host === 'github.com' && (
<li>
Copilot is powered by AI and may make mistakes. Always verify output.
Positive Feedback
Negative Feedback

Provide additional feedback

Please help us improve GitHub Copilot by sharing more details about this comment.

Please select one or more of the options
@coveralls
Copy link

coveralls commented Aug 12, 2024

Coverage Status

coverage: 83.189% (-1.2%) from 84.383%
when pulling 69e911a on feat/3049-extensions-page-fix
into 8cdae0e on develop.

packages/extensions/PluginCard.js Show resolved Hide resolved
packages/extensions/PluginCard.js Show resolved Hide resolved
packages/extensions/useInstallPlugin.js Show resolved Hide resolved
packages/extensions/PluginCard.js Show resolved Hide resolved
packages/extensions/useInstallPlugin.js Show resolved Hide resolved
build/extensions.js Fixed Show fixed Hide fixed
build/extensions.js Fixed Show fixed Hide fixed
packages/extensions/utils.js Show resolved Hide resolved
packages/extensions/utils.js Show resolved Hide resolved
packages/extensions/PluginCard.js Show resolved Hide resolved
src/Admin/Extensions/Extensions.php Show resolved Hide resolved
src/Admin/Extensions/Extensions.php Outdated Show resolved Hide resolved
Copy link

@codeclimate codeclimate bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR diff size of 10733 lines exceeds the maximum allowed for the inline comments feature.

@jasonbahl jasonbahl changed the title feat: Extensions Page (1st pass) feat: Extensions Page Jan 16, 2025
Copy link

@codeclimate codeclimate bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR diff size of 10731 lines exceeds the maximum allowed for the inline comments feature.

…l-yoast-seo-addon

feat: add the WPGraphQL Yoast SEO addon to the extensions page
@jasonbahl jasonbahl changed the title feat: Extensions Page feat: Add WPGraphQL Extensions Page to the WordPress admin Jan 16, 2025
Copy link

@codeclimate codeclimate bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR diff size of 10742 lines exceeds the maximum allowed for the inline comments feature.

# Conflicts:
#	build/app.asset.php
#	build/app.js
#	composer.lock
#	package-lock.json
#	webpack.config.js
Copy link

@codeclimate codeclimate bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR diff size of 11271 lines exceeds the maximum allowed for the inline comments feature.

@@ -0,0 +1 @@
(()=>{"use strict";var e={n:t=>{var n=t&&t.__esModule?()=>t.default:()=>t;return e.d(n,{a:n}),n},d:(t,n)=>{for(var a in n)e.o(n,a)&&!e.o(t,a)&&Object.defineProperty(t,a,{enumerable:!0,get:n[a]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)};const t=window.wp.element,n=window.React,a=window.wp.i18n,i=window.wp.components,s=window.wp.apiFetch;var l=e.n(s);const r=window.ReactJSXRuntime,o=({plugin:e})=>{const{installing:s,activating:o,status:p,error:c,installPlugin:u,activatePlugin:d}=((e,t)=>{const[i,s]=(0,n.useState)(!1),[r,o]=(0,n.useState)(!1),[p,c]=(0,n.useState)(""),[u,d]=(0,n.useState)(""),w=(e,t="")=>{c(e),d(t)},h=async(n=t)=>{if(o(!0),w((0,a.__)("Activating...","wp-graphql")),!n){let t=new URL(e).pathname.split("/").filter(Boolean).pop();n=`${t}/${t}.php`}try{const t=await l()({path:`/wp/v2/plugins/${n}`,method:"PUT",data:{status:"active"},headers:{"X-WP-Nonce":wpgraphqlExtensions.nonce}});if("active"===t.status)return w((0,a.__)("Active","wp-graphql")),window.wpgraphqlExtensions.extensions=window.wpgraphqlExtensions.extensions.map((t=>t.plugin_url===e?{...t,installed:!0,active:!0}:t)),!0;throw t.message.includes("Plugin file does not exist")?new Error((0,a.__)("Plugin file does not exist","wp-graphql")):new Error((0,a.__)("Activation failed","wp-graphql"))}catch(e){throw w((0,a.__)("Activation failed","wp-graphql"),e.message||(0,a.__)("Activation failed","wp-graphql")),e}finally{s(!1),o(!1)}};return{installing:i,activating:r,status:p,error:u,installPlugin:async()=>{s(!0),w((0,a.__)("Installing...","wp-graphql"));let n=new URL(e).pathname.split("/").filter(Boolean).pop();try{if("inactive"!==(await l()({path:"/wp/v2/plugins",method:"POST",data:{slug:n,status:"inactive"},headers:{"X-WP-Nonce":wpgraphqlExtensions.nonce}})).status)throw new Error((0,a.__)("Installation failed","wp-graphql"));await h(t)}catch(e){if(!e.message.includes("destination folder already exists"))throw w((0,a.__)("Installation failed","wp-graphql"),e.message||(0,a.__)("Installation failed","wp-graphql")),s(!1),e;await h(t)}},activatePlugin:h}})(e.plugin_url,e.plugin_path),[w,h]=(0,t.useState)(e.installed),[g,_]=(0,t.useState)(e.active),[m,x]=(0,t.useState)(!0);(0,t.useEffect)((()=>{h(e.installed),_(e.active)}),[e]);const b=new URL(e.plugin_url).host,{buttonText:f,buttonDisabled:v}=((e,t,n,i,s,l,r)=>{let o,p=!1,c=null;const u=e=>()=>window.open(e,"_blank");if(s)o=(0,a.__)("Installing...","wp-graphql"),p=!0;else if(l)o=(0,a.__)("Activating...","wp-graphql"),p=!0;else if(i)o=(0,a.__)("Active","wp-graphql"),p=!0;else if(n)o=(0,a.__)("Activate","wp-graphql"),c=r;else{const e=new URL(t).hostname.toLowerCase();switch(!0){case/github\.com$/.test(e):o=(0,a.__)("View on GitHub","wp-graphql"),c=u(t);break;case/bitbucket\.org$/.test(e):o=(0,a.__)("View on Bitbucket","wp-graphql"),c=u(t);break;case/gitlab\.com$/.test(e):o=(0,a.__)("View on GitLab","wp-graphql"),c=u(t);break;case/wordpress\.org$/.test(e):o=(0,a.__)("Install & Activate","wp-graphql"),c=r;break;default:o=(0,a.__)("View Plugin","wp-graphql"),c=u(t)}}return{buttonText:o,buttonDisabled:p,buttonOnClick:c}})(0,e.plugin_url,w,g,s,o),q=({author:e})=>e&&e.name&&e.homepage?(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)("em",{children:"By "}),(0,r.jsx)("cite",{children:(0,r.jsx)("a",{href:e.homepage,target:"_blank",rel:"noopener noreferrer",children:e.name})},e.homepage)]}):null;return(0,r.jsxs)("div",{className:"plugin-card",children:[(0,r.jsxs)("div",{className:"plugin-card-top",children:[(0,r.jsxs)("div",{className:"name column-name",children:[(0,r.jsx)("h2",{children:e.name}),(0,r.jsx)(q,{author:e.author}),e.experiment&&(0,r.jsx)("em",{className:"plugin-experimental",children:"(experimental)"})]}),(0,r.jsx)("div",{className:"action-links",children:(0,r.jsxs)("ul",{className:"plugin-action-buttons",children:[b.includes("wordpress.org")&&(0,r.jsx)("li",{children:(0,r.jsxs)("button",{type:"button",className:"button "+(g?"button-disabled":"button-primary"),disabled:v,onClick:async()=>{const t=w,n=g;try{w?(await d(e.plugin_path),_(!0)):(await u(),h(!0),_(!0))}catch(e){h(t),_(n)}finally{window.wpgraphqlExtensions.extensions=window.wpgraphqlExtensions.extensions.map((t=>t.plugin_url===e.plugin_url?{...t,installed:w,active:g}:t))}},children:[f,(s||o)&&(0,r.jsx)(i.Spinner,{})]})}),b.includes("github.com")&&(0,r.jsx)("li",{children:(0,r.jsx)("a",{href:e.plugin_url,target:"_blank",rel:"noopener noreferrer",className:"button button-secondary",children:(0,a.__)("View on GitHub","wp-graphql")})}),e.support_url&&(0,r.jsx)("li",{children:(0,r.jsx)("a",{href:e.support_url,target:"_blank",rel:"noopener noreferrer",className:"thickbox open-plugin-details-modal",children:(0,a.__)("Get Support","wp-graphql")})}),e.settings_url&&(0,r.jsx)("li",{children:(0,r.jsx)("a",{href:e.settings_url,children:(0,a.__)("Settings","wp-graphql")})})]})}),(0,r.jsx)("div",{className:"desc column-description",children:(0,r.jsx)("p",{children:e.description})})]}),c&&m&&(0,r.jsx)(i.Notice,{status:"error",isDismissible:!0,onRemove:()=>x(!1),children:c})]})},p=()=>{const[e,t]=(0,n.useState)([]);return(0,n.useEffect)((()=>{window.wpgraphqlExtensions&&window.wpgraphqlExtensions.extensions&&t(window.wpgraphqlExtensions.extensions)}),[]),(0,r.jsx)("div",{className:"wp-clearfix",children:(0,r.jsx)("div",{className:"plugin-cards",children:e.map((e=>(0,r.jsx)(o,{plugin:e},e.plugin_url)))})})};document.addEventListener("DOMContentLoaded",(()=>{const e=document.getElementById("wpgraphql-extensions");e&&(0,t.createRoot)(e).render((0,t.createElement)(p))}))})();

Check failure

Code scanning / CodeQL

Incomplete URL substring sanitization High

'
wordpress.org
' can be anywhere in the URL, and arbitrary hosts may come before or after it.

Copilot Autofix AI 11 days ago

Copilot could not generate an autofix suggestion

Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.

@@ -0,0 +1 @@
(()=>{"use strict";var e={n:t=>{var n=t&&t.__esModule?()=>t.default:()=>t;return e.d(n,{a:n}),n},d:(t,n)=>{for(var a in n)e.o(n,a)&&!e.o(t,a)&&Object.defineProperty(t,a,{enumerable:!0,get:n[a]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)};const t=window.wp.element,n=window.React,a=window.wp.i18n,i=window.wp.components,s=window.wp.apiFetch;var l=e.n(s);const r=window.ReactJSXRuntime,o=({plugin:e})=>{const{installing:s,activating:o,status:p,error:c,installPlugin:u,activatePlugin:d}=((e,t)=>{const[i,s]=(0,n.useState)(!1),[r,o]=(0,n.useState)(!1),[p,c]=(0,n.useState)(""),[u,d]=(0,n.useState)(""),w=(e,t="")=>{c(e),d(t)},h=async(n=t)=>{if(o(!0),w((0,a.__)("Activating...","wp-graphql")),!n){let t=new URL(e).pathname.split("/").filter(Boolean).pop();n=`${t}/${t}.php`}try{const t=await l()({path:`/wp/v2/plugins/${n}`,method:"PUT",data:{status:"active"},headers:{"X-WP-Nonce":wpgraphqlExtensions.nonce}});if("active"===t.status)return w((0,a.__)("Active","wp-graphql")),window.wpgraphqlExtensions.extensions=window.wpgraphqlExtensions.extensions.map((t=>t.plugin_url===e?{...t,installed:!0,active:!0}:t)),!0;throw t.message.includes("Plugin file does not exist")?new Error((0,a.__)("Plugin file does not exist","wp-graphql")):new Error((0,a.__)("Activation failed","wp-graphql"))}catch(e){throw w((0,a.__)("Activation failed","wp-graphql"),e.message||(0,a.__)("Activation failed","wp-graphql")),e}finally{s(!1),o(!1)}};return{installing:i,activating:r,status:p,error:u,installPlugin:async()=>{s(!0),w((0,a.__)("Installing...","wp-graphql"));let n=new URL(e).pathname.split("/").filter(Boolean).pop();try{if("inactive"!==(await l()({path:"/wp/v2/plugins",method:"POST",data:{slug:n,status:"inactive"},headers:{"X-WP-Nonce":wpgraphqlExtensions.nonce}})).status)throw new Error((0,a.__)("Installation failed","wp-graphql"));await h(t)}catch(e){if(!e.message.includes("destination folder already exists"))throw w((0,a.__)("Installation failed","wp-graphql"),e.message||(0,a.__)("Installation failed","wp-graphql")),s(!1),e;await h(t)}},activatePlugin:h}})(e.plugin_url,e.plugin_path),[w,h]=(0,t.useState)(e.installed),[g,_]=(0,t.useState)(e.active),[m,x]=(0,t.useState)(!0);(0,t.useEffect)((()=>{h(e.installed),_(e.active)}),[e]);const b=new URL(e.plugin_url).host,{buttonText:f,buttonDisabled:v}=((e,t,n,i,s,l,r)=>{let o,p=!1,c=null;const u=e=>()=>window.open(e,"_blank");if(s)o=(0,a.__)("Installing...","wp-graphql"),p=!0;else if(l)o=(0,a.__)("Activating...","wp-graphql"),p=!0;else if(i)o=(0,a.__)("Active","wp-graphql"),p=!0;else if(n)o=(0,a.__)("Activate","wp-graphql"),c=r;else{const e=new URL(t).hostname.toLowerCase();switch(!0){case/github\.com$/.test(e):o=(0,a.__)("View on GitHub","wp-graphql"),c=u(t);break;case/bitbucket\.org$/.test(e):o=(0,a.__)("View on Bitbucket","wp-graphql"),c=u(t);break;case/gitlab\.com$/.test(e):o=(0,a.__)("View on GitLab","wp-graphql"),c=u(t);break;case/wordpress\.org$/.test(e):o=(0,a.__)("Install & Activate","wp-graphql"),c=r;break;default:o=(0,a.__)("View Plugin","wp-graphql"),c=u(t)}}return{buttonText:o,buttonDisabled:p,buttonOnClick:c}})(0,e.plugin_url,w,g,s,o),q=({author:e})=>e&&e.name&&e.homepage?(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)("em",{children:"By "}),(0,r.jsx)("cite",{children:(0,r.jsx)("a",{href:e.homepage,target:"_blank",rel:"noopener noreferrer",children:e.name})},e.homepage)]}):null;return(0,r.jsxs)("div",{className:"plugin-card",children:[(0,r.jsxs)("div",{className:"plugin-card-top",children:[(0,r.jsxs)("div",{className:"name column-name",children:[(0,r.jsx)("h2",{children:e.name}),(0,r.jsx)(q,{author:e.author}),e.experiment&&(0,r.jsx)("em",{className:"plugin-experimental",children:"(experimental)"})]}),(0,r.jsx)("div",{className:"action-links",children:(0,r.jsxs)("ul",{className:"plugin-action-buttons",children:[b.includes("wordpress.org")&&(0,r.jsx)("li",{children:(0,r.jsxs)("button",{type:"button",className:"button "+(g?"button-disabled":"button-primary"),disabled:v,onClick:async()=>{const t=w,n=g;try{w?(await d(e.plugin_path),_(!0)):(await u(),h(!0),_(!0))}catch(e){h(t),_(n)}finally{window.wpgraphqlExtensions.extensions=window.wpgraphqlExtensions.extensions.map((t=>t.plugin_url===e.plugin_url?{...t,installed:w,active:g}:t))}},children:[f,(s||o)&&(0,r.jsx)(i.Spinner,{})]})}),b.includes("github.com")&&(0,r.jsx)("li",{children:(0,r.jsx)("a",{href:e.plugin_url,target:"_blank",rel:"noopener noreferrer",className:"button button-secondary",children:(0,a.__)("View on GitHub","wp-graphql")})}),e.support_url&&(0,r.jsx)("li",{children:(0,r.jsx)("a",{href:e.support_url,target:"_blank",rel:"noopener noreferrer",className:"thickbox open-plugin-details-modal",children:(0,a.__)("Get Support","wp-graphql")})}),e.settings_url&&(0,r.jsx)("li",{children:(0,r.jsx)("a",{href:e.settings_url,children:(0,a.__)("Settings","wp-graphql")})})]})}),(0,r.jsx)("div",{className:"desc column-description",children:(0,r.jsx)("p",{children:e.description})})]}),c&&m&&(0,r.jsx)(i.Notice,{status:"error",isDismissible:!0,onRemove:()=>x(!1),children:c})]})},p=()=>{const[e,t]=(0,n.useState)([]);return(0,n.useEffect)((()=>{window.wpgraphqlExtensions&&window.wpgraphqlExtensions.extensions&&t(window.wpgraphqlExtensions.extensions)}),[]),(0,r.jsx)("div",{className:"wp-clearfix",children:(0,r.jsx)("div",{className:"plugin-cards",children:e.map((e=>(0,r.jsx)(o,{plugin:e},e.plugin_url)))})})};document.addEventListener("DOMContentLoaded",(()=>{const e=document.getElementById("wpgraphql-extensions");e&&(0,t.createRoot)(e).render((0,t.createElement)(p))}))})();

Check failure

Code scanning / CodeQL

Incomplete URL substring sanitization High

'
github.com
' can be anywhere in the URL, and arbitrary hosts may come before or after it.

Copilot Autofix AI 11 days ago

Copilot could not generate an autofix suggestion

Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.

- update package-lock.json
Copy link

@codeclimate codeclimate bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR diff size of 8527 lines exceeds the maximum allowed for the inline comments feature.

Copy link

codeclimate bot commented Jan 16, 2025

Code Climate has analyzed commit 69e911a and detected 11 issues on this pull request.

Here's the issue category breakdown:

Category Count
Complexity 9
Duplication 2

View more on Code Climate.

@jasonbahl jasonbahl merged commit 98f69e7 into develop Jan 16, 2025
34 of 37 checks passed
@jasonbahl jasonbahl mentioned this pull request Jan 16, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants