diff --git a/.github/workflows/release_on_tag.yml b/.github/workflows/release_on_tag.yml
index 0cacbd4..9410e36 100644
--- a/.github/workflows/release_on_tag.yml
+++ b/.github/workflows/release_on_tag.yml
@@ -62,7 +62,7 @@ jobs:
run: |
gh release view "${{ github.ref_name }}" || \
gh release create "${{ github.ref_name }}" \
- --title "OneUpdate ${{ github.ref_name }}" \
+ --title "${{ github.ref_name }}" \
--generate-notes \
--draft
# Upload the artifact
diff --git a/assets/src/admin/plugin-manager/index.js b/assets/src/admin/plugin-manager/index.js
index 6f3ce2c..a902243 100644
--- a/assets/src/admin/plugin-manager/index.js
+++ b/assets/src/admin/plugin-manager/index.js
@@ -33,6 +33,7 @@ import { decodeEntities } from '@wordpress/html-entities';
import { arrowLeft, plus, loop } from '@wordpress/icons';
import PluginsSharing from '../../components/PluginsSharing';
import S3ZipUploader from '../../components/S3ZipUploader';
+import { PurifyElement } from '../../js/utils';
// Declare the OneUpdatePlugins variable
const OneUpdatePlugins = window.OneUpdatePlugins || {};
@@ -289,11 +290,11 @@ const PluginManager = () => {
name: decodeEntities( sampleSitePlugin?.Name || pluginInfo.name || slug ),
description: decodeEntities(
sampleSitePlugin?.Description ||
- pluginInfo.sections?.description?.replace( /<[^>]*>/g, '' ).substring( 0, 200 ) + '...' ||
+ PurifyElement( pluginInfo.sections?.description )?.substring( 0, 200 ) + '...' ||
pluginInfo.short_description ||
'No description available.',
),
- author: decodeEntities( sampleSitePlugin?.Author || pluginInfo.author?.replace( /<[^>]*>/g, '' ) || 'Unknown' ),
+ author: decodeEntities( sampleSitePlugin?.Author || PurifyElement( pluginInfo.author ) || 'Unknown' ),
version: sharedPluginData.version || sampleSitePlugin?.Version || pluginInfo.version || '0.0.0',
plugin_uri: sampleSitePlugin?.PluginURI || pluginInfo.homepage || '',
is_public: sampleSitePlugin?.is_public !== undefined ? sampleSitePlugin.is_public : Boolean( pluginInfo.download_link ),
diff --git a/assets/src/admin/plugin/index.js b/assets/src/admin/plugin/index.js
index 23c4ae5..24e1b86 100644
--- a/assets/src/admin/plugin/index.js
+++ b/assets/src/admin/plugin/index.js
@@ -16,7 +16,7 @@ const SiteTypeSelector = ( { value, setSiteType } ) => (
options={ [
{ label: __( 'Select…', 'oneupdate' ), value: '' },
{ label: __( 'Brand Site', 'oneupdate' ), value: 'brand-site' },
- { label: __( 'Governing site', 'oneupdate' ), value: 'governing-site' },
+ { label: __( 'Governing Site', 'oneupdate' ), value: 'governing-site' },
] }
/>
);
diff --git a/assets/src/admin/pull-requests/index.js b/assets/src/admin/pull-requests/index.js
new file mode 100644
index 0000000..2917c6b
--- /dev/null
+++ b/assets/src/admin/pull-requests/index.js
@@ -0,0 +1,554 @@
+import { useState, useEffect, useCallback, createRoot } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
+import {
+ Card,
+ CardHeader,
+ CardBody,
+ Button,
+ SelectControl,
+ TextControl,
+ Modal,
+ Spinner,
+ DropdownMenu,
+ __experimentalGrid as Grid, // eslint-disable-line @wordpress/no-unsafe-wp-apis
+ Snackbar,
+ MenuGroup,
+ MenuItem,
+} from '@wordpress/components';
+import { decodeEntities } from '@wordpress/html-entities';
+import { moreVertical } from '@wordpress/icons';
+import ViewIcon from '../../components/icons/View';
+
+const API_NAMESPACE = OneUpdatePullRequests.restUrl + '/oneupdate/v1/github';
+const NONCE = OneUpdatePullRequests.restNonce;
+const REPOS = OneUpdatePullRequests.repos;
+
+const PER_PAGE = 25;
+const PR_LABEL_STYLES = {
+ display: 'inline-block',
+ padding: '2px 6px',
+ borderRadius: '3px',
+ fontSize: '11px',
+ backgroundColor: `#1c1c1c`,
+ color: '#fff',
+};
+
+const GitHubPullRequests = () => {
+ const [ pullRequests, setPullRequests ] = useState( [] );
+ const [ loading, setLoading ] = useState( false );
+ const [ notice, setNotice ] = useState( null );
+ const [ selectedRepo, setSelectedRepo ] = useState( Object.keys( REPOS )?.[ 0 ] || '' );
+ const [ statusFilter, setStatusFilter ] = useState( 'all' );
+ const [ searchQuery, setSearchQuery ] = useState( '[OneUpdate]' );
+ const [ page, setPage ] = useState( 1 );
+ const [ totalPages, setTotalPages ] = useState( 1 );
+ const [ selectedPR, setSelectedPR ] = useState( null );
+ const [ isDetailModalOpen, setIsDetailModalOpen ] = useState( false );
+ const [ prDetails, setPrDetails ] = useState( null );
+ const [ detailsLoading, setDetailsLoading ] = useState( false );
+ const [ currentPage, setCurrentPage ] = useState( 1 );
+
+ // Repo options for SelectControl
+ const repoOptions = Object.entries( REPOS ).map( ( [ key, value ] ) => ( {
+ label: `${ value } (${ key })`,
+ value: key,
+ } ) );
+
+ // Status filter options
+ const statusOptions = [
+ { label: __( 'All Status', 'oneupdate' ), value: 'all' },
+ { label: __( 'Open', 'oneupdate' ), value: 'open' },
+ { label: __( 'Merged/Closed', 'oneupdate' ), value: 'closed' },
+ ];
+
+ const fetchPullRequests = useCallback( async () => {
+ if ( ! selectedRepo ) {
+ return;
+ }
+
+ setLoading( true );
+ setNotice( null );
+
+ try {
+ const params = new URLSearchParams( {
+ per_page: PER_PAGE.toString(),
+ page: page.toString(),
+ state: statusFilter,
+ } );
+
+ if ( searchQuery.trim() ) {
+ params.append( 'search_query', searchQuery.trim() );
+ }
+
+ const response = await fetch(
+ `${ API_NAMESPACE }/pull-requests/${ selectedRepo }?${ params.toString() }&_=${ new Date().getTime() }`,
+ {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-WP-NONCE': NONCE,
+ },
+ },
+ );
+
+ if ( ! response.ok ) {
+ if ( response?.statusText === 'Unprocessable Entity' ) {
+ setNotice( {
+ type: 'error',
+ message: __( 'Please enter valid character to search pull requests.', 'oneupdate' ),
+ } );
+ } else {
+ setNotice( {
+ type: 'error',
+ message: __( 'Failed to fetch pull requests.', 'oneupdate' ),
+ } );
+ }
+ return;
+ }
+
+ const data = await response.json();
+
+ if ( data.success ) {
+ setPullRequests( data.pull_requests || [] );
+ // Calculate total pages based on response (you might need to adjust based on your API)
+ const totalCount = response.headers.get( 'X-WP-Total' ) || data.pull_requests.length;
+ setTotalPages( Math.ceil( totalCount / PER_PAGE ) );
+ setCurrentPage( data?.pagination?.current_page || 1 );
+ } else {
+ setNotice( {
+ type: 'error',
+ message: __( 'Error fetching pull requests.', 'oneupdate' ),
+ } );
+ }
+ } catch ( error ) {
+ setNotice( {
+ type: 'error',
+ message: error.message || __( 'Error fetching pull requests.', 'oneupdate' ),
+ } );
+ setPullRequests( [] );
+ } finally {
+ setLoading( false );
+ }
+ }, [ selectedRepo, statusFilter, searchQuery, page ] );
+
+ const fetchPRDetails = useCallback( async ( prNumber ) => {
+ if ( ! selectedRepo || ! prNumber ) {
+ return;
+ }
+
+ setDetailsLoading( true );
+ try {
+ const response = await fetch(
+ `${ API_NAMESPACE }/pull-requests/${ selectedRepo }?pr_number=${ prNumber }&_=${ new Date().getTime() }`,
+ {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-WP-NONCE': NONCE,
+ },
+ },
+ );
+
+ if ( ! response.ok ) {
+ throw new Error( 'Failed to fetch PR details' );
+ }
+
+ const data = await response.json();
+
+ if ( data.success && data.pull_request?.[ 0 ] ) {
+ setPrDetails( data.pull_request[ 0 ] );
+ } else {
+ throw new Error( 'Failed to fetch PR details' );
+ }
+ } catch ( error ) {
+ setNotice( {
+ type: 'error',
+ message: error.message || __( 'Error fetching PR details.', 'oneupdate' ),
+ } );
+ } finally {
+ setDetailsLoading( false );
+ }
+ }, [ selectedRepo ] );
+
+ const openDetailModal = ( pr ) => {
+ setSelectedPR( pr );
+ setIsDetailModalOpen( true );
+ fetchPRDetails( pr.number );
+ };
+
+ const closeModals = () => {
+ setIsDetailModalOpen( false );
+ setSelectedPR( null );
+ setPrDetails( null );
+ };
+
+ const formatDate = ( dateString ) => {
+ if ( ! dateString ) {
+ return __( 'N/A', 'oneupdate' );
+ }
+ return new Date( dateString ).toLocaleString();
+ };
+
+ const getPRStatusBadge = ( pr ) => {
+ let status = pr.state;
+ let color = '#6b7280';
+
+ if ( pr.merged_at ) {
+ status = 'merged';
+ color = '#7c3aed';
+ } else if ( pr.state === 'open' ) {
+ color = '#059669';
+ } else if ( pr.state === 'closed' ) {
+ color = '#dc2626';
+ }
+
+ return (
+
+ { status }
+
+ );
+ };
+
+ const renderPRActions = ( pr ) => {
+ return (
+
+ { ( { onClose } ) => {
+ return (
+ <>
+
+ { pr.state === 'open' && (
+ <>
+ }
+ onClick={ () => {
+ openDetailModal( pr );
+ onClose();
+ } }
+ >
+ { __( 'View Details', 'oneupdate' ) }
+
+ >
+ ) }
+
+ { pr.state !== 'open' && (
+
+ }
+ onClick={ () => {
+ openDetailModal( pr );
+ onClose();
+ } }
+ >
+ { __( 'View Details', 'oneupdate' ) }
+
+
+ ) }
+ >
+ );
+ } }
+
+ );
+ };
+
+ // Reset page when filters change
+ useEffect( () => {
+ setPage( 1 );
+ }, [ selectedRepo, statusFilter, searchQuery ] );
+
+ // Fetch PRs when dependencies change
+ useEffect( () => {
+ fetchPullRequests();
+ }, [ fetchPullRequests ] );
+
+ return (
+ <>
+
+
+ { __( 'GitHub Pull Requests', 'oneupdate' ) }
+
+
+ { /* Filters */ }
+
+
+ <>
+
+
+ >
+
+
+ { /* PR Table */ }
+
+
+
+ | { __( 'PR #', 'oneupdate' ) } |
+ { __( 'Title', 'oneupdate' ) } |
+ { __( 'Author', 'oneupdate' ) } |
+ { __( 'Status', 'oneupdate' ) } |
+ { __( 'Created at', 'oneupdate' ) } |
+ { __( 'Labels', 'oneupdate' ) } |
+ { __( 'Actions', 'oneupdate' ) } |
+
+
+
+ { loading && (
+
+ |
+
+ |
+
+ ) }
+ { ! loading && pullRequests.length === 0 && (
+
+ |
+ { __( 'No pull requests found.', 'oneupdate' ) }
+ |
+
+ ) }
+ { pullRequests.map( ( pr ) => (
+
+ |
+
+ #{ pr.number }
+
+ |
+
+
+ { decodeEntities( pr.title ) }
+
+ |
+
+
+ 
+ { pr.user.login }
+
+ |
+ { getPRStatusBadge( pr ) } |
+ { formatDate( pr.created_at ) } |
+
+ { pr.labels.length > 0 ? (
+
+ { pr.labels.slice( 0, 2 ).map( ( label ) => (
+
+ { label.name }
+
+ ) ) }
+ { pr.labels.length > 2 && (
+
+ +{ pr.labels.length - 2 } { __( 'more', 'oneupdate' ) }
+
+ ) }
+
+ ) : (
+
+ { __( 'No labels', 'oneupdate' ) }
+
+ ) }
+ |
+ { renderPRActions( pr ) } |
+
+ ) ) }
+
+
+
+ { /* Pagination */ }
+ { (
+
+
+
+ { __( 'Page', 'oneupdate' ) } { page } { __( 'of', 'oneupdate' ) } { totalPages === 0 || totalPages < currentPage ? currentPage : totalPages }
+
+
+
+ ) }
+
+
+
+ { /* PR Details Modal */ }
+ { isDetailModalOpen && selectedPR && (
+
+ { /* Detailed PR Info */ }
+ { detailsLoading && (
+
+
+
{ __( 'Loading PR details…', 'oneupdate' ) }
+
+ ) }
+
+ { ! detailsLoading && prDetails && (
+ <>
+
+
+
+
{ __( 'PR Number:', 'oneupdate' ) } #{ prDetails.number }
+
{ __( 'Title:', 'oneupdate' ) } { decodeEntities( prDetails.title ) }
+
{ __( 'Author:', 'oneupdate' ) } { prDetails.user.login }
+
{ __( 'Status:', 'oneupdate' ) } { getPRStatusBadge( prDetails ) }
+
+
+
{ __( 'Created:', 'oneupdate' ) } { formatDate( prDetails.created_at ) }
+
{ __( 'Updated:', 'oneupdate' ) } { formatDate( prDetails.updated_at ) }
+
{ __( 'Branch:', 'oneupdate' ) } { prDetails.pr_branch } → { prDetails.base_branch }
+
+ { __( 'GitHub URL:', 'oneupdate' ) }{ ' ' }
+
+ { __( 'View on GitHub', 'oneupdate' ) }
+
+
+
+
+
+ { /* Labels */ }
+ { prDetails.labels.length > 0 && (
+
+
{ __( 'Labels:', 'oneupdate' ) }
+
+ { prDetails.labels.map( ( label ) => (
+
+ { label.name }
+
+ ) ) }
+
+
+ ) }
+
+ { /* Description */ }
+ { prDetails.body && (
+
+
{ __( 'Description:', 'oneupdate' ) }
+
+
+ { decodeEntities( prDetails.body ) }
+
+
+
+ ) }
+
+
+ { /* Merged By Info */ }
+ { prDetails.merged_by && (
+
+
+ { prDetails.merged_by && (
+
+
{ __( 'Merged By:', 'oneupdate' ) }
+

+
{ prDetails.merged_by.login }
+
+ ) }
+
+ ) }
+ >
+ ) }
+
+
+ ) }
+
+ { /* Notice Snackbar */ }
+ { notice?.message && (
+ setNotice( null ) }
+ className={ notice?.type === 'error' ? 'oneupdate-error-notice' : 'oneupdate-success-notice' }
+ >
+ { notice.message }
+
+ ) }
+ >
+ );
+};
+
+// Render to Gutenberg admin page with ID: oneupdate-pull-requests
+const target = document.getElementById( 'oneupdate-pull-requests' );
+if ( target ) {
+ const root = createRoot( target );
+ root.render( );
+}
diff --git a/assets/src/components/PluginGrid.js b/assets/src/components/PluginGrid.js
index f619b82..31b9ef3 100644
--- a/assets/src/components/PluginGrid.js
+++ b/assets/src/components/PluginGrid.js
@@ -153,7 +153,7 @@ const PluginGrid = () => {
{ /* Loading State */ }
{ loading && (
-
+
{ __( 'Loading plugins…', 'oneupdate' ) }
) }
diff --git a/assets/src/components/SiteSettings.js b/assets/src/components/SiteSettings.js
index e6cfa02..5b94788 100644
--- a/assets/src/components/SiteSettings.js
+++ b/assets/src/components/SiteSettings.js
@@ -73,7 +73,7 @@ const SiteSettings = () => {
}, [ fetchApiKey ] );
if ( isLoading ) {
- return ;
+ return ;
}
return (
diff --git a/assets/src/components/icons/Close.js b/assets/src/components/icons/Close.js
new file mode 100644
index 0000000..41402e0
--- /dev/null
+++ b/assets/src/components/icons/Close.js
@@ -0,0 +1,7 @@
+export default function CloseIcon() {
+ return (
+
+ );
+}
diff --git a/assets/src/components/icons/Merge.js b/assets/src/components/icons/Merge.js
new file mode 100644
index 0000000..92295b0
--- /dev/null
+++ b/assets/src/components/icons/Merge.js
@@ -0,0 +1,5 @@
+export default function MergeIcon() {
+ return (
+
+ );
+}
diff --git a/assets/src/components/icons/View.js b/assets/src/components/icons/View.js
new file mode 100644
index 0000000..e4515de
--- /dev/null
+++ b/assets/src/components/icons/View.js
@@ -0,0 +1,7 @@
+export default function ViewIcon() {
+ return (
+
+ );
+}
diff --git a/assets/src/css/admin.scss b/assets/src/css/admin.scss
index 48532fd..9f8c696 100644
--- a/assets/src/css/admin.scss
+++ b/assets/src/css/admin.scss
@@ -22,6 +22,7 @@
}
}
+#oneupdate-pull-requests,
#oneupdate-settings-page,
#oneupdate-plugin-manager {
diff --git a/assets/src/js/utils.js b/assets/src/js/utils.js
index 9d24970..85fc254 100644
--- a/assets/src/js/utils.js
+++ b/assets/src/js/utils.js
@@ -1,12 +1,14 @@
+import DOMPurify from 'dompurify';
+
const isURL = ( str ) => {
const pattern = new RegExp(
- '^(https?:\\/\\/)?' +
- '(([a-z\\d]([a-z\\d-]*[a-z\\d])*):([a-z\\d-]*[a-z\\d])*@)?' +
- '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.?)+[a-z]{2,}|' +
- '((\\d{1,3}\\.){3}\\d{1,3}))' +
- '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' +
- '(\\?[;&a-z\\d%_.~+=-]*)?' +
- '(\\#[-a-z\\d_]*)?$', 'i',
+ '^https?:\\/\\/' +
+ '(?:[a-z\\d](?:[a-z\\d-]*[a-z\\d])?\\.)?' +
+ '[a-z\\d](?:[a-z\\d-]*[a-z\\d])?\\.' +
+ '[a-z]{2,}' +
+ '(?::\\d+)?' +
+ '(?:\\/[^\\s]*)?' +
+ '$', 'i',
);
return pattern.test( str );
};
@@ -20,7 +22,12 @@ const isValidUrl = ( url ) => {
}
};
+const PurifyElement = ( item ) => {
+ return DOMPurify.sanitize( item, { ALLOWED_TAGS: [] } );
+};
+
export {
isURL,
isValidUrl,
+ PurifyElement,
};
diff --git a/composer.json b/composer.json
index e505e20..d882971 100644
--- a/composer.json
+++ b/composer.json
@@ -1,6 +1,6 @@
{
"name": "rtcamp/oneupdate",
- "version": "1.0.1",
+ "version": "1.0.2",
"description": "OneUpdate - Enterprise WordPress Plugin Manager Automate plugin updates across multiple WordPress sites with CI/CD integration. Creates pull requests for seamless development-to-production workflows.",
"type": "wordpress-plugin",
"autoload": {
@@ -32,5 +32,10 @@
},
"require": {
"aws/aws-sdk-php": "^3.343"
+ },
+ "scripts": {
+ "pot": "wp i18n make-pot . languages/oneupdate.pot --domain=oneupdate --exclude=node_modules,vendor",
+ "phpcs": "vendor/bin/phpcs --standard=./phpcs.xml.dist",
+ "phpcs:fix": "vendor/bin/phpcbf --standard=./phpcs.xml.dist"
}
}
diff --git a/inc/classes/class-assets.php b/inc/classes/class-assets.php
index a31b12d..2727a7d 100644
--- a/inc/classes/class-assets.php
+++ b/inc/classes/class-assets.php
@@ -7,6 +7,7 @@
namespace OneUpdate;
+use OneUpdate\Plugin_Configs\Constants;
use OneUpdate\Traits\Singleton;
/**
@@ -54,12 +55,9 @@ public function enqueue_admin_scripts( $hook_suffix ) {
'oneupdate-settings-script',
'OneUpdateSettings',
array(
- 'nonce' => wp_create_nonce( 'wp_rest' ),
'restUrl' => esc_url( home_url( '/wp-json' ) ),
- 'apiKey' => get_option( 'oneupdate_child_site_api_key', 'default_api_key' ),
+ 'apiKey' => get_option( Constants::ONEUPDATE_API_KEY, '' ),
'restNonce' => wp_create_nonce( 'wp_rest' ),
- 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
- 'setupUrl' => admin_url( 'admin.php?page=oneupdate-settings' ),
)
);
@@ -70,7 +68,6 @@ public function enqueue_admin_scripts( $hook_suffix ) {
if ( strpos( $hook_suffix, 'toplevel_page_oneupdate' ) !== false ) {
// remove all admin notices.
remove_all_actions( 'admin_notices' );
- setcookie( 'vip-go-cb', '1', time() + ( 86400 * 30 ), '/' ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.cookies_setcookie -- this is to avoid caching issue in vip environment.
$this->register_script(
'oneupdate-plugins-manager-script',
@@ -81,9 +78,8 @@ public function enqueue_admin_scripts( $hook_suffix ) {
'oneupdate-plugins-manager-script',
'OneUpdatePlugins',
array(
- 'nonce' => wp_create_nonce( 'wp_rest' ),
'restUrl' => esc_url( home_url( '/wp-json' ) ),
- 'apiKey' => get_option( 'oneupdate_child_site_api_key', 'default_api_key' ),
+ 'apiKey' => get_option( Constants::ONEUPDATE_API_KEY, '' ),
'restNonce' => wp_create_nonce( 'wp_rest' ),
)
);
@@ -109,11 +105,9 @@ public function enqueue_admin_scripts( $hook_suffix ) {
'oneupdate-setup-script',
'OneUpdateSettings',
array(
- 'nonce' => wp_create_nonce( 'wp_rest' ),
'restUrl' => esc_url( home_url( '/wp-json' ) ),
- 'apiKey' => get_option( 'oneupdate_child_site_api_key', 'default_api_key' ),
+ 'apiKey' => get_option( Constants::ONEUPDATE_API_KEY, '' ),
'restNonce' => wp_create_nonce( 'wp_rest' ),
- 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'setupUrl' => admin_url( 'admin.php?page=oneupdate-settings' ),
)
);
@@ -122,6 +116,38 @@ public function enqueue_admin_scripts( $hook_suffix ) {
}
+ if ( strpos( $hook_suffix, 'oneupdate-pull-requests' ) !== false ) {
+ remove_all_actions( 'admin_notices' );
+ $this->register_script(
+ 'oneupdate-pull-requests-script',
+ 'js/pull-requests.js',
+ );
+
+ $site_name_gh_repo = array();
+ $oneupdate_sites = $GLOBALS['oneupdate_sites'] ?? array();
+ if ( ! empty( $oneupdate_sites ) && is_array( $oneupdate_sites ) ) {
+ foreach ( $oneupdate_sites as $site ) {
+ if ( ! empty( $site['siteName'] ) && ! empty( $site['gh_repo'] ) && in_array( $site['gh_repo'], $site_name_gh_repo, true ) === false ) {
+ $site_name_gh_repo[ $site['gh_repo'] ] = $site['siteName'];
+ }
+ }
+ }
+
+ wp_localize_script(
+ 'oneupdate-pull-requests-script',
+ 'OneUpdatePullRequests',
+ array(
+ 'restUrl' => esc_url( home_url( '/wp-json' ) ),
+ 'apiKey' => get_option( Constants::ONEUPDATE_API_KEY, '' ),
+ 'restNonce' => wp_create_nonce( 'wp_rest' ),
+ 'repos' => $site_name_gh_repo,
+ )
+ );
+
+ wp_enqueue_script( 'oneupdate-pull-requests-script' );
+
+ }
+
// load admin styles.
$this->register_style( 'oneupdate-admin-style', 'css/admin.css' );
wp_enqueue_style( 'oneupdate-admin-style' );
diff --git a/inc/classes/class-cache.php b/inc/classes/class-cache.php
index 10343fd..a955aa0 100644
--- a/inc/classes/class-cache.php
+++ b/inc/classes/class-cache.php
@@ -34,7 +34,7 @@ protected function __construct() {
public function setup_hooks(): void {
// if current site type is governing site then do not run the cache hooks.
- if ( 'governing-site' === get_option( 'oneupdate_site_type', '' ) ) {
+ if ( Utils::is_governing_site() ) {
return;
}
diff --git a/inc/classes/class-hooks.php b/inc/classes/class-hooks.php
index da5b768..3e592d3 100644
--- a/inc/classes/class-hooks.php
+++ b/inc/classes/class-hooks.php
@@ -7,6 +7,7 @@
namespace OneUpdate;
+use OneUpdate\Plugin_Configs\Constants;
use OneUpdate\Traits\Singleton;
/**
@@ -59,7 +60,7 @@ public function add_body_class_for_modal( $classes ): string {
}
// get oneupdate_site_type_transient transient to check if site type is set.
- $site_type_transient = get_transient( 'oneupdate_site_type_transient' );
+ $site_type_transient = get_transient( Constants::ONEUPDATE_SITE_TYPE_TRANSIENT );
if ( $site_type_transient ) {
// If site type is already set, do not show the modal.
return $classes;
@@ -85,7 +86,7 @@ public function add_site_selection_modal(): void {
}
// get oneupdate_site_type_transient transient to check if site type is set.
- $site_type_transient = get_transient( 'oneupdate_site_type_transient' );
+ $site_type_transient = get_transient( Constants::ONEUPDATE_SITE_TYPE_TRANSIENT );
if ( $site_type_transient ) {
// If site type is already set, do not show the modal.
return;
@@ -108,7 +109,7 @@ public function create_global_oneupdate_sites(): void {
return;
}
- $sites = get_option( 'oneupdate_shared_sites', array() );
+ $sites = get_option( Constants::ONEUPDATE_SHARED_SITES, array() );
if ( ! empty( $sites ) && is_array( $sites ) ) {
$oneupdate_sites = array();
@@ -157,7 +158,7 @@ public function add_body_class_for_missing_sites( $classes ): string {
}
// get oneupdate_shared_sites option.
- $shared_sites = get_option( 'oneupdate_shared_sites', array() );
+ $shared_sites = get_option( Constants::ONEUPDATE_SHARED_SITES, array() );
// if shared_sites is empty or not an array, return the classes.
if ( empty( $shared_sites ) || ! is_array( $shared_sites ) ) {
@@ -165,6 +166,7 @@ public function add_body_class_for_missing_sites( $classes ): string {
// remove plugin manager submenu.
remove_submenu_page( 'oneupdate', 'oneupdate' );
+ remove_submenu_page( 'oneupdate', 'oneupdate-pull-requests' );
return $classes;
}
diff --git a/inc/classes/class-plugin.php b/inc/classes/class-plugin.php
index 14af920..12a56b1 100644
--- a/inc/classes/class-plugin.php
+++ b/inc/classes/class-plugin.php
@@ -38,6 +38,7 @@ public function load_plugin_classes(): void {
Settings::get_instance();
REST::get_instance();
Cache::get_instance();
+ S3_Upload::get_instance();
}
/**
diff --git a/inc/classes/class-rest.php b/inc/classes/class-rest.php
index f07ef50..c398376 100644
--- a/inc/classes/class-rest.php
+++ b/inc/classes/class-rest.php
@@ -7,7 +7,7 @@
namespace OneUpdate;
-use OneUpdate\REST\{ Basic_Options, S3, Workflow };
+use OneUpdate\REST\{ Basic_Options, S3, Workflow, GitHub_Pull_Requests };
use OneUpdate\Traits\Singleton;
/**
@@ -36,6 +36,7 @@ public function setup_hooks(): void {
Basic_Options::get_instance();
Workflow::get_instance();
S3::get_instance();
+ GitHub_Pull_Requests::get_instance();
// fix cors headers for REST API requests.
add_filter( 'rest_pre_serve_request', array( $this, 'add_cors_headers' ), PHP_INT_MAX - 20, 4 );
diff --git a/inc/classes/class-s3-upload.php b/inc/classes/class-s3-upload.php
index c2c9518..eaac125 100644
--- a/inc/classes/class-s3-upload.php
+++ b/inc/classes/class-s3-upload.php
@@ -9,8 +9,7 @@
use OneUpdate\Traits\Singleton;
use Aws\Exception\AwsException;
-
-use OneUpdate\REST\S3;
+use OneUpdate\Plugin_Configs\Constants;
/**
* Class S3_Upload
@@ -34,7 +33,6 @@ protected function __construct() {
*/
public function setup_hooks(): void {
add_action( 'oneupdate_s3_zip_cleanup_event', array( $this, 'oneupdate_s3_zip_cleanup_event' ) );
- add_action( 'oneupdate_s3_zip_history_cleanup_event', array( $this, 'oneupdate_s3_zip_history_cleanup_event' ) );
}
/**
@@ -44,13 +42,13 @@ public function setup_hooks(): void {
*
* @throws \Exception If there is an error deleting files from S3.
*/
- // phpcs:disable -- its custom query to cleanup s3 bucket.
+ // phpcs:disable -- its custom query to cleanup s3 bucket & history table.
public function oneupdate_s3_zip_cleanup_event(): void {
- $s3_credentials = get_option( 'oneupdate_s3_credentials' );
+ $s3_credentials = get_option( Constants::ONEUPDATE_S3_CREDENTIALS, array() );
global $wpdb;
- $table_name = $wpdb->prefix . 'oneupdate_s3_zip_history';
- $s3 = S3::get_s3_instance();
+ $table_name = $wpdb->prefix . Constants::ONEUPDATE_S3_ZIP_HISTORY_TABLE;
+ $s3 = Utils::get_s3_instance();
$one_hour_ago = gmdate( 'Y-m-d H:i:s', current_time( 'timestamp', 1 ) - 3600 );
$expired_files = $wpdb->get_results(
@@ -84,49 +82,5 @@ public function oneupdate_s3_zip_cleanup_event(): void {
)
);
}
- // phpcs:enable.
-
- /**
- * Handle S3 zip history cleanup event.
- *
- * @return void
- *
- * @throws \Exception If there is an error deleting files from S3.
- */
- // phpcs:disable -- its custom query to cleanup s3 zip history.
- public function oneupdate_s3_zip_history_cleanup_event(): void {
- global $wpdb;
- $table_name = $wpdb->prefix . 'oneupdate_s3_zip_history';
- $one_week_ago = date( 'Y-m-d H:i:s', strtotime( '-1 week' ) );
- $batch_size = 1000;
- $sleep_seconds = 2;
-
- // Count total records to delete.
- $total_records = $wpdb->get_var(
- $wpdb->prepare( "SELECT COUNT(*) FROM $table_name WHERE upload_time <= %s", $one_week_ago )
- );
-
- if ( $total_records > 0 ) {
- $offset = 0;
- while ( $offset < $total_records ) {
- $query_template = sprintf(
- 'DELETE FROM `%s` WHERE upload_time <= %%s LIMIT %%d',
- esc_sql( $table_name )
- );
-
- $wpdb->query(
- $wpdb->prepare(
- $query_template,
- $one_week_ago,
- $batch_size
- )
- );
- $offset += $batch_size;
- if ( $offset < $total_records ) {
- sleep( $sleep_seconds );
- }
- }
- }
- }
- // phpcs:enable.
+ // phpcs:enable -- its custom query to cleanup s3 bucket & history table.
}
diff --git a/inc/classes/class-utils.php b/inc/classes/class-utils.php
new file mode 100644
index 0000000..bdee087
--- /dev/null
+++ b/inc/classes/class-utils.php
@@ -0,0 +1,137 @@
+setup_hooks();
+ }
+
+ /**
+ * Setup WordPress hooks
+ */
+ public function setup_hooks(): void {
+ }
+
+ /**
+ * Get current site type.
+ *
+ * @return string
+ */
+ public static function get_site_type(): string {
+ $site_type = get_option( Constants::ONEUPDATE_SITE_TYPE, '' );
+ return $site_type;
+ }
+
+ /**
+ * Is brand site.
+ *
+ * @return bool
+ */
+ public static function is_brand_site(): bool {
+ return 'brand-site' === self::get_site_type();
+ }
+
+ /**
+ * Is governing site.
+ *
+ * @return bool
+ */
+ public static function is_governing_site(): bool {
+ return 'governing-site' === self::get_site_type();
+ }
+
+ /**
+ * Get S3 instance.
+ *
+ * @return S3Client
+ */
+ public static function get_s3_instance(): S3Client {
+ $s3_credentials = get_option( Constants::ONEUPDATE_S3_CREDENTIALS, array() );
+ if ( empty( $s3_credentials ) || ! is_array( $s3_credentials ) ) {
+ return new S3Client( array() ); // Return an empty S3Client.
+ }
+ $s3 = new S3Client(
+ array(
+ 'version' => 'latest',
+ 'region' => $s3_credentials['region'] ?? '',
+ 'credentials' => array(
+ 'key' => $s3_credentials['accessKey'] ?? '',
+ 'secret' => $s3_credentials['secretKey'] ?? '',
+ ),
+ 'use_accelerate_endpoint' => true,
+ )
+ );
+
+ // first check if the bucket has getBucketAccelerateConfiguration.
+
+ try {
+ $accelerate_config = $s3->getBucketAccelerateConfiguration(
+ array(
+ 'Bucket' => $s3_credentials['bucketName'] ?? '',
+ )
+ );
+ if ( ! empty( $accelerate_config['Status'] ) && 'Enabled' === $accelerate_config['Status'] ) {
+ return $s3;
+ }
+ } catch ( AwsException $e ) {
+ $s3 = new S3Client(
+ array(
+ 'version' => 'latest',
+ 'region' => $s3_credentials['region'] ?? '',
+ 'endpoint' => $s3_credentials['endpoint'] ?? '',
+ 'credentials' => array(
+ 'key' => $s3_credentials['accessKey'] ?? '',
+ 'secret' => $s3_credentials['secretKey'] ?? '',
+ ),
+ 'use_path_style_endpoint' => true, // use path style endpoint.
+ )
+ );
+ }
+
+ return $s3;
+ }
+
+ /**
+ * Get GitHub token.
+ *
+ * @return string
+ */
+ public static function get_gh_token(): string {
+ return get_option( Constants::ONEUPDATE_GH_TOKEN, '' );
+ }
+
+ /**
+ * Add query args to a URL.
+ *
+ * @param string $url The base URL.
+ * @param array $args The query args to add.
+ *
+ * @return string The URL with the added query args.
+ */
+ public static function add_query_args( string $url, array $args ): string {
+ return add_query_arg( $args, $url );
+ }
+}
diff --git a/inc/classes/plugin-configs/class-constants.php b/inc/classes/plugin-configs/class-constants.php
new file mode 100644
index 0000000..2f03039
--- /dev/null
+++ b/inc/classes/plugin-configs/class-constants.php
@@ -0,0 +1,126 @@
+define_constants();
+ }
+
+ /**
+ * Define plugin constants
+ */
+ private function define_constants(): void {
+ // future constants can be defined here.
+ }
+}
diff --git a/inc/classes/plugin-configs/class-db.php b/inc/classes/plugin-configs/class-db.php
index 8cfd774..ca33fe3 100644
--- a/inc/classes/plugin-configs/class-db.php
+++ b/inc/classes/plugin-configs/class-db.php
@@ -32,7 +32,7 @@ protected function __construct() {
*/
public static function create_oneupdate_s3_zip_history_table(): void {
global $wpdb;
- $table_name = $wpdb->prefix . 'oneupdate_s3_zip_history';
+ $table_name = $wpdb->prefix . Constants::ONEUPDATE_S3_ZIP_HISTORY_TABLE;
$charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE IF NOT EXISTS $table_name (
diff --git a/inc/classes/plugin-configs/class-secret-key.php b/inc/classes/plugin-configs/class-secret-key.php
index 4ae2c50..672867e 100644
--- a/inc/classes/plugin-configs/class-secret-key.php
+++ b/inc/classes/plugin-configs/class-secret-key.php
@@ -38,11 +38,11 @@ public function setup_hooks(): void {
* @return void
*/
public function generate_secret_key(): void {
- $secret_key = get_option( 'oneupdate_child_site_api_key' );
+ $secret_key = get_option( Constants::ONEUPDATE_API_KEY );
if ( empty( $secret_key ) ) {
$secret_key = wp_generate_password( 128, false, false );
// Store the secret key in the database.
- update_option( 'oneupdate_child_site_api_key', $secret_key );
+ update_option( Constants::ONEUPDATE_API_KEY, $secret_key, false );
}
}
@@ -52,10 +52,10 @@ public function generate_secret_key(): void {
* @return \WP_REST_Response| \WP_Error
*/
public static function get_secret_key(): \WP_REST_Response|\WP_Error {
- $secret_key = get_option( 'oneupdate_child_site_api_key' );
+ $secret_key = get_option( Constants::ONEUPDATE_API_KEY );
if ( empty( $secret_key ) ) {
self::regenerate_secret_key();
- $secret_key = get_option( 'oneupdate_child_site_api_key' );
+ $secret_key = get_option( Constants::ONEUPDATE_API_KEY );
}
return rest_ensure_response(
array(
@@ -72,7 +72,7 @@ public static function get_secret_key(): \WP_REST_Response|\WP_Error {
public static function regenerate_secret_key(): \WP_REST_Response|\WP_Error {
$regenerated_key = wp_generate_password( 128, false, false );
// Update the option with the new key.
- update_option( 'oneupdate_child_site_api_key', $regenerated_key );
+ update_option( Constants::ONEUPDATE_API_KEY, $regenerated_key, false );
return rest_ensure_response(
array(
diff --git a/inc/classes/plugin-configs/class-vip-plugin-activation.php b/inc/classes/plugin-configs/class-vip-plugin-activation.php
index 8c9cfe8..c4e3ead 100644
--- a/inc/classes/plugin-configs/class-vip-plugin-activation.php
+++ b/inc/classes/plugin-configs/class-vip-plugin-activation.php
@@ -45,7 +45,7 @@ public function activate_vip_plugins(): void {
}
// get oneupdate_plugins_options option.
- $oneupdate_plugins_options = get_option( 'oneupdate_plugins_options', array() );
+ $oneupdate_plugins_options = get_option( Constants::ONEUPDATE_PLUGINS_OPTIONS, array() );
// activate all plugins that are in the oneupdate_plugins_options.
if ( ! empty( $oneupdate_plugins_options ) ) {
diff --git a/inc/classes/rest/class-basic-options.php b/inc/classes/rest/class-basic-options.php
index ab3f757..fc1a57f 100644
--- a/inc/classes/rest/class-basic-options.php
+++ b/inc/classes/rest/class-basic-options.php
@@ -7,6 +7,7 @@
namespace OneUpdate\REST;
+use OneUpdate\Plugin_Configs\Constants;
use OneUpdate\Traits\Singleton;
use OneUpdate\Plugin_Configs\Secret_Key;
use WP_REST_Server;
@@ -248,7 +249,7 @@ public function health_check(): \WP_REST_Response|\WP_Error {
*/
public function get_github_repos(): \WP_REST_Response|\WP_Error {
- $github_token = get_option( 'oneupdate_gh_token', '' );
+ $github_token = get_option( Constants::ONEUPDATE_GH_TOKEN, '' );
if ( empty( $github_token ) ) {
return new \WP_Error( 'no_github_token', __( 'GitHub token not found.', 'oneupdate' ), array( 'status' => 404 ) );
@@ -328,7 +329,7 @@ public function get_github_repos(): \WP_REST_Response|\WP_Error {
*/
public function get_site_type(): \WP_REST_Response|\WP_Error {
- $site_type = get_option( 'oneupdate_site_type', '' );
+ $site_type = get_option( Constants::ONEUPDATE_SITE_TYPE, '' );
return rest_ensure_response(
array(
@@ -348,10 +349,10 @@ public function set_site_type( \WP_REST_Request $request ): \WP_REST_Response|\W
$site_type = sanitize_text_field( $request->get_param( 'site_type' ) );
- update_option( 'oneupdate_site_type', $site_type );
+ update_option( Constants::ONEUPDATE_SITE_TYPE, $site_type, false );
// set transient to indicating that site type has been set for infinite time.
- set_transient( 'oneupdate_site_type_transient', true, 0 );
+ set_transient( Constants::ONEUPDATE_SITE_TYPE_TRANSIENT, true, 0 );
return rest_ensure_response(
array(
@@ -399,7 +400,7 @@ public function set_github_token( \WP_REST_Request $request ): \WP_REST_Response
);
}
- update_option( 'oneupdate_gh_token', $github_token );
+ update_option( Constants::ONEUPDATE_GH_TOKEN, $github_token, false );
return rest_ensure_response(
array(
@@ -415,7 +416,7 @@ public function set_github_token( \WP_REST_Request $request ): \WP_REST_Response
* @return \WP_REST_Response|\WP_Error
*/
public function get_s3_credentials(): \WP_REST_Response|\WP_Error {
- $s3_credentials = get_option( 'oneupdate_s3_credentials' );
+ $s3_credentials = get_option( Constants::ONEUPDATE_S3_CREDENTIALS, array() );
return rest_ensure_response(
array(
@@ -450,7 +451,7 @@ public function set_s3_credentials( \WP_REST_Request $request ): \WP_REST_Respon
}
// Update S3 credentials in options.
- update_option( 'oneupdate_s3_credentials', $s3_credentials );
+ update_option( Constants::ONEUPDATE_S3_CREDENTIALS, $s3_credentials, false );
return rest_ensure_response(
array(
@@ -466,7 +467,7 @@ public function set_s3_credentials( \WP_REST_Request $request ): \WP_REST_Respon
* @return \WP_REST_Response|\WP_Error
*/
public function get_shared_sites(): \WP_REST_Response|\WP_Error {
- $shared_sites = get_option( 'oneupdate_shared_sites', array() );
+ $shared_sites = get_option( Constants::ONEUPDATE_SHARED_SITES, array() );
return rest_ensure_response(
array(
'success' => true,
@@ -506,7 +507,7 @@ public function set_shared_sites( \WP_REST_Request $request ): \WP_REST_Response
$gtihub_repos[] = $site['githubRepo'] ?? '';
}
- update_option( 'oneupdate_shared_sites', $sites_data );
+ update_option( Constants::ONEUPDATE_SHARED_SITES, $sites_data, false );
return rest_ensure_response(
array(
@@ -522,7 +523,7 @@ public function set_shared_sites( \WP_REST_Request $request ): \WP_REST_Response
* @return \WP_REST_Response|\WP_Error
*/
public function get_github_token(): \WP_REST_Response|\WP_Error {
- $github_token = get_option( 'oneupdate_gh_token', '' );
+ $github_token = get_option( Constants::ONEUPDATE_GH_TOKEN, '' );
return rest_ensure_response(
array(
diff --git a/inc/classes/rest/class-github-pull-requests.php b/inc/classes/rest/class-github-pull-requests.php
new file mode 100644
index 0000000..fbf78d1
--- /dev/null
+++ b/inc/classes/rest/class-github-pull-requests.php
@@ -0,0 +1,601 @@
+setup_hooks();
+ }
+
+ /**
+ * Setup WordPress hooks
+ *
+ * @return void
+ */
+ public function setup_hooks(): void {
+ add_action( 'rest_api_init', array( $this, 'register_routes' ), 99 );
+ }
+
+ /**
+ * Register REST API routes.
+ *
+ * @return void
+ */
+ public function register_routes(): void {
+ /**
+ * Register a route to get pull requests by pagination.
+ */
+ register_rest_route(
+ self::NAMESPACE,
+ '/pull-requests/(?P[a-zA-Z0-9._-]+)/(?P[a-zA-Z0-9._-]+)',
+ array(
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_pull_requests' ),
+ 'permission_callback' => array( self::class, 'permission_callback' ),
+ 'args' => array(
+ 'owner' => array(
+ 'required' => true,
+ 'type' => 'string',
+ ),
+ 'repo' => array(
+ 'required' => true,
+ 'type' => 'string',
+ ),
+ 'pr_number' => array(
+ 'required' => false,
+ 'type' => 'integer',
+ ),
+ 'state' => array(
+ 'required' => false,
+ 'type' => 'string',
+ 'default' => 'all',
+ 'enum' => array( 'open', 'closed', 'all', 'merged' ),
+ ),
+ 'page' => array(
+ 'required' => false,
+ 'type' => 'integer',
+ 'default' => 1,
+ ),
+ 'per_page' => array(
+ 'required' => false,
+ 'type' => 'integer',
+ 'default' => 25,
+ 'maximum' => 100,
+ 'minimum' => 1,
+ ),
+ 'search_query' => array(
+ 'required' => false,
+ 'type' => 'string',
+ ),
+ ),
+ ),
+ )
+ );
+ }
+
+ /**
+ * Permission callback for the routes.
+ *
+ * @return bool
+ */
+ public static function permission_callback(): bool {
+ return current_user_can( 'manage_options' );
+ }
+
+ /**
+ * Get pull requests by pagination.
+ *
+ * @param \WP_REST_Request $request The REST request.
+ *
+ * @return \WP_REST_Response
+ */
+ public function get_pull_requests( \WP_REST_Request $request ): \WP_REST_Response {
+ $gh_owner = sanitize_text_field( $request['owner'] );
+ $gh_repo = sanitize_text_field( $request['repo'] );
+ $pr_number = filter_var( $request->get_param( 'pr_number' ), FILTER_VALIDATE_INT ) ?? 0;
+ $page = filter_var( $request->get_param( 'page' ), FILTER_VALIDATE_INT ) ?? 1;
+ $pr_state = sanitize_text_field( $request->get_param( 'state' ) ) ?? 'all';
+ $per_page = filter_var( $request->get_param( 'per_page' ), FILTER_VALIDATE_INT ) ?? 25;
+ $search_query = sanitize_text_field( $request->get_param( 'search_query' ) ) ?? '';
+
+ // if pr_number & search query is not provided, get all pull requests.
+ if ( empty( $pr_number ) && empty( $search_query ) ) {
+ return self::get_all_pull_requests( $gh_owner, $gh_repo, $pr_state, $per_page, $page );
+ }
+
+ // if pr_number is not provided but search_query is provided, search pull requests.
+ if ( ! empty( $search_query ) ) {
+ return self::search_pull_requests( $gh_owner, $gh_repo, $search_query, $per_page, $page, $pr_state );
+ }
+
+ // if pr_number is provided, get specific pull request.
+ return self::get_specific_pull_request( $gh_owner, $gh_repo, $pr_number );
+ }
+
+ /**
+ * Get all pull requests for a given repo.
+ *
+ * @param string $gh_owner GitHub owner.
+ * @param string $gh_repo GitHub repo.
+ * @param string $pr_state State of pull requests to fetch. Default is 'open'.
+ * @param int $per_page Number of pull requests per page. Default is 25.
+ * @param int $page Page number. Default is 1.
+ *
+ * @return \WP_REST_Response
+ */
+ private static function get_all_pull_requests( string $gh_owner, string $gh_repo, string $pr_state = 'open', int $per_page = 25, int $page = 1 ): \WP_REST_Response {
+
+ // gh api endpoint to get pull requests.
+ $gh_api_endpoint = self::GH_API_BASE_URL . "/repos/{$gh_owner}/{$gh_repo}/pulls";
+ $query_args = array(
+ 'state' => $pr_state,
+ 'per_page' => $per_page,
+ 'page' => $page,
+ 'order' => 'desc',
+ );
+
+ $gh_api_endpoint = Utils::add_query_args( $gh_api_endpoint, $query_args );
+
+ $response = self::gh_api_request_with_validation( $gh_api_endpoint );
+
+ if ( false === $response['success'] ) {
+ return new \WP_REST_Response(
+ array(
+ 'success' => false,
+ 'message' => $response['message'],
+ ),
+ $response['status_code']
+ );
+ }
+
+ $pull_requests = $response['data'] ?? array();
+ $headers = $response['headers'] ?? array();
+
+ $pull_requests = self::format_github_pull_requests_info( $pull_requests );
+ $total_count = self::get_total_count_from_headers( $headers, count( $pull_requests ), $per_page );
+ $total_pages = ceil( $total_count / $per_page );
+
+ $pull_requests_response = new \WP_REST_Response(
+ array(
+ 'success' => true,
+ 'pull_requests' => $pull_requests,
+ 'pagination' => array(
+ 'current_page' => $page,
+ 'per_page' => $per_page,
+ 'total_pages' => $total_pages,
+ 'total_count' => $total_count,
+ ),
+ 'api' => $gh_api_endpoint,
+ ),
+ 200
+ );
+
+ $pull_requests_response->header( 'X-WP-Total', $total_count );
+ $pull_requests_response->header( 'X-WP-TotalPages', $total_pages );
+
+ return $pull_requests_response;
+ }
+
+ /**
+ * Search pull requests in a given repo.
+ *
+ * @param string $gh_owner GitHub owner.
+ * @param string $gh_repo GitHub repo.
+ * @param string $search_query Search query.
+ * @param int $per_page Number of pull requests per page. Default is 25.
+ * @param int $page Page number. Default is 1.
+ * @param string $pr_state State of pull requests to fetch. Default is 'all'.
+ *
+ * @return \WP_REST_Response
+ */
+ private static function search_pull_requests( string $gh_owner, string $gh_repo, string $search_query, int $per_page = 25, int $page = 1, string $pr_state = 'all' ): \WP_REST_Response {
+
+ // If we have a specific search query, use search API with state filter.
+ if ( ! empty( $search_query ) && 'all' !== $pr_state ) {
+ return self::search_pull_requests_with_query_and_state( $gh_owner, $gh_repo, $search_query, $per_page, $page, $pr_state );
+ }
+
+ $query_args = array();
+
+ // If no search query or state is 'all', use the original search approach.
+ if ( ! empty( $search_query ) ) {
+ $gh_api_endpoint = self::GH_API_BASE_URL . '/search/issues';
+ $query_args = array_merge(
+ $query_args,
+ array(
+ 'q' => $search_query . "+repo:{$gh_owner}/{$gh_repo}+type:pr",
+ ),
+ );
+
+ // Add state to search query if not 'all'.
+ if ( 'all' !== $pr_state ) {
+ $gh_api_endpoint .= "+state:{$pr_state}";
+ }
+ } else {
+ // Use pulls API for better state filtering when no search query.
+ $gh_api_endpoint = self::GH_API_BASE_URL . "/repos/{$gh_owner}/{$gh_repo}/pulls";
+ $query_args = array_merge(
+ $query_args,
+ array(
+ 'state' => $pr_state,
+ )
+ );
+ }
+
+ $query_args = array_merge(
+ $query_args,
+ array(
+ 'per_page' => $per_page,
+ 'page' => $page,
+ 'order' => 'desc',
+ ),
+ );
+ $gh_api_endpoint = Utils::add_query_args( $gh_api_endpoint, $query_args );
+
+ $response = self::gh_api_request_with_validation( $gh_api_endpoint );
+
+ if ( false === $response['success'] ) {
+ return new \WP_REST_Response(
+ array(
+ 'success' => false,
+ 'message' => $response['message'],
+ ),
+ $response['status_code']
+ );
+ }
+
+ $results = $response['data'] ?? array();
+ $headers = $response['headers'] ?? array();
+
+ // Handle different response formats.
+ if ( ! empty( $search_query ) ) {
+ // Search API response.
+ $pull_requests = isset( $results['items'] ) ? self::format_github_pull_requests_info( $results['items'] ) : array();
+ $total_count = $results['total_count'] ?? 0;
+ } else {
+ // Pulls API response.
+ $pull_requests = self::format_github_pull_requests_info( $results );
+ $total_count = self::get_total_count_from_headers( $headers, count( $pull_requests ), $per_page );
+ }
+
+ $total_pages = ceil( $total_count / $per_page );
+
+ $response_data = new \WP_REST_Response(
+ array(
+ 'success' => true,
+ 'pull_requests' => $pull_requests,
+ 'pagination' => array(
+ 'current_page' => $page,
+ 'per_page' => $per_page,
+ 'total_pages' => $total_pages,
+ 'total_count' => $total_count,
+ ),
+ ),
+ 200
+ );
+
+ $response_data->header( 'X-WP-Total', $total_count );
+ $response_data->header( 'X-WP-TotalPages', $total_pages );
+
+ return $response_data;
+ }
+
+ /**
+ * Handle search with both query and state filters using multiple API calls if needed
+ *
+ * @param string $gh_owner GitHub owner.
+ * @param string $gh_repo GitHub repo.
+ * @param string $search_query Search query.
+ * @param int $per_page Number of pull requests per page.
+ * @param int $page Page number.
+ * @param string $pr_state State of pull requests to fetch.
+ *
+ * @return \WP_REST_Response
+ */
+ private static function search_pull_requests_with_query_and_state( string $gh_owner, string $gh_repo, string $search_query, int $per_page, int $page, string $pr_state ): \WP_REST_Response {
+
+ // Use search API with state filter in query.
+ $gh_api_endpoint = self::GH_API_BASE_URL . '/search/issues';
+ $query_args = array(
+ 'q' => $search_query . "+repo:{$gh_owner}/{$gh_repo}+type:pr+state:{$pr_state}",
+ 'per_page' => $per_page,
+ 'page' => $page,
+ 'order' => 'desc',
+ );
+ $gh_api_endpoint = Utils::add_query_args( $gh_api_endpoint, $query_args );
+
+ $response = self::gh_api_request_with_validation( $gh_api_endpoint );
+
+ if ( false === $response['success'] ) {
+ return new \WP_REST_Response(
+ array(
+ 'success' => false,
+ 'message' => $response['message'],
+ ),
+ $response['status_code']
+ );
+ }
+
+ $search_results = $response['data'] ?? array();
+ $pull_requests = isset( $search_results['items'] ) ? self::format_github_pull_requests_info( $search_results['items'] ) : array();
+ $total_count = $search_results['total_count'] ?? 0;
+ $total_pages = ceil( $total_count / $per_page );
+
+ $response_data = new \WP_REST_Response(
+ array(
+ 'success' => true,
+ 'pull_requests' => $pull_requests,
+ 'pagination' => array(
+ 'current_page' => $page,
+ 'per_page' => $per_page,
+ 'total_pages' => $total_pages,
+ 'total_count' => $total_count,
+ ),
+ ),
+ 200
+ );
+
+ $response_data->header( 'X-WP-Total', $total_count );
+ $response_data->header( 'X-WP-TotalPages', $total_pages );
+
+ return $response_data;
+ }
+
+ /**
+ * Extract total count from Link headers when using pulls API
+ *
+ * @param array|\WpOrg\Requests\Utility\CaseInsensitiveDictionary $headers Response headers.
+ * @param int $current_count Current count of items fetched.
+ * @param int $per_page Number of items per page. Default is 25.
+ *
+ * @return int Total count of items.
+ */
+ private static function get_total_count_from_headers( array|\WpOrg\Requests\Utility\CaseInsensitiveDictionary $headers, int $current_count, int $per_page = 25 ): int {
+
+ // if headers is instance of CaseInsensitiveDictionary, convert to array.
+ if ( $headers instanceof \WpOrg\Requests\Utility\CaseInsensitiveDictionary ) {
+ $headers = $headers->getAll();
+ }
+
+ if ( ! isset( $headers['link'] ) ) {
+ return $current_count;
+ }
+
+ $link_header = $headers['link'] ?? '';
+
+ // Parse the Link header to get last page.
+ if ( preg_match( '/<[^>]*\/pulls\?[^>]*page=(\d+)[^>]*>;\s*rel=["\']last["\']/i', $link_header, $matches ) ) {
+ $last_page = (int) $matches[1];
+ // This is an approximation based on link header.
+ return $last_page * $per_page;
+ }
+ return $current_count;
+ }
+
+ /**
+ * Get a specific pull request by its number.
+ *
+ * @param string $gh_owner GitHub owner.
+ * @param string $gh_repo GitHub repo.
+ * @param int $pr_number Pull request number.
+ *
+ * @return \WP_REST_Response
+ */
+ private static function get_specific_pull_request( string $gh_owner, string $gh_repo, int $pr_number ): \WP_REST_Response {
+
+ // gh api endpoint to get a specific pull request.
+ $gh_api_endpoint = self::GH_API_BASE_URL . "/repos/{$gh_owner}/{$gh_repo}/pulls/{$pr_number}";
+
+ $response = self::gh_api_request_with_validation( $gh_api_endpoint );
+
+ if ( false === $response['success'] ) {
+ return new \WP_REST_Response(
+ array(
+ 'success' => false,
+ 'message' => $response['message'],
+ ),
+ $response['status_code']
+ );
+ }
+
+ $pull_request = $response['data'] ?? array();
+
+ $pull_request = self::format_github_pull_requests_info( array( $pull_request ) );
+
+ return new \WP_REST_Response(
+ array(
+ 'success' => true,
+ 'pull_request' => $pull_request,
+ ),
+ 200
+ );
+ }
+
+ /**
+ * Format GitHub pull requests info to return only necessary fields.
+ *
+ * @param array $pull_requests Array of pull requests from GitHub API.
+ *
+ * @return array Formatted array of pull requests.
+ */
+ private static function format_github_pull_requests_info( array $pull_requests ): array {
+ $formatted_prs = array();
+ foreach ( $pull_requests as $pr ) {
+ $formatted_prs[] = array(
+ 'id' => $pr['id'] ?? '',
+ 'url' => $pr['url'] ?? '',
+ 'number' => $pr['number'] ?? '',
+ 'title' => $pr['title'] ?? '',
+ 'user' => isset( $pr['user'] ) ? array(
+ 'login' => $pr['user']['login'] ?? '',
+ 'avatar_url' => $pr['user']['avatar_url'] ?? '',
+ 'html_url' => $pr['user']['html_url'] ?? '',
+ ) : null,
+ 'labels' => $pr['labels'] ?? '',
+ 'state' => $pr['state'] ?? '',
+ 'created_at' => $pr['created_at'] ?? '',
+ 'updated_at' => $pr['updated_at'] ?? '',
+ 'closed_at' => $pr['closed_at'] ?? '',
+ 'html_url' => $pr['html_url'] ?? '',
+ 'body' => $pr['body'] ?? '',
+ 'pr_branch' => isset( $pr['head'] ) ? ( $pr['head']['ref'] ?? '' ) : '',
+ 'base_branch' => isset( $pr['base'] ) ? ( $pr['base']['ref'] ?? '' ) : '',
+ 'merged_at' => $pr['merged_at'] ?? null,
+ 'merged' => $pr['merged'] ?? null,
+ 'merged_by' => isset( $pr['merged_by'] ) ? array(
+ 'login' => $pr['merged_by']['login'] ?? '',
+ 'avatar_url' => $pr['merged_by']['avatar_url'] ?? '',
+ 'html_url' => $pr['merged_by']['html_url'] ?? '',
+ ) : null,
+
+ 'comments' => $pr['comments'] ?? null,
+ 'commits' => $pr['commits'] ?? null,
+ 'additions' => $pr['additions'] ?? null,
+ 'deletions' => $pr['deletions'] ?? null,
+ 'changed_files' => $pr['changed_files'] ?? null,
+ 'rebaseable' => $pr['rebaseable'] ?? null,
+ 'draft' => $pr['draft'] ?? null,
+ 'auto_merge' => $pr['auto_merge'] ?? null,
+
+ );
+ }
+ return $formatted_prs;
+ }
+
+ /**
+ * Make a GitHub API request with validation.
+ *
+ * @param string $endpoint GitHub API endpoint.
+ *
+ * @return array Array containing success status, data, headers, status_code, and message.
+ */
+ private static function gh_api_request_with_validation( string $endpoint ): array {
+ $response = self::gh_api_request( $endpoint );
+
+ // Check for WP_Error.
+ if ( is_wp_error( $response ) ) {
+ return array(
+ 'success' => false,
+ 'status_code' => 500,
+ 'message' => $response->get_error_message(),
+ );
+ }
+
+ // Get response details.
+ $status_code = wp_remote_retrieve_response_code( $response );
+ $body = wp_remote_retrieve_body( $response );
+ $headers = wp_remote_retrieve_headers( $response );
+
+ // Check for non-200 status codes.
+ if ( 200 !== $status_code ) {
+ return array(
+ 'success' => false,
+ 'status_code' => $status_code,
+ 'message' => sprintf(
+ /* translation: %s github response code */
+ 'GitHub API returned status code %d.',
+ $status_code,
+ ),
+ 'body' => $body,
+ 'headers' => $headers,
+ );
+ }
+
+ // Decode JSON response.
+ $data = json_decode( $body, true );
+
+ // Check for JSON decode errors.
+ if ( json_last_error() !== JSON_ERROR_NONE ) {
+ return array(
+ 'success' => false,
+ 'status_code' => 500,
+ 'message' => __( 'Failed to parse GitHub API response as JSON.', 'oneupdate' ),
+ 'body' => $body,
+ );
+ }
+
+ return array(
+ 'success' => true,
+ 'status_code' => $status_code,
+ 'data' => $data,
+ 'headers' => $headers,
+ 'body' => $body,
+ );
+ }
+
+ /**
+ * Make a GitHub API request.
+ *
+ * @param string $endpoint GitHub API endpoint.
+ *
+ * @return array|\WP_Error|\WP_REST_Response
+ */
+ private static function gh_api_request( string $endpoint ): array|\WP_Error|\WP_REST_Response {
+ $gh_token = Utils::get_gh_token();
+
+ if ( empty( $gh_token ) ) {
+ return new \WP_REST_Response(
+ array(
+ 'success' => false,
+ 'message' => __( 'GitHub token not configured.', 'oneupdate' ),
+ ),
+ 401
+ );
+ }
+
+ $response = wp_safe_remote_get(
+ $endpoint,
+ array(
+ 'headers' => array(
+ 'Authorization' => "Bearer {$gh_token}",
+ 'Accept' => 'application/vnd.github.v3+json',
+ 'User-Agent' => __( 'OneUpdate Plugin Loader', 'oneupdate' ),
+ 'Content-Type' => 'application/json',
+ 'Accept-Encoding' => 'identity',
+ ),
+ 'httpversion' => '1.1',
+ 'timeout' => 15, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout -- this is to avoid timeout issues.
+ ),
+ );
+
+ return $response;
+ }
+}
diff --git a/inc/classes/rest/class-s3.php b/inc/classes/rest/class-s3.php
index d9efd64..537fdea 100644
--- a/inc/classes/rest/class-s3.php
+++ b/inc/classes/rest/class-s3.php
@@ -9,8 +9,9 @@
use OneUpdate\Traits\Singleton;
use WP_REST_Server;
-use Aws\S3\S3Client;
use Aws\Exception\AwsException;
+use OneUpdate\Plugin_Configs\Constants;
+use OneUpdate\Utils;
/**
* Class S3
@@ -100,11 +101,11 @@ public function register_routes(): void {
* @return \WP_REST_Response| \WP_Error
*/
public function s3_health_check(): \WP_REST_Response|\WP_Error {
- $s3_credentials = get_option( 'oneupdate_s3_credentials' );
+ $s3_credentials = get_option( Constants::ONEUPDATE_S3_CREDENTIALS, array() );
if ( empty( $s3_credentials ) || ! is_array( $s3_credentials ) ) {
return new \WP_REST_Response( array( 'message' => 'S3 credentials not set' ), 400 );
}
- $s3 = self::get_s3_instance();
+ $s3 = Utils::get_s3_instance();
try {
// Attempt to list buckets to check connectivity.
$result = $s3->listBuckets();
@@ -149,8 +150,8 @@ public function handle_s3_upload( \WP_REST_Request $request ): \WP_REST_Response
if ( pathinfo( $file['name'], PATHINFO_EXTENSION ) !== 'zip' ) {
return new \WP_REST_Response( array( 'message' => 'Only ZIP files are allowed' ), 400 );
}
- $s3_credentials = get_option( 'oneupdate_s3_credentials' );
- $s3 = self::get_s3_instance();
+ $s3_credentials = get_option( Constants::ONEUPDATE_S3_CREDENTIALS, array() );
+ $s3 = Utils::get_s3_instance();
$s3_key = 'Uploads/' . uniqid() . '_' . basename( $file['name'] );
try {
@@ -175,7 +176,7 @@ public function handle_s3_upload( \WP_REST_Request $request ): \WP_REST_Response
)->getUri()->__toString();
global $wpdb;
- $table_name = $wpdb->prefix . 'oneupdate_s3_zip_history';
+ $table_name = $wpdb->prefix . Constants::ONEUPDATE_S3_ZIP_HISTORY_TABLE;
$insert_result = $wpdb->insert( // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- inserting private plugin data.
$table_name,
array(
@@ -210,61 +211,10 @@ public function handle_s3_upload( \WP_REST_Request $request ): \WP_REST_Response
*/
public function get_s3_upload_history(): \WP_REST_Response|\WP_Error {
global $wpdb;
- $table_name = $wpdb->prefix . 'oneupdate_s3_zip_history';
+ $table_name = $wpdb->prefix . Constants::ONEUPDATE_S3_ZIP_HISTORY_TABLE;
$query = "SELECT * FROM $table_name ORDER BY upload_time DESC";
$results = $wpdb->get_results( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching -- Static query with no variables, caching not suitable for dynamic upload history.
return new \WP_REST_Response( $results ? $results : array(), 200 );
}
-
- /**
- * Get S3 instance.
- *
- * @return S3Client
- */
- public static function get_s3_instance(): S3Client {
- $s3_credentials = get_option( 'oneupdate_s3_credentials' );
- if ( empty( $s3_credentials ) || ! is_array( $s3_credentials ) ) {
- return new S3Client( array() ); // Return an empty S3Client.
- }
- $s3 = new S3Client(
- array(
- 'version' => 'latest',
- 'region' => $s3_credentials['region'] ?? '',
- 'credentials' => array(
- 'key' => $s3_credentials['accessKey'] ?? '',
- 'secret' => $s3_credentials['secretKey'] ?? '',
- ),
- 'use_accelerate_endpoint' => true,
- )
- );
-
- // first check if the bucket has getBucketAccelerateConfiguration.
-
- try {
- $accelerate_config = $s3->getBucketAccelerateConfiguration(
- array(
- 'Bucket' => $s3_credentials['bucketName'] ?? '',
- )
- );
- if ( ! empty( $accelerate_config['Status'] ) && 'Enabled' === $accelerate_config['Status'] ) {
- return $s3;
- }
- } catch ( AwsException $e ) {
- $s3 = new S3Client(
- array(
- 'version' => 'latest',
- 'region' => $s3_credentials['region'] ?? '',
- 'endpoint' => $s3_credentials['endpoint'] ?? '',
- 'credentials' => array(
- 'key' => $s3_credentials['accessKey'] ?? '',
- 'secret' => $s3_credentials['secretKey'] ?? '',
- ),
- 'use_path_style_endpoint' => true, // use path style endpoint.
- )
- );
- }
-
- return $s3;
- }
}
diff --git a/inc/classes/rest/class-workflow.php b/inc/classes/rest/class-workflow.php
index 6f3b5f1..56a43a6 100644
--- a/inc/classes/rest/class-workflow.php
+++ b/inc/classes/rest/class-workflow.php
@@ -9,6 +9,7 @@
use OneUpdate\Traits\Singleton;
use OneUpdate\Cache;
+use OneUpdate\Plugin_Configs\Constants;
/**
* Class Workflow
@@ -257,7 +258,7 @@ public function register_rest_routes(): void {
*/
public function webhook_permission_callback(): bool {
$secret = isset( $_GET['secret'] ) ? sanitize_text_field( wp_unslash( $_GET['secret'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no need for nonce as its called from webhook like vip or github.
- $valid_secret = get_option( 'oneupdate_child_site_api_key', 'default_api_key' );
+ $valid_secret = get_option( Constants::ONEUPDATE_API_KEY, '' );
return hash_equals( $secret, $valid_secret );
}
@@ -636,7 +637,7 @@ public function apply_private_plugins_to_selected_sites( \WP_REST_Request $reque
* @return array|\WP_Error
*/
private function trigger_github_action_for_private_plugin( string $repo, string $private_plugin, string $branch, string $site_name ): array|\WP_Error {
- $github_token = get_option( 'oneupdate_gh_token', '' );
+ $github_token = get_option( Constants::ONEUPDATE_GH_TOKEN, '' );
if ( empty( $github_token ) ) {
return new \WP_Error( 'no_github_token', __( 'GitHub token not found.', 'oneupdate' ), array( 'status' => 404 ) );
@@ -714,7 +715,7 @@ private function trigger_github_action_for_private_plugin( string $repo, string
* @return WP_REST_Response|\WP_Error
*/
public function get_oneupdate_plugins_options(): \WP_REST_Response|\WP_Error {
- $options = get_option( 'oneupdate_plugins_options', array() );
+ $options = get_option( Constants::ONEUPDATE_PLUGINS_OPTIONS, array() );
return rest_ensure_response(
array(
@@ -745,12 +746,12 @@ public function update_oneupdate_plugins_options( \WP_REST_Request $request ): \
$plugin_type = $request_options['plugin_type'] ?? 'add_update';
// oneupdate_plugin_activate options.
- $oneupdate_plugin_activate = get_option( 'oneupdate_plugins_options', array() );
+ $oneupdate_plugin_activate = get_option( Constants::ONEUPDATE_PLUGINS_OPTIONS, array() );
// if plugin type is deactivate/remove then remove the plugin from options.
if ( 'deactivate' === $plugin_type || 'remove' === $plugin_type ) {
// get active plugins options.
- $active_plugins = get_option( 'active_plugins', array() );
+ $active_plugins = get_option( Constants::ONEUPDATE_ACTIVE_PLUGINS, array() );
// remove the plugins from active plugins options.
foreach ( $plugins as $plugin ) {
if ( in_array( $plugin, $active_plugins, true ) ) {
@@ -762,12 +763,12 @@ public function update_oneupdate_plugins_options( \WP_REST_Request $request ): \
}
}
// update the active plugins options.
- update_option( 'active_plugins', $active_plugins );
+ update_option( Constants::ONEUPDATE_ACTIVE_PLUGINS, $active_plugins, false );
}
if ( 'activate' === $plugin_type ) {
// if plugin type is activate then activate the plugins.
- $active_plugins = get_option( 'active_plugins', array() );
+ $active_plugins = get_option( Constants::ONEUPDATE_ACTIVE_PLUGINS, array() );
foreach ( $plugins as $plugin ) {
if ( ! in_array( $plugin, $active_plugins, true ) ) {
activate_plugin( $plugin, '', false, true );
@@ -779,7 +780,7 @@ public function update_oneupdate_plugins_options( \WP_REST_Request $request ): \
}
}
- update_option( 'oneupdate_plugins_options', $oneupdate_plugin_activate );
+ update_option( Constants::ONEUPDATE_PLUGINS_OPTIONS, $oneupdate_plugin_activate, false );
if ( ! empty( $plugins ) ) {
Cache::rebuild_transient_for_single_plugin(
@@ -863,7 +864,7 @@ public function apply_plugins_to_selected_sites( \WP_REST_Request $request ): \W
// if current site is same as site_url then use current site token.
if ( empty( $token ) ) {
- $token = get_option( 'oneupdate_child_site_api_key', 'default_api_key' );
+ $token = get_option( Constants::ONEUPDATE_API_KEY, '' );
}
// create comma separated string array of plugins.
@@ -922,7 +923,7 @@ public function apply_plugins_to_selected_sites( \WP_REST_Request $request ): \W
* @return array|\WP_Error
*/
private function trigger_github_action_for_pr_creation( string $repo, string $branch, string $plugin_slug, string $version, string $plugin_type, string $site_name ): array|\WP_Error {
- $github_token = get_option( 'oneupdate_gh_token' );
+ $github_token = get_option( Constants::ONEUPDATE_GH_TOKEN, '' );
if ( empty( $github_token ) ) {
return new \WP_Error( 'no_github_token', __( 'GitHub token not found.', 'oneupdate' ), array( 'status' => 404 ) );
@@ -1023,7 +1024,7 @@ private function trigger_github_action_for_pr_creation( string $repo, string $br
* @return string|null The latest workflow run ID or null if not found.
*/
private function get_latest_workflow_run_id( string $repo, string $workflow_filename ): string|null {
- $github_token = get_option( 'oneupdate_gh_token' );
+ $github_token = get_option( Constants::ONEUPDATE_GH_TOKEN, '' );
if ( empty( $github_token ) ) {
return null;
diff --git a/inc/classes/settings/class-shared-sites.php b/inc/classes/settings/class-shared-sites.php
index 1635147..f1f61b9 100644
--- a/inc/classes/settings/class-shared-sites.php
+++ b/inc/classes/settings/class-shared-sites.php
@@ -8,6 +8,7 @@
namespace OneUpdate\Settings;
use OneUpdate\Traits\Singleton;
+use OneUpdate\Utils;
/**
* Class Shared_Sites
@@ -60,6 +61,18 @@ public function add_admin_menu(): void {
array( $this, 'render_oneupdate_plugin_manager' )
);
+ // if governing site then add pull requests menu.
+ if ( Utils::is_governing_site() ) {
+ add_submenu_page(
+ 'oneupdate',
+ __( 'Pull Requests', 'oneupdate' ),
+ __( 'Pull Requests', 'oneupdate' ),
+ 'manage_options',
+ 'oneupdate-pull-requests',
+ array( $this, 'render_oneupdate_pull_requests_page' )
+ );
+ }
+
// Add your other submenu page.
add_submenu_page(
'oneupdate',
@@ -71,7 +84,7 @@ public function add_admin_menu(): void {
);
// if site type is brand then remove the governing site menu.
- if ( 'governing-site' !== get_option( 'oneupdate_site_type', '' ) ) {
+ if ( ! Utils::is_governing_site() ) {
remove_submenu_page( 'oneupdate', 'oneupdate' );
}
}
@@ -83,7 +96,7 @@ public function add_admin_menu(): void {
*/
public function render_oneupdate_plugin_manager(): void {
// Check if the user has permission to manage options.
- if ( ! current_user_can( 'manage_options' ) || 'governing-site' !== get_option( 'oneupdate_site_type', '' ) ) {
+ if ( ! current_user_can( 'manage_options' ) || ! Utils::is_governing_site() ) {
wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'oneupdate' ) );
}
?>
@@ -112,4 +125,22 @@ public function render_oneupdate_settings_page(): void {
+
+ \n"
"Language-Team: LANGUAGE \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"POT-Creation-Date: 2025-08-29T11:41:39+00:00\n"
+"POT-Creation-Date: 2025-09-19T10:19:07+00:00\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"X-Generator: WP-CLI 2.12.0\n"
"X-Domain: oneupdate\n"
#. Plugin Name of the plugin
#: oneupdate.php
-#: inc/classes/settings/class-shared-sites.php:45
-#: oneupdate.php:44
+#: inc/classes/settings/class-shared-sites.php:46
+#: oneupdate.php:47
#: assets/build/js/plugin.js:1
#: assets/src/admin/plugin/index.js:111
msgid "OneUpdate"
msgstr ""
+#. Plugin URI of the plugin
+#: oneupdate.php
+msgid "https://github.com/rtCamp/OneUpdate/"
+msgstr ""
+
#. Description of the plugin
#: oneupdate.php
msgid "OneUpdate - Enterprise WordPress Plugin Manager Automate plugin updates across multiple WordPress sites with CI/CD integration. Creates pull requests for seamless development-to-production workflows."
@@ -42,15 +47,15 @@ msgstr ""
msgid "No plugins found."
msgstr ""
-#: inc/classes/class-hooks.php:139
-#: inc/classes/settings/class-shared-sites.php:66
-#: inc/classes/settings/class-shared-sites.php:67
-#: inc/classes/settings/class-shared-sites.php:110
+#: inc/classes/class-hooks.php:140
+#: inc/classes/settings/class-shared-sites.php:79
+#: inc/classes/settings/class-shared-sites.php:80
+#: inc/classes/settings/class-shared-sites.php:123
msgid "Settings"
msgstr ""
#. translators: %s is the error message from AWS S3
-#: inc/classes/class-s3-upload.php:73
+#: inc/classes/class-s3-upload.php:71
#, php-format
msgid "Error deleting file from S3: %s"
msgstr ""
@@ -59,157 +64,179 @@ msgstr ""
msgid "Secret key regenerated successfully."
msgstr ""
-#: inc/classes/rest/class-basic-options.php:239
+#: inc/classes/rest/class-basic-options.php:240
msgid "Health check passed successfully."
msgstr ""
-#: inc/classes/rest/class-basic-options.php:254
-#: inc/classes/rest/class-workflow.php:642
-#: inc/classes/rest/class-workflow.php:928
+#: inc/classes/rest/class-basic-options.php:255
+#: inc/classes/rest/class-workflow.php:643
+#: inc/classes/rest/class-workflow.php:929
msgid "GitHub token not found."
msgstr ""
-#: inc/classes/rest/class-basic-options.php:280
+#: inc/classes/rest/class-basic-options.php:281
msgid "Failed to fetch GitHub repositories."
msgstr ""
-#: inc/classes/rest/class-basic-options.php:312
+#: inc/classes/rest/class-basic-options.php:313
msgid "No repositories found for rtCamp or wpcomvip."
msgstr ""
-#: inc/classes/rest/class-basic-options.php:375
+#: inc/classes/rest/class-basic-options.php:376
msgid "GitHub token is required."
msgstr ""
-#: inc/classes/rest/class-basic-options.php:394
+#: inc/classes/rest/class-basic-options.php:395
msgid "Invalid GitHub token provided."
msgstr ""
-#: inc/classes/rest/class-basic-options.php:441
-#: inc/classes/rest/class-basic-options.php:448
+#: inc/classes/rest/class-basic-options.php:442
+#: inc/classes/rest/class-basic-options.php:449
msgid "Invalid S3 credentials provided."
msgstr ""
-#: inc/classes/rest/class-basic-options.php:495
+#: inc/classes/rest/class-basic-options.php:496
msgid "Brand Site already exists."
msgstr ""
-#: inc/classes/rest/class-basic-options.php:504
+#: inc/classes/rest/class-basic-options.php:505
msgid "GitHub repository already exists in one of Brand sites."
msgstr ""
-#: inc/classes/rest/class-workflow.php:280
+#: inc/classes/rest/class-github-pull-requests.php:550
+msgid "Failed to parse GitHub API response as JSON."
+msgstr ""
+
+#: inc/classes/rest/class-github-pull-requests.php:578
+msgid "GitHub token not configured."
+msgstr ""
+
+#: inc/classes/rest/class-github-pull-requests.php:590
+msgid "OneUpdate Plugin Loader"
+msgstr ""
+
+#: inc/classes/rest/class-workflow.php:281
msgid "Transient rebuilt successfully."
msgstr ""
-#: inc/classes/rest/class-workflow.php:298
+#: inc/classes/rest/class-workflow.php:299
msgid "Invalid plugins provided."
msgstr ""
-#: inc/classes/rest/class-workflow.php:307
+#: inc/classes/rest/class-workflow.php:308
msgid "Invalid site provided."
msgstr ""
-#: inc/classes/rest/class-workflow.php:337
+#: inc/classes/rest/class-workflow.php:338
msgid "Bulk plugin update initiated successfully."
msgstr ""
-#: inc/classes/rest/class-workflow.php:361
+#: inc/classes/rest/class-workflow.php:362
msgid "Invalid action provided."
msgstr ""
-#: inc/classes/rest/class-workflow.php:365
+#: inc/classes/rest/class-workflow.php:366
msgid "Invalid plugin slug provided."
msgstr ""
-#: inc/classes/rest/class-workflow.php:369
+#: inc/classes/rest/class-workflow.php:370
msgid "Invalid sites provided."
msgstr ""
#. translators: %s is the site URL
-#: inc/classes/rest/class-workflow.php:407
-#: inc/classes/rest/class-workflow.php:434
+#: inc/classes/rest/class-workflow.php:408
+#: inc/classes/rest/class-workflow.php:435
#, php-format
msgid "Failed to execute plugin action on site %s."
msgstr ""
-#: inc/classes/rest/class-workflow.php:440
+#: inc/classes/rest/class-workflow.php:441
msgid "Unknown error occurred."
msgstr ""
#. translators: %s is the site URL
-#: inc/classes/rest/class-workflow.php:458
-#: inc/classes/rest/class-workflow.php:498
-#: inc/classes/rest/class-workflow.php:538
+#: inc/classes/rest/class-workflow.php:459
+#: inc/classes/rest/class-workflow.php:499
+#: inc/classes/rest/class-workflow.php:539
#, php-format
msgid "GitHub repository not found for site %s."
msgstr ""
-#: inc/classes/rest/class-workflow.php:572
+#: inc/classes/rest/class-workflow.php:573
msgid "Plugin action executed successfully."
msgstr ""
-#: inc/classes/rest/class-workflow.php:592
+#: inc/classes/rest/class-workflow.php:593
msgid "Invalid data provided."
msgstr ""
-#: inc/classes/rest/class-workflow.php:606
-#: inc/classes/rest/class-workflow.php:847
+#: inc/classes/rest/class-workflow.php:607
+#: inc/classes/rest/class-workflow.php:848
msgid "Invalid site data provided."
msgstr ""
-#: inc/classes/rest/class-workflow.php:671
-#: inc/classes/rest/class-workflow.php:685
-#: inc/classes/rest/class-workflow.php:963
-#: inc/classes/rest/class-workflow.php:984
+#: inc/classes/rest/class-workflow.php:672
+#: inc/classes/rest/class-workflow.php:686
+#: inc/classes/rest/class-workflow.php:964
+#: inc/classes/rest/class-workflow.php:985
msgid "Failed to trigger GitHub action for PR creation."
msgstr ""
-#: inc/classes/rest/class-workflow.php:702
-#: inc/classes/rest/class-workflow.php:1008
+#: inc/classes/rest/class-workflow.php:703
+#: inc/classes/rest/class-workflow.php:1009
msgid "GitHub Action workflow dispatched successfully"
msgstr ""
-#: inc/classes/rest/class-workflow.php:740
+#: inc/classes/rest/class-workflow.php:741
msgid "Invalid options provided."
msgstr ""
-#: inc/classes/settings/class-shared-sites.php:44
-#: inc/classes/settings/class-shared-sites.php:56
+#: inc/classes/settings/class-shared-sites.php:45
#: inc/classes/settings/class-shared-sites.php:57
+#: inc/classes/settings/class-shared-sites.php:58
msgid "Plugin Manager"
msgstr ""
-#: inc/classes/settings/class-shared-sites.php:87
-#: inc/classes/settings/class-shared-sites.php:105
+#: inc/classes/settings/class-shared-sites.php:68
+#: inc/classes/settings/class-shared-sites.php:69
+msgid "Pull Requests"
+msgstr ""
+
+#: inc/classes/settings/class-shared-sites.php:100
+#: inc/classes/settings/class-shared-sites.php:118
+#: inc/classes/settings/class-shared-sites.php:137
msgid "You do not have sufficient permissions to access this page."
msgstr ""
-#: inc/classes/settings/class-shared-sites.php:91
+#: inc/classes/settings/class-shared-sites.php:104
#: assets/build/js/plugin-manager.js:3
-#: assets/src/admin/plugin-manager/index.js:1189
+#: assets/src/admin/plugin-manager/index.js:1190
msgid "OneUpdate - Plugin Manager"
msgstr ""
+#: inc/classes/settings/class-shared-sites.php:141
+msgid "OneUpdate - Pull Requests"
+msgstr ""
+
#. translators: %s is the plugin name.
-#: oneupdate.php:43
+#: oneupdate.php:46
#, php-format
msgid "You are running the %s plugin from the GitHub repository. Please build the assets and install composer dependencies to use the plugin."
msgstr ""
#. translators: %s is the command to run.
-#: oneupdate.php:52
+#: oneupdate.php:55
#, php-format
msgid "Run the following commands in the plugin directory: %s"
msgstr ""
#. translators: %s is the plugin name.
-#: oneupdate.php:60
+#: oneupdate.php:63
#, php-format
msgid "Please refer to the %s for more information."
msgstr ""
-#: oneupdate.php:64
+#: oneupdate.php:67
msgid "OneUpdate GitHub repository"
msgstr ""
@@ -220,7 +247,7 @@ msgstr ""
#: assets/build/js/plugin-manager.js:1
#: assets/build/js/plugin-manager.js:3
-#: assets/src/admin/plugin-manager/index.js:569
+#: assets/src/admin/plugin-manager/index.js:570
#: assets/src/components/PluginsSharing.js:72
msgid "Latest"
msgstr ""
@@ -244,7 +271,7 @@ msgstr ""
#: assets/build/js/plugin-manager.js:1
#: assets/build/js/plugin-manager.js:7
-#: assets/src/admin/plugin-manager/index.js:2047
+#: assets/src/admin/plugin-manager/index.js:2048
#: assets/src/components/PluginCard.js:118
#: assets/src/components/PluginsSharing.js:144
msgid "Version"
@@ -267,7 +294,7 @@ msgstr ""
#: assets/build/js/plugin-manager.js:1
#: assets/build/js/plugin-manager.js:3
-#: assets/src/admin/plugin-manager/index.js:1282
+#: assets/src/admin/plugin-manager/index.js:1283
#: assets/src/components/PluginsSharing.js:198
msgid "Search Plugins"
msgstr ""
@@ -302,7 +329,7 @@ msgstr ""
#: assets/build/js/plugin-manager.js:1
#: assets/build/js/plugin-manager.js:5
-#: assets/src/admin/plugin-manager/index.js:1727
+#: assets/src/admin/plugin-manager/index.js:1728
#: assets/src/components/PluginGrid.js:181
#: assets/src/components/PluginsSharing.js:376
msgid "No plugins found"
@@ -356,24 +383,32 @@ msgid "Install Selected Plugins"
msgstr ""
#: assets/build/js/plugin-manager.js:1
+#: assets/build/js/pull-requests.js:1
+#: assets/src/admin/pull-requests/index.js:408
#: assets/src/components/PluginGrid.js:249
#: assets/src/components/PluginsSharing.js:467
msgid "Previous"
msgstr ""
#: assets/build/js/plugin-manager.js:1
+#: assets/build/js/pull-requests.js:1
+#: assets/src/admin/pull-requests/index.js:411
#: assets/src/components/PluginGrid.js:252
#: assets/src/components/PluginsSharing.js:470
msgid "Page"
msgstr ""
#: assets/build/js/plugin-manager.js:1
+#: assets/build/js/pull-requests.js:1
+#: assets/src/admin/pull-requests/index.js:411
#: assets/src/components/PluginGrid.js:252
#: assets/src/components/PluginsSharing.js:470
msgid "of"
msgstr ""
#: assets/build/js/plugin-manager.js:1
+#: assets/build/js/pull-requests.js:1
+#: assets/src/admin/pull-requests/index.js:419
#: assets/src/components/PluginGrid.js:259
#: assets/src/components/PluginsSharing.js:477
#: assets/src/components/PluginsSharing.js:523
@@ -408,7 +443,7 @@ msgstr ""
#: assets/build/js/plugin-manager.js:1
#: assets/build/js/plugin-manager.js:5
#: assets/build/js/settings.js:1
-#: assets/src/admin/plugin-manager/index.js:1897
+#: assets/src/admin/plugin-manager/index.js:1898
#: assets/src/components/PluginsSharing.js:517
#: assets/src/components/PluginsSharing.js:749
#: assets/src/components/S3ZipUploader.js:156
@@ -425,7 +460,7 @@ msgstr ""
#: assets/build/js/plugin-manager.js:1
#: assets/build/js/plugin-manager.js:5
-#: assets/src/admin/plugin-manager/index.js:1789
+#: assets/src/admin/plugin-manager/index.js:1790
#: assets/src/components/PluginsSharing.js:646
#: assets/src/components/S3ZipUploader.js:221
#: assets/src/components/S3ZipUploader.js:819
@@ -434,7 +469,7 @@ msgstr ""
#: assets/build/js/plugin-manager.js:1
#: assets/build/js/plugin-manager.js:5
-#: assets/src/admin/plugin-manager/index.js:1801
+#: assets/src/admin/plugin-manager/index.js:1802
#: assets/src/components/PluginsSharing.js:664
#: assets/src/components/S3ZipUploader.js:77
#: assets/src/components/S3ZipUploader.js:233
@@ -528,9 +563,9 @@ msgstr ""
#: assets/build/js/plugin-manager.js:1
#: assets/build/js/plugin-manager.js:3
-#: assets/src/admin/plugin-manager/index.js:977
-#: assets/src/admin/plugin-manager/index.js:1055
-#: assets/src/admin/plugin-manager/index.js:1082
+#: assets/src/admin/plugin-manager/index.js:978
+#: assets/src/admin/plugin-manager/index.js:1056
+#: assets/src/admin/plugin-manager/index.js:1083
#: assets/src/components/S3ZipUploader.js:342
msgid "Back"
msgstr ""
@@ -655,7 +690,10 @@ msgstr ""
#: assets/build/js/plugin-manager.js:1
#: assets/build/js/plugin-manager.js:3
-#: assets/src/admin/plugin-manager/index.js:1293
+#: assets/build/js/pull-requests.js:1
+#: assets/src/admin/plugin-manager/index.js:1294
+#: assets/src/admin/pull-requests/index.js:300
+#: assets/src/admin/pull-requests/index.js:315
#: assets/src/components/S3ZipUploader.js:1000
msgid "Status"
msgstr ""
@@ -679,552 +717,556 @@ msgstr ""
#: assets/build/js/plugin-manager.js:3
#: assets/build/js/plugin-manager.js:5
#: assets/build/js/plugin-manager.js:8
-#: assets/src/admin/plugin-manager/index.js:1298
-#: assets/src/admin/plugin-manager/index.js:1869
-#: assets/src/admin/plugin-manager/index.js:2280
+#: assets/src/admin/plugin-manager/index.js:1299
+#: assets/src/admin/plugin-manager/index.js:1870
+#: assets/src/admin/plugin-manager/index.js:2281
#: assets/src/components/S3ZipUploader.js:1055
msgid "Active"
msgstr ""
#: assets/build/js/plugin-manager.js:1
-#: assets/src/admin/plugin-manager/index.js:88
+#: assets/src/admin/plugin-manager/index.js:89
msgid "Failed to perform S3 health check."
msgstr ""
#: assets/build/js/plugin-manager.js:1
-#: assets/src/admin/plugin-manager/index.js:116
+#: assets/src/admin/plugin-manager/index.js:117
msgid "Failed to fetch sites data."
msgstr ""
#: assets/build/js/plugin-manager.js:1
-#: assets/src/admin/plugin-manager/index.js:349
+#: assets/src/admin/plugin-manager/index.js:350
msgid "Fetching sites…"
msgstr ""
#: assets/build/js/plugin-manager.js:1
-#: assets/src/admin/plugin-manager/index.js:355
+#: assets/src/admin/plugin-manager/index.js:356
msgid "No sites found."
msgstr ""
#: assets/build/js/plugin-manager.js:1
-#: assets/src/admin/plugin-manager/index.js:361
+#: assets/src/admin/plugin-manager/index.js:362
msgid "Fetching shared plugins data…"
msgstr ""
#: assets/build/js/plugin-manager.js:1
-#: assets/src/admin/plugin-manager/index.js:371
+#: assets/src/admin/plugin-manager/index.js:372
msgid "Fetching real-time plugin status…"
msgstr ""
#: assets/build/js/plugin-manager.js:1
-#: assets/src/admin/plugin-manager/index.js:382
+#: assets/src/admin/plugin-manager/index.js:383
msgid "Fetching plugins info from"
msgstr ""
#. translators: %s is the error message
#: assets/build/js/plugin-manager.js:2
-#: assets/src/admin/plugin-manager/index.js:399
+#: assets/src/admin/plugin-manager/index.js:400
#, js-format
msgid "Error fetching plugins data: %s"
msgstr ""
#: assets/build/js/plugin-manager.js:2
-#: assets/src/admin/plugin-manager/index.js:400
+#: assets/src/admin/plugin-manager/index.js:401
msgid "Unknown error"
msgstr ""
#. translators: %s is the list of sites with updates available
#: assets/build/js/plugin-manager.js:3
-#: assets/src/admin/plugin-manager/index.js:448
+#: assets/src/admin/plugin-manager/index.js:449
#, js-format
msgid "Updates available on: %s"
msgstr ""
#: assets/build/js/plugin-manager.js:3
-#: assets/src/admin/plugin-manager/index.js:588
+#: assets/src/admin/plugin-manager/index.js:589
msgid "No sites available for installation."
msgstr ""
#: assets/build/js/plugin-manager.js:3
-#: assets/src/admin/plugin-manager/index.js:912
+#: assets/src/admin/plugin-manager/index.js:913
msgid "Activate Plugin"
msgstr ""
#: assets/build/js/plugin-manager.js:3
-#: assets/src/admin/plugin-manager/index.js:913
+#: assets/src/admin/plugin-manager/index.js:914
msgid "Select sites where you want to activate this plugin."
msgstr ""
#: assets/build/js/plugin-manager.js:3
-#: assets/src/admin/plugin-manager/index.js:914
+#: assets/src/admin/plugin-manager/index.js:915
msgid "Activate on Selected Sites"
msgstr ""
#: assets/build/js/plugin-manager.js:3
-#: assets/src/admin/plugin-manager/index.js:919
+#: assets/src/admin/plugin-manager/index.js:920
msgid "Deactivate Plugin"
msgstr ""
#: assets/build/js/plugin-manager.js:3
-#: assets/src/admin/plugin-manager/index.js:920
+#: assets/src/admin/plugin-manager/index.js:921
msgid "Select sites where you want to deactivate this plugin."
msgstr ""
#: assets/build/js/plugin-manager.js:3
-#: assets/src/admin/plugin-manager/index.js:921
+#: assets/src/admin/plugin-manager/index.js:922
msgid "Deactivate on Selected Sites"
msgstr ""
#: assets/build/js/plugin-manager.js:3
-#: assets/src/admin/plugin-manager/index.js:926
+#: assets/src/admin/plugin-manager/index.js:927
msgid "Update Plugin"
msgstr ""
#: assets/build/js/plugin-manager.js:3
-#: assets/src/admin/plugin-manager/index.js:927
+#: assets/src/admin/plugin-manager/index.js:928
msgid "Select sites where you want to update this plugin."
msgstr ""
#: assets/build/js/plugin-manager.js:3
-#: assets/src/admin/plugin-manager/index.js:928
+#: assets/src/admin/plugin-manager/index.js:929
msgid "Update on Selected Sites"
msgstr ""
#: assets/build/js/plugin-manager.js:3
-#: assets/src/admin/plugin-manager/index.js:933
+#: assets/src/admin/plugin-manager/index.js:934
msgid "Change Plugin Version"
msgstr ""
#: assets/build/js/plugin-manager.js:3
-#: assets/src/admin/plugin-manager/index.js:934
+#: assets/src/admin/plugin-manager/index.js:935
msgid "Choose plugin version and select sites where you want to change/update version."
msgstr ""
#: assets/build/js/plugin-manager.js:3
-#: assets/src/admin/plugin-manager/index.js:935
+#: assets/src/admin/plugin-manager/index.js:936
msgid "Change Version on Selected Sites"
msgstr ""
#: assets/build/js/plugin-manager.js:3
-#: assets/src/admin/plugin-manager/index.js:940
+#: assets/src/admin/plugin-manager/index.js:941
msgid "Remove Plugin"
msgstr ""
#: assets/build/js/plugin-manager.js:3
-#: assets/src/admin/plugin-manager/index.js:941
+#: assets/src/admin/plugin-manager/index.js:942
msgid "Select sites where you want to remove this plugin."
msgstr ""
#: assets/build/js/plugin-manager.js:3
-#: assets/src/admin/plugin-manager/index.js:942
+#: assets/src/admin/plugin-manager/index.js:943
msgid "Remove from Selected Sites"
msgstr ""
#: assets/build/js/plugin-manager.js:3
-#: assets/src/admin/plugin-manager/index.js:947
+#: assets/src/admin/plugin-manager/index.js:948
msgid "Plugin Action"
msgstr ""
#: assets/build/js/plugin-manager.js:3
-#: assets/src/admin/plugin-manager/index.js:948
+#: assets/src/admin/plugin-manager/index.js:949
msgid "Select sites for this action."
msgstr ""
#: assets/build/js/plugin-manager.js:3
-#: assets/src/admin/plugin-manager/index.js:949
+#: assets/src/admin/plugin-manager/index.js:950
msgid "Execute Action"
msgstr ""
#: assets/build/js/plugin-manager.js:3
-#: assets/src/admin/plugin-manager/index.js:1104
+#: assets/src/admin/plugin-manager/index.js:1105
#: assets/src/components/PluginGrid.js:157
msgid "Loading plugins…"
msgstr ""
#: assets/build/js/plugin-manager.js:3
-#: assets/src/admin/plugin-manager/index.js:982
-#: assets/src/admin/plugin-manager/index.js:1215
+#: assets/src/admin/plugin-manager/index.js:983
+#: assets/src/admin/plugin-manager/index.js:1216
msgid "Add Plugin"
msgstr ""
#: assets/build/js/plugin-manager.js:3
-#: assets/src/admin/plugin-manager/index.js:985
+#: assets/src/admin/plugin-manager/index.js:986
msgid "Choose how you want to add a plugin to your sites."
msgstr ""
#: assets/build/js/plugin-manager.js:3
#: assets/build/js/plugin-manager.js:7
-#: assets/src/admin/plugin-manager/index.js:1001
-#: assets/src/admin/plugin-manager/index.js:1995
+#: assets/src/admin/plugin-manager/index.js:1002
+#: assets/src/admin/plugin-manager/index.js:1996
msgid "Public Plugin"
msgstr ""
#: assets/build/js/plugin-manager.js:3
-#: assets/src/admin/plugin-manager/index.js:1002
+#: assets/src/admin/plugin-manager/index.js:1003
msgid "Add a plugin from the WordPress.org repository."
msgstr ""
#: assets/build/js/plugin-manager.js:3
-#: assets/src/admin/plugin-manager/index.js:1010
+#: assets/src/admin/plugin-manager/index.js:1011
msgid "Invalid S3 credentials. Please check your settings."
msgstr ""
#: assets/build/js/plugin-manager.js:3
#: assets/build/js/plugin-manager.js:7
-#: assets/src/admin/plugin-manager/index.js:1026
-#: assets/src/admin/plugin-manager/index.js:1996
+#: assets/src/admin/plugin-manager/index.js:1027
+#: assets/src/admin/plugin-manager/index.js:1997
msgid "Private Plugin"
msgstr ""
#: assets/build/js/plugin-manager.js:3
-#: assets/src/admin/plugin-manager/index.js:1027
+#: assets/src/admin/plugin-manager/index.js:1028
msgid "Upload a custom plugin from your computer."
msgstr ""
#: assets/build/js/plugin-manager.js:3
-#: assets/src/admin/plugin-manager/index.js:1192
+#: assets/src/admin/plugin-manager/index.js:1193
msgid "Manage plugins across all your WordPress sites"
msgstr ""
#: assets/build/js/plugin-manager.js:3
-#: assets/src/admin/plugin-manager/index.js:818
-#: assets/src/admin/plugin-manager/index.js:826
-#: assets/src/admin/plugin-manager/index.js:882
+#: assets/src/admin/plugin-manager/index.js:819
+#: assets/src/admin/plugin-manager/index.js:827
+#: assets/src/admin/plugin-manager/index.js:883
msgid "Failed to update plugins."
msgstr ""
#: assets/build/js/plugin-manager.js:3
-#: assets/src/admin/plugin-manager/index.js:852
+#: assets/src/admin/plugin-manager/index.js:853
msgid "Plugins update's PR raised successfully."
msgstr ""
#: assets/build/js/plugin-manager.js:3
-#: assets/src/admin/plugin-manager/index.js:1205
+#: assets/src/admin/plugin-manager/index.js:1206
msgid "Update All"
msgstr ""
#: assets/build/js/plugin-manager.js:3
-#: assets/src/admin/plugin-manager/index.js:1285
+#: assets/src/admin/plugin-manager/index.js:1286
msgid "Search by name or description…"
msgstr ""
#: assets/build/js/plugin-manager.js:3
-#: assets/src/admin/plugin-manager/index.js:1297
+#: assets/build/js/pull-requests.js:1
+#: assets/src/admin/plugin-manager/index.js:1298
+#: assets/src/admin/pull-requests/index.js:59
msgid "All Status"
msgstr ""
#: assets/build/js/plugin-manager.js:3
#: assets/build/js/plugin-manager.js:8
-#: assets/src/admin/plugin-manager/index.js:1299
-#: assets/src/admin/plugin-manager/index.js:2281
+#: assets/src/admin/plugin-manager/index.js:1300
+#: assets/src/admin/plugin-manager/index.js:2282
msgid "Inactive"
msgstr ""
#: assets/build/js/plugin-manager.js:3
-#: assets/src/admin/plugin-manager/index.js:1304
+#: assets/src/admin/plugin-manager/index.js:1305
msgid "Type"
msgstr ""
#: assets/build/js/plugin-manager.js:3
-#: assets/src/admin/plugin-manager/index.js:1308
+#: assets/src/admin/plugin-manager/index.js:1309
msgid "All Types"
msgstr ""
#: assets/build/js/plugin-manager.js:3
#: assets/build/js/plugin-manager.js:4
-#: assets/src/admin/plugin-manager/index.js:1309
-#: assets/src/admin/plugin-manager/index.js:1483
+#: assets/src/admin/plugin-manager/index.js:1310
+#: assets/src/admin/plugin-manager/index.js:1484
msgid "Public"
msgstr ""
#: assets/build/js/plugin-manager.js:3
#: assets/build/js/plugin-manager.js:4
-#: assets/src/admin/plugin-manager/index.js:1310
-#: assets/src/admin/plugin-manager/index.js:1484
+#: assets/src/admin/plugin-manager/index.js:1311
+#: assets/src/admin/plugin-manager/index.js:1485
msgid "Private"
msgstr ""
#: assets/build/js/plugin-manager.js:3
-#: assets/src/admin/plugin-manager/index.js:1315
+#: assets/src/admin/plugin-manager/index.js:1316
msgid "Updates"
msgstr ""
#: assets/build/js/plugin-manager.js:3
-#: assets/src/admin/plugin-manager/index.js:1319
+#: assets/src/admin/plugin-manager/index.js:1320
msgid "All Updates"
msgstr ""
#: assets/build/js/plugin-manager.js:3
-#: assets/src/admin/plugin-manager/index.js:1320
+#: assets/src/admin/plugin-manager/index.js:1321
msgid "Updates Available"
msgstr ""
#: assets/build/js/plugin-manager.js:3
-#: assets/src/admin/plugin-manager/index.js:1326
+#: assets/src/admin/plugin-manager/index.js:1327
msgid "Filter By Site"
msgstr ""
#: assets/build/js/plugin-manager.js:3
-#: assets/src/admin/plugin-manager/index.js:1330
+#: assets/src/admin/plugin-manager/index.js:1331
msgid "All Sites"
msgstr ""
#: assets/build/js/plugin-manager.js:3
-#: assets/src/admin/plugin-manager/index.js:1331
+#: assets/src/admin/plugin-manager/index.js:1332
msgid "Common plugins"
msgstr ""
#. translators: %1$d is the number of filtered plugins, %2$d is the total number of plugins
#: assets/build/js/plugin-manager.js:4
-#: assets/src/admin/plugin-manager/index.js:1348
+#: assets/src/admin/plugin-manager/index.js:1349
#, js-format
msgid "Showing %1$d of %2$d plugins"
msgstr ""
#: assets/build/js/plugin-manager.js:4
-#: assets/src/admin/plugin-manager/index.js:1492
+#: assets/src/admin/plugin-manager/index.js:1493
msgid "Plugin Actions"
msgstr ""
#: assets/build/js/plugin-manager.js:4
-#: assets/src/admin/plugin-manager/index.js:1516
+#: assets/src/admin/plugin-manager/index.js:1517
msgid "Change version/update"
msgstr ""
#: assets/build/js/plugin-manager.js:4
-#: assets/src/admin/plugin-manager/index.js:1529
+#: assets/src/admin/plugin-manager/index.js:1530
msgid "Activate on sites"
msgstr ""
#: assets/build/js/plugin-manager.js:4
-#: assets/src/admin/plugin-manager/index.js:1542
+#: assets/src/admin/plugin-manager/index.js:1543
msgid "Deactivate on sites"
msgstr ""
#: assets/build/js/plugin-manager.js:4
-#: assets/src/admin/plugin-manager/index.js:1558
+#: assets/src/admin/plugin-manager/index.js:1559
msgid "Install on sites"
msgstr ""
#: assets/build/js/plugin-manager.js:4
-#: assets/src/admin/plugin-manager/index.js:1569
+#: assets/src/admin/plugin-manager/index.js:1570
msgid "Uninstall from sites"
msgstr ""
#: assets/build/js/plugin-manager.js:4
-#: assets/src/admin/plugin-manager/index.js:1581
+#: assets/src/admin/plugin-manager/index.js:1582
msgid "Plugin Details"
msgstr ""
#: assets/build/js/plugin-manager.js:4
-#: assets/src/admin/plugin-manager/index.js:1694
+#: assets/src/admin/plugin-manager/index.js:1695
msgid "1 site needs update"
msgstr ""
#. translators: %d is the number of sites needing updates
#: assets/build/js/plugin-manager.js:5
-#: assets/src/admin/plugin-manager/index.js:1697
+#: assets/src/admin/plugin-manager/index.js:1698
#, js-format
msgid "%d sites need updates"
msgstr ""
#: assets/build/js/plugin-manager.js:5
-#: assets/src/admin/plugin-manager/index.js:1730
+#: assets/src/admin/plugin-manager/index.js:1731
msgid "No plugins found matching your filters. Try adjusting your search criteria."
msgstr ""
#: assets/build/js/plugin-manager.js:5
-#: assets/src/admin/plugin-manager/index.js:1763
+#: assets/src/admin/plugin-manager/index.js:1764
msgid "Select Version"
msgstr ""
#: assets/build/js/plugin-manager.js:5
-#: assets/src/admin/plugin-manager/index.js:1767
+#: assets/src/admin/plugin-manager/index.js:1768
msgid "Select a version…"
msgstr ""
#: assets/build/js/plugin-manager.js:5
-#: assets/src/admin/plugin-manager/index.js:1770
+#: assets/src/admin/plugin-manager/index.js:1771
msgid "Choose from the latest 5 stable versions available"
msgstr ""
#: assets/build/js/plugin-manager.js:5
-#: assets/src/admin/plugin-manager/index.js:1776
+#: assets/src/admin/plugin-manager/index.js:1777
msgid "No stable versions available for this plugin."
msgstr ""
#: assets/build/js/plugin-manager.js:5
-#: assets/src/admin/plugin-manager/index.js:1818
+#: assets/src/admin/plugin-manager/index.js:1819
msgid "No sites available for activation. Plugin is already active on all sites."
msgstr ""
#: assets/build/js/plugin-manager.js:5
-#: assets/src/admin/plugin-manager/index.js:1823
+#: assets/src/admin/plugin-manager/index.js:1824
msgid "No sites available for deactivation. Plugin is not active on any sites."
msgstr ""
#: assets/build/js/plugin-manager.js:5
-#: assets/src/admin/plugin-manager/index.js:1828
+#: assets/src/admin/plugin-manager/index.js:1829
msgid "No sites have updates available for this plugin."
msgstr ""
#: assets/build/js/plugin-manager.js:5
#: assets/build/js/plugin-manager.js:8
-#: assets/src/admin/plugin-manager/index.js:1874
-#: assets/src/admin/plugin-manager/index.js:2295
+#: assets/src/admin/plugin-manager/index.js:1875
+#: assets/src/admin/plugin-manager/index.js:2296
msgid "Update Available"
msgstr ""
#: assets/build/js/plugin-manager.js:5
-#: assets/src/admin/plugin-manager/index.js:648
+#: assets/src/admin/plugin-manager/index.js:649
msgid "Failed to execute action."
msgstr ""
#: assets/build/js/plugin-manager.js:5
-#: assets/src/admin/plugin-manager/index.js:652
+#: assets/src/admin/plugin-manager/index.js:653
msgid "Action failed."
msgstr ""
#: assets/build/js/plugin-manager.js:5
-#: assets/src/admin/plugin-manager/index.js:685
+#: assets/src/admin/plugin-manager/index.js:686
msgid "Activated"
msgstr ""
#: assets/build/js/plugin-manager.js:5
-#: assets/src/admin/plugin-manager/index.js:688
+#: assets/src/admin/plugin-manager/index.js:689
msgid "Deactivated"
msgstr ""
#: assets/build/js/plugin-manager.js:5
-#: assets/src/admin/plugin-manager/index.js:691
+#: assets/src/admin/plugin-manager/index.js:692
msgid "Updated"
msgstr ""
#: assets/build/js/plugin-manager.js:5
-#: assets/src/admin/plugin-manager/index.js:694
+#: assets/src/admin/plugin-manager/index.js:695
msgid "Installed"
msgstr ""
#: assets/build/js/plugin-manager.js:5
-#: assets/src/admin/plugin-manager/index.js:697
+#: assets/src/admin/plugin-manager/index.js:698
msgid "Removed"
msgstr ""
#: assets/build/js/plugin-manager.js:5
-#: assets/src/admin/plugin-manager/index.js:700
+#: assets/src/admin/plugin-manager/index.js:701
msgid "Version change"
msgstr ""
#: assets/build/js/plugin-manager.js:5
-#: assets/src/admin/plugin-manager/index.js:703
+#: assets/src/admin/plugin-manager/index.js:704
msgid "Executed"
msgstr ""
#: assets/build/js/plugin-manager.js:5
-#: assets/src/admin/plugin-manager/index.js:709
+#: assets/src/admin/plugin-manager/index.js:710
msgid "selected sites"
msgstr ""
#. translators: %s is the plugin name, %s is the action verb, %s is the site names
#: assets/build/js/plugin-manager.js:6
-#: assets/src/admin/plugin-manager/index.js:715
+#: assets/src/admin/plugin-manager/index.js:716
#, js-format
msgid "%1$s %2$s PR raised successfully."
msgstr ""
#. translators: %s is the plugin name, %s is the action verb, %s is the site names
#: assets/build/js/plugin-manager.js:7
-#: assets/src/admin/plugin-manager/index.js:723
+#: assets/src/admin/plugin-manager/index.js:724
#, js-format
msgid "%1$s %2$s successfully on %3$s."
msgstr ""
#: assets/build/js/plugin-manager.js:7
-#: assets/src/admin/plugin-manager/index.js:1912
+#: assets/src/admin/plugin-manager/index.js:1913
msgid "Processing…"
msgstr ""
#: assets/build/js/plugin-manager.js:7
-#: assets/src/admin/plugin-manager/index.js:2023
+#: assets/src/admin/plugin-manager/index.js:2024
msgid "Description"
msgstr ""
#: assets/build/js/plugin-manager.js:7
-#: assets/src/admin/plugin-manager/index.js:2041
+#: assets/build/js/pull-requests.js:1
+#: assets/src/admin/plugin-manager/index.js:2042
+#: assets/src/admin/pull-requests/index.js:314
msgid "Author"
msgstr ""
#: assets/build/js/plugin-manager.js:7
-#: assets/src/admin/plugin-manager/index.js:2053
+#: assets/src/admin/plugin-manager/index.js:2054
msgid "Sites"
msgstr ""
#: assets/build/js/plugin-manager.js:7
-#: assets/src/admin/plugin-manager/index.js:2062
+#: assets/src/admin/plugin-manager/index.js:2063
msgid "Downloads"
msgstr ""
#: assets/build/js/plugin-manager.js:7
-#: assets/src/admin/plugin-manager/index.js:2072
+#: assets/src/admin/plugin-manager/index.js:2073
msgid "Requires WordPress"
msgstr ""
#: assets/build/js/plugin-manager.js:7
-#: assets/src/admin/plugin-manager/index.js:2080
+#: assets/src/admin/plugin-manager/index.js:2081
msgid "Tested up to"
msgstr ""
#: assets/build/js/plugin-manager.js:7
-#: assets/src/admin/plugin-manager/index.js:2088
+#: assets/src/admin/plugin-manager/index.js:2089
msgid "Requires PHP"
msgstr ""
#: assets/build/js/plugin-manager.js:7
-#: assets/src/admin/plugin-manager/index.js:2096
+#: assets/src/admin/plugin-manager/index.js:2097
msgid "Last Updated"
msgstr ""
#: assets/build/js/plugin-manager.js:7
-#: assets/src/admin/plugin-manager/index.js:2109
+#: assets/src/admin/plugin-manager/index.js:2110
msgid "Tags"
msgstr ""
#: assets/build/js/plugin-manager.js:7
-#: assets/src/admin/plugin-manager/index.js:2136
+#: assets/src/admin/plugin-manager/index.js:2137
msgid "Plugin URI"
msgstr ""
#: assets/build/js/plugin-manager.js:7
-#: assets/src/admin/plugin-manager/index.js:2153
+#: assets/src/admin/plugin-manager/index.js:2154
msgid "Installation"
msgstr ""
#: assets/build/js/plugin-manager.js:7
-#: assets/src/admin/plugin-manager/index.js:2170
+#: assets/src/admin/plugin-manager/index.js:2171
msgid "FAQ"
msgstr ""
#: assets/build/js/plugin-manager.js:7
-#: assets/src/admin/plugin-manager/index.js:2187
+#: assets/src/admin/plugin-manager/index.js:2188
msgid "Changelog"
msgstr ""
#: assets/build/js/plugin-manager.js:7
#: assets/build/js/plugin-manager.js:8
-#: assets/src/admin/plugin-manager/index.js:2211
-#: assets/src/admin/plugin-manager/index.js:2312
+#: assets/src/admin/plugin-manager/index.js:2212
+#: assets/src/admin/plugin-manager/index.js:2313
msgid "Close"
msgstr ""
#. translators: %s is the plugin name
#: assets/build/js/plugin-manager.js:8
-#: assets/src/admin/plugin-manager/index.js:2223
+#: assets/src/admin/plugin-manager/index.js:2224
#, js-format
msgid "%s - Sites"
msgstr ""
#: assets/build/js/plugin-manager.js:8
-#: assets/src/admin/plugin-manager/index.js:2232
+#: assets/src/admin/plugin-manager/index.js:2233
msgid "Plugin status across all sites"
msgstr ""
@@ -1250,7 +1292,7 @@ msgstr ""
#: assets/build/js/plugin.js:1
#: assets/src/admin/plugin/index.js:19
-msgid "Governing site"
+msgid "Governing Site"
msgstr ""
#: assets/build/js/plugin.js:1
@@ -1271,6 +1313,188 @@ msgstr ""
msgid "Select current site type"
msgstr ""
+#: assets/build/js/pull-requests.js:1
+#: assets/src/admin/pull-requests/index.js:60
+msgid "Open"
+msgstr ""
+
+#: assets/build/js/pull-requests.js:1
+#: assets/src/admin/pull-requests/index.js:61
+msgid "Merged/Closed"
+msgstr ""
+
+#: assets/build/js/pull-requests.js:1
+#: assets/src/admin/pull-requests/index.js:97
+msgid "Please enter valid character to search pull requests."
+msgstr ""
+
+#: assets/build/js/pull-requests.js:1
+#: assets/src/admin/pull-requests/index.js:102
+msgid "Failed to fetch pull requests."
+msgstr ""
+
+#: assets/build/js/pull-requests.js:1
+#: assets/src/admin/pull-requests/index.js:119
+#: assets/src/admin/pull-requests/index.js:125
+msgid "Error fetching pull requests."
+msgstr ""
+
+#: assets/build/js/pull-requests.js:1
+#: assets/src/admin/pull-requests/index.js:164
+msgid "Error fetching PR details."
+msgstr ""
+
+#: assets/build/js/pull-requests.js:1
+#: assets/src/admin/pull-requests/index.js:185
+msgid "N/A"
+msgstr ""
+
+#: assets/build/js/pull-requests.js:1
+#: assets/src/admin/pull-requests/index.js:281
+msgid "GitHub Pull Requests"
+msgstr ""
+
+#: assets/build/js/pull-requests.js:1
+#: assets/src/admin/pull-requests/index.js:287
+#: assets/src/components/PluginGrid.js:149
+msgid "Search"
+msgstr ""
+
+#: assets/build/js/pull-requests.js:1
+#: assets/src/admin/pull-requests/index.js:288
+msgid "Search by title, number…"
+msgstr ""
+
+#: assets/build/js/pull-requests.js:1
+#: assets/build/js/settings.js:1
+#: assets/src/admin/pull-requests/index.js:294
+#: assets/src/components/SiteTable.js:28
+msgid "Brand Sites"
+msgstr ""
+
+#: assets/build/js/pull-requests.js:1
+#: assets/src/admin/pull-requests/index.js:312
+msgid "PR #"
+msgstr ""
+
+#: assets/build/js/pull-requests.js:1
+#: assets/src/admin/pull-requests/index.js:313
+msgid "Title"
+msgstr ""
+
+#: assets/build/js/pull-requests.js:1
+#: assets/src/admin/pull-requests/index.js:316
+msgid "Created at"
+msgstr ""
+
+#: assets/build/js/pull-requests.js:1
+#: assets/src/admin/pull-requests/index.js:317
+msgid "Labels"
+msgstr ""
+
+#: assets/build/js/pull-requests.js:1
+#: assets/build/js/settings.js:1
+#: assets/src/admin/pull-requests/index.js:318
+#: assets/src/components/SiteTable.js:45
+msgid "Actions"
+msgstr ""
+
+#: assets/build/js/pull-requests.js:1
+#: assets/src/admin/pull-requests/index.js:332
+msgid "No pull requests found."
+msgstr ""
+
+#: assets/build/js/pull-requests.js:1
+#: assets/src/admin/pull-requests/index.js:383
+msgid "more"
+msgstr ""
+
+#: assets/build/js/pull-requests.js:1
+#: assets/src/admin/pull-requests/index.js:389
+msgid "No labels"
+msgstr ""
+
+#: assets/build/js/pull-requests.js:1
+#: assets/src/admin/pull-requests/index.js:225
+msgid "PR Actions"
+msgstr ""
+
+#: assets/build/js/pull-requests.js:1
+#: assets/src/admin/pull-requests/index.js:242
+#: assets/src/admin/pull-requests/index.js:256
+msgid "View Details"
+msgstr ""
+
+#: assets/build/js/pull-requests.js:1
+#: assets/src/admin/pull-requests/index.js:429
+msgid "Pull Request Details"
+msgstr ""
+
+#: assets/build/js/pull-requests.js:1
+#: assets/src/admin/pull-requests/index.js:438
+msgid "Loading PR details…"
+msgstr ""
+
+#: assets/build/js/pull-requests.js:1
+#: assets/src/admin/pull-requests/index.js:447
+msgid "PR Number:"
+msgstr ""
+
+#: assets/build/js/pull-requests.js:1
+#: assets/src/admin/pull-requests/index.js:448
+msgid "Title:"
+msgstr ""
+
+#: assets/build/js/pull-requests.js:1
+#: assets/src/admin/pull-requests/index.js:449
+msgid "Author:"
+msgstr ""
+
+#: assets/build/js/pull-requests.js:1
+#: assets/src/admin/pull-requests/index.js:450
+msgid "Status:"
+msgstr ""
+
+#: assets/build/js/pull-requests.js:1
+#: assets/src/admin/pull-requests/index.js:453
+msgid "Created:"
+msgstr ""
+
+#: assets/build/js/pull-requests.js:1
+#: assets/src/admin/pull-requests/index.js:454
+msgid "Updated:"
+msgstr ""
+
+#: assets/build/js/pull-requests.js:1
+#: assets/src/admin/pull-requests/index.js:455
+msgid "Branch:"
+msgstr ""
+
+#: assets/build/js/pull-requests.js:1
+#: assets/src/admin/pull-requests/index.js:457
+msgid "GitHub URL:"
+msgstr ""
+
+#: assets/build/js/pull-requests.js:1
+#: assets/src/admin/pull-requests/index.js:459
+msgid "View on GitHub"
+msgstr ""
+
+#: assets/build/js/pull-requests.js:1
+#: assets/src/admin/pull-requests/index.js:468
+msgid "Labels:"
+msgstr ""
+
+#: assets/build/js/pull-requests.js:1
+#: assets/src/admin/pull-requests/index.js:485
+msgid "Description:"
+msgstr ""
+
+#: assets/build/js/pull-requests.js:1
+#: assets/src/admin/pull-requests/index.js:511
+msgid "Merged By:"
+msgstr ""
+
#: assets/build/js/settings.js:1
#: assets/src/components/SiteTable.js:101
msgid "Delete Brand Site"
@@ -1287,11 +1511,6 @@ msgstr ""
msgid "Delete"
msgstr ""
-#: assets/build/js/settings.js:1
-#: assets/src/components/SiteTable.js:28
-msgid "Brand Sites"
-msgstr ""
-
#: assets/build/js/settings.js:1
#: assets/src/components/SiteModal.js:113
#: assets/src/components/SiteTable.js:34
@@ -1319,11 +1538,6 @@ msgstr ""
msgid "API Key"
msgstr ""
-#: assets/build/js/settings.js:1
-#: assets/src/components/SiteTable.js:45
-msgid "Actions"
-msgstr ""
-
#: assets/build/js/settings.js:1
#: assets/src/components/SiteTable.js:52
msgid "No Brand Sites found."
@@ -1665,10 +1879,6 @@ msgstr ""
msgid "Search plugins…"
msgstr ""
-#: assets/src/components/PluginGrid.js:149
-msgid "Search"
-msgstr ""
-
#: assets/src/components/PluginGrid.js:182
msgid "Unable to find any plugins to display."
msgstr ""
diff --git a/oneupdate.php b/oneupdate.php
index 10fff9f..bc9c3c6 100644
--- a/oneupdate.php
+++ b/oneupdate.php
@@ -1,7 +1,8 @@