Add and apply coding standards. (#129)

Signed-off-by: costdev <79332690+costdev@users.noreply.github.com>
Signed-off-by: Colin Stewart <79332690+costdev@users.noreply.github.com>
Co-authored-by: Andy Fragen <andy@thefragens.com>
This commit is contained in:
Colin Stewart 2025-06-26 18:10:59 +01:00 committed by GitHub
parent 6de87db50f
commit 0dc693b68d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 375 additions and 181 deletions

0
.cache/.gitkeep Normal file
View file

View file

@ -1,11 +1,13 @@
# Main
.git/
.github/
.cache/
bin/
node_modules/
tests/
phpunit.xml.dist
wp-tests-config-sample.php
phpcs.xml.dist
composer.json
composer.lock
package.json

2
.gitattributes vendored
View file

@ -3,10 +3,12 @@
/.git export-ignore
/.github export-ignore
/bin export-ignore
/.cache export-ignore
/node_modules export-ignore
/tests export-ignore
phpunit.xml.dist export-ignore
wp-tests-config-sample.php export-ignore
phpcs.xml.dist export-ignore
composer.json export-ignore
composer.lock export-ignore
package.json export-ignore

64
.github/workflows/coding-standards.yml vendored Normal file
View file

@ -0,0 +1,64 @@
name: PHP coding standards
on:
push:
branches:
- main
pull_request:
branches:
- main
workflow_dispatch:
# Cancels all previous workflow runs for pull requests that have not completed.
concurrency:
# The concurrency group contains the workflow name and the branch name for pull requests
# or the commit hash for any other events.
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }}
cancel-in-progress: true
# Disable permissions for all available scopes by default.
# Any needed permissions should be configured at the job level.
permissions: {}
jobs:
# Runs the PHP coding standards checks.
#
# Violations are reported inline with annotations.
#
# Performs the following steps:
# - Checks out the repository.
# - Sets up PHP.
# - Installs Composer dependencies.
# - Make Composer packages available globally.
# - Runs PHPCS on the full codebase (warnings excluded).
# - Ensures version-controlled files are not modified or deleted.
phpcs:
name: Run coding standards checks
runs-on: ubuntu-latest
permissions:
contents: read
timeout-minutes: 20
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
show-progress: ${{ runner.debug == '1' && 'true' || 'false' }}
- name: Set up PHP
uses: shivammathur/setup-php@cf4cade2721270509d5b1c766ab3549210a39a2a # v2.33.0
with:
coverage: none
- name: Install Composer dependencies
uses: ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520 # v3.1.1
- name: Make Composer packages available globally
run: echo "${PWD}/vendor/bin" >> $GITHUB_PATH
- name: Run PHPCS on all files
id: phpcs-files
run: phpcs . -n --report-full
- name: Ensure version-controlled files are not modified during the checks
run: git diff --exit-code

5
.gitignore vendored
View file

@ -1,9 +1,14 @@
/.cache/
/node_modules/
*.bak
/.vscode
/vendor/
.phpunit.result.cache
phpunit.xml
phpcs.xml
/tests/phpunit/cache
/tests/phpunit/coverage
wp-tests-config.php
# Track placeholders so that empty directories stay in the repo.
!.gitkeep

View file

