Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Show settings link & API Key in Network Admin sites list #583

Merged
merged 24 commits into from
Jan 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
25a63c1
Show settings link in Network Admin sites list
jblz Jan 10, 2022
585d9a4
Add column for the API key to the network admin site listing. (#584)
pauarge Jan 10, 2022
0868620
Strict type check on status, invert comparison, yoda condition check,…
jblz Jan 10, 2022
ae8689d
really add yoda conditional & ignore switch_to_blog
jblz Jan 10, 2022
29beebf
Move functionality to its own UI class
jblz Jan 10, 2022
f0fcb86
rely on type declaration providing an int for blog id
jblz Jan 11, 2022
50ca880
Move column name to a class constant
jblz Jan 11, 2022
dd8af6a
fixup docblocks
jblz Jan 11, 2022
6e4ebde
return type on run fn
jblz Jan 11, 2022
424fe11
Wrap api key missing string for translation
jblz Jan 11, 2022
4b8d639
include multisite integration tests in CI
jblz Jan 11, 2022
9a69e7d
Integration tests for MS Sites List API Key Column
jblz Jan 11, 2022
a92dc1b
Add test coverage for Parsely::get_settings_url
jblz Jan 13, 2022
4cf3f6f
fill out covers and uses test fn annotations
jblz Jan 13, 2022
56aeb49
run testwp-ms in CI on php 8.0 as well
jblz Jan 13, 2022
09f37f7
pull admin_init hook into named function
jblz Jan 19, 2022
9275ad2
Correct errant namespace import
jblz Jan 19, 2022
73063de
ensure column is not present prior to run()
jblz Jan 19, 2022
1db3e05
Merge branch 'develop' into add/network-site-list-settings-link
pauarge Jan 21, 2022
063e3cb
Merge branch 'develop' into add/network-site-list-settings-link
pauarge Jan 25, 2022
9137b8e
Merge branch 'develop' into add/network-site-list-settings-link
jblz Jan 25, 2022
8a0fbcf
no need to require the class file -- composer dump-autoload first
jblz Jan 25, 2022
57cd658
add an aria-label for the link to site settings
jblz Jan 25, 2022
f2a4bd3
Merge branch 'develop' into add/network-site-list-settings-link
pauarge Jan 25, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ jobs:
if: ${{ matrix.php != 8.0 }}
run: composer testwp --no-interaction

- name: Run integration tests (multi site)
run: composer testwp-ms --no-interaction

- name: Run integration tests (multisite site with code coverage)
if: ${{ matrix.php == 8.0 }}
run: composer coveragewp-ci --no-interaction
125 changes: 125 additions & 0 deletions src/UI/class-network-admin-sites-list.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<?php
/**
* Parsely Network Admin Site List class
*
* @package Parsely
* @since 3.2.0
*/

declare(strict_types=1);

namespace Parsely\UI;

use Parsely\Parsely;
use WP_Site;

/**
* Render the additions to the WordPress Multisite Network Admin Sites List page
*
* @since 3.2.0
*/
final class Network_Admin_Sites_List {
const COLUMN_NAME = 'parsely-api-key';

/**
* Constructor.
*
* @param Parsely $parsely Instance of Parsely class.
*/
public function __construct( Parsely $parsely ) {
$this->parsely = $parsely;
}

/**
* Attach network admin page functionality to the appropriate action and filter hooks.
*
* @since 3.2.0
* @return void
*/
public function run(): void {
add_filter( 'manage_sites_action_links', array( __CLASS__, 'add_action_link' ), 10, 2 );
add_filter( 'wpmu_blogs_columns', array( __CLASS__, 'add_api_key_column' ) );
add_action( 'manage_sites_custom_column', array( $this, 'populate_api_key_column' ), 10, 2 );
}

/**
* Use the manage_sites_action_links filter to append a link to the settings page in the "row actions."
*
* @since 3.2.0
jblz marked this conversation as resolved.
Show resolved Hide resolved
*
* @param array $actions The list of actions meant to be displayed for the current site's context in the row actions.
* @param int $_blog_id The blog ID for the current context.
* @return array The list of actions including ours.
*/
public static function add_action_link( array $actions, int $_blog_id ): array {
if ( ! current_user_can( Parsely::CAPABILITY ) ) {
return $actions;
}

$actions['parsely-settings'] = sprintf(
'<a href="%1$s" aria-label="%2$s">%3$s</a>',
esc_url( esc_url( Parsely::get_settings_url( $_blog_id ) ) ),
esc_attr( self::generate_aria_label_for_blog_id( $_blog_id ) ),
__( 'Parse.ly Settings', 'wp-parsely' )
);

return $actions;
}

/**
* Generate ARIA label content.
*
* @since 3.2.0
*
* @param int $_blog_id Which sub-site to include in the ARIA label.
* @return string ARIA label content including the blogname.
*/
private static function generate_aria_label_for_blog_id( int $_blog_id ): string {
$site = get_blog_details( $_blog_id );

return sprintf(
/* translators: blog name or blog id if empty */
__( 'Go to Parse.ly stats for "%s"', 'wp-parsely' ),
empty( $site->blogname ) ? $_blog_id : $site->blogname
);
}

/**
* Use the wpmu_blogs_columns filter to register the column where we'll display the site's API Key (if configured).
*
* @since 3.2.0
*
* @param array $sites_columns The list of columns meant to be displayed in the sites list table.
* @return array The list of columns to display in the network admin table including ours.
*/
public static function add_api_key_column( array $sites_columns ): array {
$sites_columns[ self::COLUMN_NAME ] = __( 'Parse.ly API Key', 'wp-parsely' );
return $sites_columns;
}

/**
* Use the manage_sites_custom_column action to output each site's API Key (if configured).
*
* @since 3.2.0
*
* @param string $column_name The column name for the current context.
* @param int $_blog_id The blog ID for the current context.
* @return void
*/
public function populate_api_key_column( string $column_name, int $_blog_id ): void {
if ( self::COLUMN_NAME !== $column_name ) {
return;
}

// phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.switch_to_blog_switch_to_blog
switch_to_blog( $_blog_id );
$apikey = $this->parsely->get_api_key();
restore_current_blog();
GaryJones marked this conversation as resolved.
Show resolved Hide resolved

if ( strlen( $apikey ) > 0 ) {
echo esc_html( $apikey );
} else {
echo '<em>' . esc_html__( 'Parse.ly API key is missing', 'wp-parsely' ) . '</em>';
}
}
}
6 changes: 4 additions & 2 deletions src/class-parsely.php
Original file line number Diff line number Diff line change
Expand Up @@ -974,10 +974,12 @@ public function get_clean_parsely_page_value( ?string $val ): string {
/**
* Get the URL of the plugin settings page.
*
* @param int $_blog_id The Blog ID for the multisite subsite to use for context (Default null for current).
*
* @return string
*/
public static function get_settings_url(): string {
return admin_url( 'options-general.php?page=' . self::MENU_SLUG );
public static function get_settings_url( int $_blog_id = null ): string {
return get_admin_url( $_blog_id, 'options-general.php?page=' . self::MENU_SLUG );
jblz marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand Down
116 changes: 116 additions & 0 deletions tests/Integration/UI/NetworkAdminSitesListTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<?php
/**
* UI Tests for the Network Admin Sites List.
*
* @package Parsely
*/

declare(strict_types=1);

namespace Parsely\Tests\Integration\UI;

use Parsely\Parsely;
use Parsely\Tests\Integration\TestCase;
use Parsely\UI\Network_Admin_Sites_List;
use WP_Site;

/**
* UI Tests for the Network Admin Sites List.
*/
final class NetworkAdminSitesListTest extends TestCase {
/**
* Hold an insance of Network_Admin_Sites_List
*
* @var Network_Admin_Sites_List
*/
private static $sites_list;

/**
* Hold an instance of WP_MS_Sites_List_Table
*
* @var WP_MS_Sites_List_Table
*/
public $table = false;

/**
* Skip all tests for non-multisite runs.
* Set up an instance variable to hold a `WP_MS_Sites_List_Table` object.
*
* @return void
*/
public function set_up(): void {
parent::set_up();

if ( ! is_multisite() ) {
self::markTestSkipped();
}

$this->table = _get_list_table( 'WP_MS_Sites_List_Table', array( 'screen' => 'ms-sites' ) );
self::$sites_list = new Network_Admin_Sites_List( new Parsely() );
}

/**
* Make sure the custom column is included.
*
* @covers \Parsely\UI\Network_Admin_Sites_List::add_api_key_column
* @covers \Parsely\UI\Network_Admin_Sites_List::run
* @uses \Parsely\UI\Network_Admin_Sites_List::__construct
* @return void
*/
public function test_api_key_column_is_present(): void {
$columns = $this->table->get_columns();
self::assertArrayNotHasKey( 'parsely-api-key', $columns );

self::$sites_list->run();
$columns = $this->table->get_columns();

self::assertArrayHasKey( 'parsely-api-key', $columns );
self::assertSame( 'Parse.ly API Key', $columns['parsely-api-key'] );
}

/**
* Make sure the custom column is populated with default data for no option and the API key when set.
*
* @covers \Parsely\UI\Network_Admin_Sites_List::populate_api_key_column
* @covers \Parsely\UI\Network_Admin_Sites_List::run
* @uses \Parsely\Parsely::api_key_is_set
* @uses \Parsely\Parsely::get_api_key
* @uses \Parsely\Parsely::get_options
* @uses \Parsely\UI\Network_Admin_Sites_List::__construct
* @return void
*/
public function test_api_key_column_is_correctly_printed(): void {
$blog_id_with_api_key = $this->factory->blog->create();
$blog_id_without_api_key = $this->factory->blog->create();

self::$sites_list->run();

update_blog_option( $blog_id_with_api_key, Parsely::OPTIONS_KEY, array( 'apikey' => 'parselyrocks.example.com' ) );

$this->table->prepare_items();

self::assertCount( 3, $this->table->items, 'There should be the main site, the subsite with the apikey set, and a subsite without.' );

foreach ( $this->table->items as $site ) {
self::assertInstanceOf( WP_Site::class, $site );

ob_start();
$this->table->column_default( $site->to_array(), 'parsely-api-key' );
$api_key_col_value = ob_get_clean();

if ( $blog_id_with_api_key === (int) $site->blog_id ) {
self::assertSame(
'parselyrocks.example.com',
$api_key_col_value,
'The API key was not printed and should have been.'
);
} else {
self::assertSame(
'<em>Parse.ly API key is missing</em>',
$api_key_col_value,
'The default value was not printed and should have been.'
);
}
}
}
}
53 changes: 53 additions & 0 deletions tests/Integration/UI/SettingsPageTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,57 @@ public function test_validate_duplicate_tracking_with_unexpected_array_order():
$actual = self::$settings_page->validate_options( $options );
self::assertSame( $expected, $actual );
}

/**
* Make sure that the settings URL is correctly returned for single sites and multisites with and without a blog_id param.
*
* @covers \Parsely\Parsely::get_settings_url
* @uses \Parsely\UI\Settings_Page::__construct
* @return void
*/
public function test_get_settings_url_with_and_without_blog_id(): void {
self::assertSame(
'http://example.org/wp-admin/options-general.php?page=parsely',
self::$parsely->get_settings_url(),
'The URL did not match the expected value without a $blog_id param.'
);

self::assertSame(
'http://example.org/wp-admin/options-general.php?page=parsely',
self::$parsely->get_settings_url( get_current_blog_id() ),
'The URL did not match the expected value with a $blog_id param.'
);

if ( ! is_multisite() ) {
return;
}

$subsite_blog_id = $this->factory->blog->create(
array(
'domain' => 'parselyrocks.example.org',
'path' => '/vipvipvip',
)
);

self::assertSame(
'http://parselyrocks.example.org/vipvipvip/wp-admin/options-general.php?page=parsely',
self::$parsely->get_settings_url( $subsite_blog_id ),
'The URL did not match when passing $subsite_blog_id.'
);

// phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.switch_to_blog_switch_to_blog
switch_to_blog( $subsite_blog_id );
self::assertSame(
'http://parselyrocks.example.org/vipvipvip/wp-admin/options-general.php?page=parsely',
self::$parsely->get_settings_url(),
'The URL did not match the subsite without passing a $blog_id param.'
);
restore_current_blog();

self::assertSame(
'http://example.org/wp-admin/options-general.php?page=parsely',
self::$parsely->get_settings_url(),
'The URL did not match the expected value for the main site with no $blog_id param after switching back.'
);
}
}
14 changes: 14 additions & 0 deletions wp-parsely.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
use Parsely\UI\Admin_Bar;
use Parsely\UI\Admin_Warning;
use Parsely\UI\Plugins_Actions;
use Parsely\UI\Network_Admin_Sites_List;
use Parsely\UI\Recommended_Widget;
use Parsely\UI\Row_Actions;
use Parsely\UI\Settings_Page;
Expand Down Expand Up @@ -104,6 +105,19 @@ function parsely_admin_menu_register(): void {
$settings_page->run();
}

require __DIR__ . '/src/UI/class-network-admin-sites-list.php';

add_action( 'admin_init', __NAMESPACE__ . '\\admin_init_network_sites_list' );
/**
* Register the additions the Multisite Network Admin Sites List table.
*
* @return void
*/
function admin_init_network_sites_list(): void {
$network_admin_sites_list = new Network_Admin_Sites_List( $GLOBALS['parsely'] );
$network_admin_sites_list->run();
}

require __DIR__ . '/src/UI/class-recommended-widget.php';

add_action( 'widgets_init', __NAMESPACE__ . '\\parsely_recommended_widget_register' );
Expand Down