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 */ } + + + + + + + + + + + + + + { loading && ( + + + + ) } + { ! loading && pullRequests.length === 0 && ( + + + + ) } + { pullRequests.map( ( pr ) => ( + + + + + + + + + + ) ) } + +
{ __( 'PR #', 'oneupdate' ) }{ __( 'Title', 'oneupdate' ) }{ __( 'Author', 'oneupdate' ) }{ __( 'Status', 'oneupdate' ) }{ __( 'Created at', 'oneupdate' ) }{ __( 'Labels', 'oneupdate' ) }{ __( 'Actions', 'oneupdate' ) }
+ +
+ { __( 'No pull requests found.', 'oneupdate' ) } +
+ + #{ 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 @@