@ -13,12 +13,18 @@
},
"require-dev": {
"yoast/phpunit-polyfills": "*",
"nimut/phpunit-merger": "*"
"nimut/phpunit-merger": "*",
"humanmade/coding-standards": "^1.2"
},
"config": {
"lock": false
"lock": false,
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
}
},
"scripts": {
"lint": "@php ./vendor/bin/phpcs .",
"format": "@php ./vendor/bin/phpcbf .",
"test": [
"Composer\\Config::disableProcessTimeout",
"@php ./vendor/phpunit/phpunit/phpunit"

View file

@ -1,4 +1,9 @@
<?php
/**
* Adds a local source for avatars.
*
* @package FAIR
*/
namespace FAIR\Avatars;
@ -41,7 +46,7 @@ function enqueue_media_scripts( $hook_suffix ) {
}
// Grab the user ID to pass along for alt text.
$user_id = 'profile.php' === $hook_suffix ? get_current_user_id() : absint( $_GET['user_id'] );
$user_id = 'profile.php' === $hook_suffix ? get_current_user_id() : absint( $_GET['user_id'] ?? 0 );
$display_name = get_user( $user_id )->display_name;
wp_enqueue_media();
@ -105,6 +110,8 @@ function add_avatar_upload_field( $description, $profile_user ) {
* @param int $user_id User ID.
*/
function save_avatar_upload( $user_id ) {
check_admin_referer( 'update-user_' . $user_id );
if ( ! current_user_can( 'edit_user', $user_id ) ) {
return;
}
@ -137,7 +144,7 @@ function save_avatar_upload( $user_id ) {
function filter_avatar( $avatar, $id_or_email, $size, $default, $alt, $args ) {
$avatar_url = get_avatar_url( $id_or_email, $args );
$class = array( 'avatar', 'avatar-' . (int) $size, 'photo' );
$class = [ 'avatar', 'avatar-' . (int) $size, 'photo' ];
if ( ! empty( $args['class'] ) ) {
$class = array_merge( $class, (array) $args['class'] );
}

View file

@ -1,4 +1,9 @@
<?php
/**
* Changes events to use The WP World, and news to use FAIR Planet.
*
* @package FAIR
*/
namespace FAIR\Dashboard_Widgets;
@ -31,7 +36,6 @@ function bootstrap() {
/**
* Fires after core widgets for the admin dashboard have been registered.
*
*/
function on_dashboard_setup() : void {
// Swap the "Primary" dashboard widget's callback.
@ -112,7 +116,7 @@ function get_community_events() {
$url = add_query_arg( 'ref', 'fair-dashboard', $event['camp_website_url'] ?? $event['link'] );
$events[] = array(
$events[] = [
'type' => 'event',
'title' => $event['title']['rendered'],
'url' => $url,
@ -128,7 +132,7 @@ function get_community_events() {
'latitude' => $event['camp_lat'] ?? 0,
'longitude' => $event['camp_lng'] ?? 0,
],
);
];
}
// Resort events by start date.
@ -171,9 +175,9 @@ function render_news_widget() : void {
'<a href="%1$s" target="_blank">%2$s <span class="screen-reader-text"> %3$s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a>',
/* translators: If a Rosetta site exists (e.g. https://es.fair.pm/news/), then use that. Otherwise, leave untranslated. */
esc_url( _x( 'https://fair.pm/', 'Events and News dashboard widget', 'fair' ) ),
__( 'News' ),
__( 'News', 'fair' ),
/* translators: Hidden accessibility text. */
__( '(opens in a new tab)' )
__( '(opens in a new tab)', 'fair' )
);
?>
@ -185,7 +189,7 @@ function render_news_widget() : void {
'https://thewp.world/events/',
__( 'Events (by The WP World)', 'fair' ),
/* translators: Hidden accessibility text. */
__( '(opens in a new tab)' )
__( '(opens in a new tab)', 'fair' )
);
?>
</p>
@ -240,6 +244,7 @@ function set_help_content_fair_planet_urls() : void {
$planet_fair_url = rtrim( $planet_fair_url, '/' );
$new_tab_content = preg_replace(
/* phpcs:ignore WordPress.WP.CapitalPDangit.Misspelled */
'/https?:\/\/planet\.wordpress\.org/',
$planet_fair_url,
$tab['content'],

View file

@ -7,8 +7,6 @@
namespace FAIR\Default_Repo;
use WP_Error;
/**
* Bootstrap.
*/
@ -39,8 +37,9 @@ function get_default_repo_domain() : string {
* and themes APIs. Only these get passed to the chosen FAIR repo, as the others are
* handled in other modules.
*
* @param array $args
* @param string $url
* @param bool|array $status Filtered value, or false to proceed.
* @param array $args HTTP request arguments.
* @param string $url The request URL.
* @return bool|array Replaced value, or false to proceed.
*/
function replace_repo_api_urls( $status, $args, $url ) {
@ -76,7 +75,7 @@ function replace_repo_api_urls( $status, $args, $url ) {
* This tab only makes sense for WordPress.org, so is not supported by
* other repositories.
*
* @param array $tabs
* @param array $tabs Tabs in the plugin browser.
*/
function remove_favorites_tab( array $tabs ) : array {
unset( $tabs['favorites'] );

View file

@ -8,7 +8,6 @@
namespace FAIR\Icons;
use FAIR;
use stdClass;
/**
* Bootstrap
@ -40,7 +39,7 @@ function set_default_icon( $transient ) {
* @return string
*/
function set_random_color() {
$rand = str_pad( dechex( rand( 0x000000, 0xFFFFFF ) ), 6, 0, STR_PAD_LEFT );
$rand = str_pad( dechex( wp_rand( 0x000000, 0xFFFFFF ) ), 6, 0, STR_PAD_LEFT );
return $rand;
}

View file

@ -5,6 +5,7 @@
* @package FAIR
*/
// phpcs:ignore HM.Security.ValidatedSanitizedInput.MissingUnslash, HM.Security.ValidatedSanitizedInput.InputNotSanitized
$color = isset( $_GET['color'] ) ? sanitize_hex_color( '#' . stripslashes( $_GET['color'] ) ) : '';
/**
@ -29,7 +30,7 @@ function sanitize_hex_color( $color ) {
header( 'Content-Type: image/svg+xml' );
// Echo the SVG content.
// phpcs:ignore HM.Security.EscapeOutput.OutputNotEscaped
// phpcs:disable HM.Security.EscapeOutput.OutputNotEscaped
echo '<?xml version="1.0" encoding="UTF-8"?>
<svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<rect width="200" height="200" style="fill:' . $color . ';"/>
@ -56,3 +57,4 @@ echo '<?xml version="1.0" encoding="UTF-8"?>
<polygon points="50 173 25 173 40.7 200 59.3 200 75 173 50 173" style="fill:rgba(255,255,255,.41);"/><polygon points="100 173 75 173 90.7 200 109.3 200 125 173 100 173" style="fill:rgba(255,255,255,.25);"/>
<polygon points="150 173 125 173 140.7 200 159.3 200 175 173 150 173" style="fill:rgba(255,255,255,.33);"/>
</svg>';
// phpcs:enable HM.Security.EscapeOutput.OutputNotEscaped

View file

@ -7,8 +7,6 @@
namespace FAIR\Importers;
use WP_Error;
/**
* Bootstrap.
*/

View file

@ -11,6 +11,9 @@ use Fragen\Git_Updater;
const NS_SEPARATOR = '\\';
/**
* Bootstrap.
*/
function bootstrap() {
// Prevent accidental re-initialization of the plugin.
static $did_init = false;
@ -22,7 +25,7 @@ function bootstrap() {
register_class_path( __NAMESPACE__, __DIR__ . '/inc' );
// Modules:
// Modules.
Avatars\bootstrap();
Credits\bootstrap();
Dashboard_Widgets\bootstrap();

View file

@ -1,4 +1,9 @@
<?php
/**
* Replaces Pingomatic with IndexNow.
*
* @package FAIR
*/
namespace FAIR\Pings;
@ -81,17 +86,18 @@ function handle_key_file_request() {
$key = get_indexnow_key();
if ( ! $key || $key !== get_query_var( 'fair_indexnow_key' ) ) {
wp_die( 'Invalid key: ' . get_query_var( 'fair_indexnow_key' ), 'IndexNow Key Error', [ 'response' => 403 ] );
$error = 'Invalid key: ' . get_query_var( 'fair_indexnow_key' );
wp_die( esc_html( $error ), 'IndexNow Key Error', [ 'response' => 403 ] );
return;
}
// Set the content type to text/plain
// Set the content type to text/plain.
header( 'Content-Type: text/plain' );
header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', time() + YEAR_IN_SECONDS ) . ' GMT' );
header( 'Cache-Control: public, max-age=' . YEAR_IN_SECONDS );
// Output the key
echo $key;
// Output the key.
echo esc_html( $key );
exit;
}
@ -157,12 +163,14 @@ function ping_indexnow( $new_status, $old_status, $post ) : void {
// Log the response for debugging. As per https://www.indexnow.org/documentation#response, either 200 or 202 is acceptable.
if ( is_wp_error( $response ) ) {
/* phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r */
error_log( 'IndexNow ping failed: ' . $response->get_error_message() . print_r( $request, true ) );
return;
}
$status = wp_remote_retrieve_response_code( $response );
if ( ! in_array( $status, [ 200, 202 ], true ) ) {
/* phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r */
error_log( 'IndexNow ping failed: ' . $status . print_r( $request, true ) );
}
}

View file

@ -7,5 +7,8 @@
namespace FAIR\Repositories;
/**
* Bootstrap.
*/
function bootstrap() {
}

View file

@ -23,8 +23,8 @@ function bootstrap() {
* Replace the call to retrieve generated salt values.
*
* @param bool|array $value Filtered value, or false to proceed.
* @param array $args
* @param string $url
* @param array $args HTTP request arguments.
* @param string $url The request URL.
* @return bool|array Replaced value, or false to proceed.
*/
function replace_salt_generation_via_api( $value, $args, $url ) {
@ -179,7 +179,7 @@ function generate_string_via_mt_rand() {
for ( $i = 0; $i < 64; $i++ ) {
// Randomly select an index from the character set using mt_rand().
$set_index = mt_rand( 0, strlen( CHARACTER_SET ) - 1 );
$set_index = wp_rand( 0, strlen( CHARACTER_SET ) - 1 );
// Append the character to the string.
$saltgrain .= CHARACTER_SET[ $set_index ];
@ -218,7 +218,7 @@ function generate_string_via_substr() {
for ( $i = 0; $i < 64; $i++ ) {
// Append the character to the string.
$saltgrain .= substr( CHARACTER_SET, mt_rand( 0, strlen( CHARACTER_SET ) - 1 ), 1 );
$saltgrain .= substr( CHARACTER_SET, wp_rand( 0, strlen( CHARACTER_SET ) - 1 ), 1 );
}
return esc_attr( $saltgrain );

View file

@ -7,6 +7,8 @@
namespace FAIR\Settings;
/* phpcs:disable Squiz.PHP.EmbeddedPhp.ContentBeforeOpen, Squiz.PHP.EmbeddedPhp.ContentAfterOpen, Squiz.PHP.EmbeddedPhp.ContentBeforeEnd */
/**
* Bootstrap.
*/
@ -69,17 +71,25 @@ function render_settings_page() {
?>
<div class="wrap fair-settings">
<h1><?php esc_html_e( 'FAIR Settings', 'fair' ); ?></h1>
<p><?php printf(
<p>
<?php
printf(
/* translators: %s: AspirePress website URL */
esc_html__( 'Your site is now disconnected from WordPress.org managed servers and uses a dedicated mirror from %s. In the coming weeks and months, we\'ll be making the mirror configurable and add in the ability to add your own.', 'fair' ),
'<a href="https://aspirepress.org/">AspirePress</a>'
); ?></p>
<p><?php printf(
);
?>
</p>
<p>
<?php
printf(
/* translators: %1$s: FAIR GitHub URL, %2$s: FAQ URL */
esc_html__( 'If you want to learn more, check out the %1$s and our %2$s.', 'fair' ),
'<a href="https://fair.pm/">FAIR GitHub</a>',
'<a href="https://github.com/fairpm/tsc/blob/main/faqs/README.md">FAQ</a>'
); ?></p>
);
?>
</p>
<form method="post">
<?php wp_nonce_field( 'fair_save_settings' ); ?>
<?php render_avatar_setting( $settings ); ?>
@ -138,7 +148,7 @@ function save_settings() : bool {
return false;
}
$raw = is_array( $_POST['fair_settings'] ) ? $_POST['fair_settings'] : [];
$raw = is_array( $_POST['fair_settings'] ) ? array_map( 'sanitize_text_field', wp_unslash( $_POST['fair_settings'] ) ) : [];
$settings = get_option( 'fair_settings', [] );

View file

@ -61,8 +61,8 @@ function bootstrap() {
* Replace the browser version check.
*
* @param bool|array $value Filtered value, or false to proceed.
* @param array $args
* @param string $url
* @param array $args HTTP request arguments.
* @param string $url The request URL.
* @return bool|array Replaced value, or false to proceed.
*/
function replace_browser_version_check( $value, $args, $url ) {

View file

@ -4,6 +4,8 @@
},
"scripts": {
"env": "wp-env",
"lint:php": "wp-env run cli --env-cwd=wp-content/plugins/plugin composer run lint",
"format:php": "wp-env run cli --env-cwd=wp-content/plugins/plugin composer run format",
"test:php:install-deps": "wp-env run tests-cli --env-cwd=wp-content/plugins/plugin composer install",
"test:php": "wp-env run tests-cli --env-cwd=wp-content/plugins/plugin composer run test",
"test:php:multisite": "wp-env run tests-cli --env-cwd=wp-content/plugins/plugin composer run test:multisite",

75
phpcs.xml.dist Normal file
View file

@ -0,0 +1,75 @@
<?xml version="1.0"?>
<ruleset name="FAIR coding standards">
<description>Coding standards for the FAIR plugin</description>
<!-- Only scan PHP files. -->
<arg name="extensions" value="php" />
<!-- Whenever possible, cache the scan results and re-use those for unchanged files on the next scan. -->
<arg name="cache" value=".cache/phpcs.json"/>
<!-- Path to strip from the front of file paths inside reports (displays shorter paths) -->
<arg name="basepath" value="." />
<!-- Show sniff codes in all reports. -->
<arg value="ps"/>
<!-- Set a minimum PHP version for PHPCompatibility -->
<config name="testVersion" value="7.4-" />
<!-- Set the minimum support WordPress version -->
<config name="minimum_supported_wp_version" value="5.4"/>
<!-- Set the text domain -->
<config name="text_domain" value="fair"/>
<!-- Use HM Coding Standards -->
<rule ref="vendor/humanmade/coding-standards">
<!-- Deprecated in PHPCS. Removal in 4.0.0 -->
<exclude name="Generic.Functions.CallTimePassByReference" />
<!-- Scanning CSS/JS files is deprecated in PHPCS. Removal in 4.0.0 -->
<exclude name="HM.Debug.ESLint" />
</rule>
<!-- Exclude third-party dependencies -->
<exclude-pattern>inc/updater/*</exclude-pattern>
<!-- Exclude language files -->
<exclude-pattern>languages/*</exclude-pattern>
<!-- Exclude sample files -->
<exclude-pattern>wp-tests-config-sample\.php</exclude-pattern>
<!-- Exclude the main plugin file from namespace file rules -->
<rule ref="HM.Files.FunctionFileName.WrongFile">
<exclude-pattern>plugin\.php</exclude-pattern>
</rule>
<!-- Exclude the unit tests from the file name rules -->
<rule ref="WordPress.Files.FileName">
<exclude-pattern>tests/phpunit/*</exclude-pattern>
</rule>
<rule ref="HM.Files.ClassFileName">
<exclude-pattern>tests/phpunit/*</exclude-pattern>
</rule>
<rule ref="HM.Functions.NamespacedFunctions.MissingNamespace">
<exclude-pattern>inc/icons/svg\.php</exclude-pattern>
</rule>
<!-- Exclude the unit tests from the namespace rules -->
<rule ref="HM.Functions.NamespacedFunctions.MissingNamespace">
<exclude-pattern>tests/phpunit/*</exclude-pattern>
</rule>
<rule ref="PSR1.Classes.ClassDeclaration.MissingNamespace">
<exclude-pattern>tests/phpunit/*</exclude-pattern>
</rule>
<!-- Exclude some files from the side effects rules -->
<rule ref="PSR1.Files.SideEffects">
<exclude-pattern>plugin\.php</exclude-pattern>
<exclude-pattern>inc/icons/svg\.php</exclude-pattern>
<exclude-pattern>tests/phpunit/bootstrap\.php</exclude-pattern>
</rule>
</ruleset>

View file

@ -37,7 +37,11 @@ require_once __DIR__ . '/inc/version-check/namespace.php';
// External dependencies.
require_once __DIR__ . '/inc/updater/class-lite.php';
// Load translations.
/**
* Load translations.
*
* @return void
*/
function load_textdomain() {
load_plugin_textdomain( 'fair', false, dirname( plugin_basename( PLUGIN_FILE ) ) . '/languages' );
}