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 # Main
.git/ .git/
.github/ .github/
.cache/
bin/ bin/
node_modules/ node_modules/
tests/ tests/
phpunit.xml.dist phpunit.xml.dist
wp-tests-config-sample.php wp-tests-config-sample.php
phpcs.xml.dist
composer.json composer.json
composer.lock composer.lock
package.json package.json

2
.gitattributes vendored
View file

@ -3,10 +3,12 @@
/.git export-ignore /.git export-ignore
/.github export-ignore /.github export-ignore
/bin export-ignore /bin export-ignore
/.cache export-ignore
/node_modules export-ignore /node_modules export-ignore
/tests export-ignore /tests export-ignore
phpunit.xml.dist export-ignore phpunit.xml.dist export-ignore
wp-tests-config-sample.php export-ignore wp-tests-config-sample.php export-ignore
phpcs.xml.dist export-ignore
composer.json export-ignore composer.json export-ignore
composer.lock export-ignore composer.lock export-ignore
package.json 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/ /node_modules/
*.bak *.bak
/.vscode /.vscode
/vendor/ /vendor/
.phpunit.result.cache .phpunit.result.cache
phpunit.xml phpunit.xml
phpcs.xml
/tests/phpunit/cache /tests/phpunit/cache
/tests/phpunit/coverage /tests/phpunit/coverage
wp-tests-config.php wp-tests-config.php
# Track placeholders so that empty directories stay in the repo.
!.gitkeep

View file

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

View file

@ -13,8 +13,8 @@ const DEFAULT_EMOJI_BASE = 'https://cdn.jsdelivr.net/gh/jdecked/twemoji@15.1.0/a
* Bootstrap. * Bootstrap.
*/ */
function bootstrap() { function bootstrap() {
add_filter( 'emoji_url', __NAMESPACE__ . '\\replace_emoji_url' ); add_filter( 'emoji_url', __NAMESPACE__ . '\\replace_emoji_url' );
add_filter( 'emoji_svg_url', __NAMESPACE__ . '\\replace_emoji_svg_url' ); add_filter( 'emoji_svg_url', __NAMESPACE__ . '\\replace_emoji_svg_url' );
} }
/** /**
@ -36,7 +36,7 @@ function get_emoji_base_url() : string {
* @return string The base URL. * @return string The base URL.
*/ */
function replace_emoji_url() { function replace_emoji_url() {
return get_emoji_base_url() . '72x72/'; return get_emoji_base_url() . '72x72/';
} }
/** /**
@ -45,5 +45,5 @@ function replace_emoji_url() {
* @return string The base URL. * @return string The base URL.
*/ */
function replace_emoji_svg_url() { function replace_emoji_svg_url() {
return get_emoji_base_url() . 'svg/'; return get_emoji_base_url() . 'svg/';
} }

View file

@ -1,4 +1,9 @@
<?php <?php
/**
* Adds a local source for avatars.
*
* @package FAIR
*/
namespace FAIR\Avatars; namespace FAIR\Avatars;
@ -41,11 +46,11 @@ function enqueue_media_scripts( $hook_suffix ) {
} }
// Grab the user ID to pass along for alt text. // 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; $display_name = get_user( $user_id )->display_name;
wp_enqueue_media(); wp_enqueue_media();
wp_enqueue_script( 'fair-avatars', esc_url( plugin_dir_url( \FAIR\PLUGIN_FILE ) . 'assets/js/fair-avatars.js' ), ['jquery','wp-a11y','wp-i18n'], \FAIR\VERSION, true ); wp_enqueue_script( 'fair-avatars', esc_url( plugin_dir_url( \FAIR\PLUGIN_FILE ) . 'assets/js/fair-avatars.js' ), [ 'jquery', 'wp-a11y', 'wp-i18n' ], \FAIR\VERSION, true );
wp_localize_script( 'fair-avatars', 'fairAvatars', wp_localize_script( 'fair-avatars', 'fairAvatars',
[ [
'defaultImg' => generate_default_avatar( $display_name ), 'defaultImg' => generate_default_avatar( $display_name ),
@ -54,7 +59,7 @@ function enqueue_media_scripts( $hook_suffix ) {
); );
// Some inline CSS for our fields. // Some inline CSS for our fields.
$setup_css = ' $setup_css = '
span.fair-avatar-desc { span.fair-avatar-desc {
display: block; display: block;
margin-top: 5px; margin-top: 5px;
@ -105,6 +110,8 @@ function add_avatar_upload_field( $description, $profile_user ) {
* @param int $user_id User ID. * @param int $user_id User ID.
*/ */
function save_avatar_upload( $user_id ) { function save_avatar_upload( $user_id ) {
check_admin_referer( 'update-user_' . $user_id );
if ( ! current_user_can( 'edit_user', $user_id ) ) { if ( ! current_user_can( 'edit_user', $user_id ) ) {
return; return;
} }
@ -137,7 +144,7 @@ function save_avatar_upload( $user_id ) {
function filter_avatar( $avatar, $id_or_email, $size, $default, $alt, $args ) { function filter_avatar( $avatar, $id_or_email, $size, $default, $alt, $args ) {
$avatar_url = get_avatar_url( $id_or_email, $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'] ) ) { if ( ! empty( $args['class'] ) ) {
$class = array_merge( $class, (array) $args['class'] ); $class = array_merge( $class, (array) $args['class'] );
} }

View file

@ -1,4 +1,9 @@
<?php <?php
/**
* Changes events to use The WP World, and news to use FAIR Planet.
*
* @package FAIR
*/
namespace FAIR\Dashboard_Widgets; namespace FAIR\Dashboard_Widgets;
@ -31,7 +36,6 @@ function bootstrap() {
/** /**
* Fires after core widgets for the admin dashboard have been registered. * Fires after core widgets for the admin dashboard have been registered.
*
*/ */
function on_dashboard_setup() : void { function on_dashboard_setup() : void {
// Swap the "Primary" dashboard widget's callback. // 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'] ); $url = add_query_arg( 'ref', 'fair-dashboard', $event['camp_website_url'] ?? $event['link'] );
$events[] = array( $events[] = [
'type' => 'event', 'type' => 'event',
'title' => $event['title']['rendered'], 'title' => $event['title']['rendered'],
'url' => $url, 'url' => $url,
@ -128,7 +132,7 @@ function get_community_events() {
'latitude' => $event['camp_lat'] ?? 0, 'latitude' => $event['camp_lat'] ?? 0,
'longitude' => $event['camp_lng'] ?? 0, 'longitude' => $event['camp_lng'] ?? 0,
], ],
); ];
} }
// Resort events by start date. // 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>', '<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. */ /* 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' ) ), esc_url( _x( 'https://fair.pm/', 'Events and News dashboard widget', 'fair' ) ),
__( 'News' ), __( 'News', 'fair' ),
/* translators: Hidden accessibility text. */ /* 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/', 'https://thewp.world/events/',
__( 'Events (by The WP World)', 'fair' ), __( 'Events (by The WP World)', 'fair' ),
/* translators: Hidden accessibility text. */ /* translators: Hidden accessibility text. */
__( '(opens in a new tab)' ) __( '(opens in a new tab)', 'fair' )
); );
?> ?>
</p> </p>
@ -240,6 +244,7 @@ function set_help_content_fair_planet_urls() : void {
$planet_fair_url = rtrim( $planet_fair_url, '/' ); $planet_fair_url = rtrim( $planet_fair_url, '/' );
$new_tab_content = preg_replace( $new_tab_content = preg_replace(
/* phpcs:ignore WordPress.WP.CapitalPDangit.Misspelled */
'/https?:\/\/planet\.wordpress\.org/', '/https?:\/\/planet\.wordpress\.org/',
$planet_fair_url, $planet_fair_url,
$tab['content'], $tab['content'],

View file

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

View file

@ -11,7 +11,7 @@ namespace FAIR\Disable_Openverse;
* Bootstrap the module functionality. * Bootstrap the module functionality.
*/ */
function bootstrap() { function bootstrap() {
add_filter( 'block_editor_settings_all', __NAMESPACE__ . '\\disable_openverse_block_editor_settings' ); add_filter( 'block_editor_settings_all', __NAMESPACE__ . '\\disable_openverse_block_editor_settings' );
} }
/** /**
@ -21,6 +21,6 @@ function bootstrap() {
* @return array The modified block editor settings. * @return array The modified block editor settings.
*/ */
function disable_openverse_block_editor_settings( array $settings ) : array { function disable_openverse_block_editor_settings( array $settings ) : array {
$settings['enableOpenverseMediaCategory'] = false; $settings['enableOpenverseMediaCategory'] = false;
return $settings; return $settings;
} }

View file

@ -8,7 +8,6 @@
namespace FAIR\Icons; namespace FAIR\Icons;
use FAIR; use FAIR;
use stdClass;
/** /**
* Bootstrap * Bootstrap
@ -40,7 +39,7 @@ function set_default_icon( $transient ) {
* @return string * @return string
*/ */
function set_random_color() { 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; return $rand;
} }

View file

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

View file

@ -7,8 +7,6 @@
namespace FAIR\Importers; namespace FAIR\Importers;
use WP_Error;
/** /**
* Bootstrap. * Bootstrap.
*/ */
@ -84,50 +82,50 @@ function get_popular_importers_gte_46() {
return [ return [
// slug => name, description, plugin slug, and register_importer() slug. // slug => name, description, plugin slug, and register_importer() slug.
'blogger' => [ 'blogger' => [
'name' => 'Blogger' , 'name' => 'Blogger',
'description' => 'Import posts, comments, and users from a Blogger blog.' , 'description' => 'Import posts, comments, and users from a Blogger blog.',
'plugin-slug' => 'blogger-importer', 'plugin-slug' => 'blogger-importer',
'importer-id' => 'blogger', 'importer-id' => 'blogger',
], ],
'wpcat2tag' => [ 'wpcat2tag' => [
'name' => 'Categories and Tags Converter' , 'name' => 'Categories and Tags Converter',
'description' => 'Convert existing categories to tags or tags to categories, selectively.' , 'description' => 'Convert existing categories to tags or tags to categories, selectively.',
'plugin-slug' => 'wpcat2tag-importer', 'plugin-slug' => 'wpcat2tag-importer',
'importer-id' => 'wp-cat2tag', 'importer-id' => 'wp-cat2tag',
], ],
'livejournal' => [ 'livejournal' => [
'name' => 'LiveJournal' , 'name' => 'LiveJournal',
'description' => 'Import posts from LiveJournal using their API.' , 'description' => 'Import posts from LiveJournal using their API.',
'plugin-slug' => 'livejournal-importer', 'plugin-slug' => 'livejournal-importer',
'importer-id' => 'livejournal', 'importer-id' => 'livejournal',
], ],
'movabletype' => [ 'movabletype' => [
'name' => 'Movable Type and TypePad' , 'name' => 'Movable Type and TypePad',
'description' => 'Import posts and comments from a Movable Type or TypePad blog.' , 'description' => 'Import posts and comments from a Movable Type or TypePad blog.',
'plugin-slug' => 'movabletype-importer', 'plugin-slug' => 'movabletype-importer',
'importer-id' => 'mt', 'importer-id' => 'mt',
], ],
'opml' => [ 'opml' => [
'name' => 'Blogroll' , 'name' => 'Blogroll',
'description' => 'Import links in OPML format.' , 'description' => 'Import links in OPML format.',
'plugin-slug' => 'opml-importer', 'plugin-slug' => 'opml-importer',
'importer-id' => 'opml', 'importer-id' => 'opml',
], ],
'rss' => [ 'rss' => [
'name' => 'RSS' , 'name' => 'RSS',
'description' => 'Import posts from an RSS feed.' , 'description' => 'Import posts from an RSS feed.',
'plugin-slug' => 'rss-importer', 'plugin-slug' => 'rss-importer',
'importer-id' => 'rss', 'importer-id' => 'rss',
], ],
'tumblr' => [ 'tumblr' => [
'name' => 'Tumblr' , 'name' => 'Tumblr',
'description' => 'Import posts &amp; media from Tumblr using their API.' , 'description' => 'Import posts &amp; media from Tumblr using their API.',
'plugin-slug' => 'tumblr-importer', 'plugin-slug' => 'tumblr-importer',
'importer-id' => 'tumblr', 'importer-id' => 'tumblr',
], ],
'wordpress' => [ 'wordpress' => [
'name' => 'WordPress', 'name' => 'WordPress',
'description' => 'Import posts, pages, comments, custom fields, categories, and tags from a WordPress export file.' , 'description' => 'Import posts, pages, comments, custom fields, categories, and tags from a WordPress export file.',
'plugin-slug' => 'wordpress-importer', 'plugin-slug' => 'wordpress-importer',
'importer-id' => 'wordpress', 'importer-id' => 'wordpress',
], ],
@ -147,50 +145,50 @@ function get_popular_importers_lt_46() {
return [ return [
// slug => name, description, plugin slug, and register_importer() slug. // slug => name, description, plugin slug, and register_importer() slug.
'blogger' => [ 'blogger' => [
'name' => 'Blogger' , 'name' => 'Blogger',
'description' => 'Install the Blogger importer to import posts, comments, and users from a Blogger blog.' , 'description' => 'Install the Blogger importer to import posts, comments, and users from a Blogger blog.',
'plugin-slug' => 'blogger-importer', 'plugin-slug' => 'blogger-importer',
'importer-id' => 'blogger', 'importer-id' => 'blogger',
], ],
'wpcat2tag' => [ 'wpcat2tag' => [
'name' => 'Categories and Tags Converter' , 'name' => 'Categories and Tags Converter',
'description' => 'Install the category/tag converter to convert existing categories to tags or tags to categories, selectively.' , 'description' => 'Install the category/tag converter to convert existing categories to tags or tags to categories, selectively.',
'plugin-slug' => 'wpcat2tag-importer', 'plugin-slug' => 'wpcat2tag-importer',
'importer-id' => 'wpcat2tag', 'importer-id' => 'wpcat2tag',
], ],
'livejournal' => [ 'livejournal' => [
'name' => 'LiveJournal' , 'name' => 'LiveJournal',
'description' => 'Install the LiveJournal importer to import posts from LiveJournal using their API.' , 'description' => 'Install the LiveJournal importer to import posts from LiveJournal using their API.',
'plugin-slug' => 'livejournal-importer', 'plugin-slug' => 'livejournal-importer',
'importer-id' => 'livejournal', 'importer-id' => 'livejournal',
], ],
'movabletype' => [ 'movabletype' => [
'name' => 'Movable Type and TypePad' , 'name' => 'Movable Type and TypePad',
'description' => 'Install the Movable Type importer to import posts and comments from a Movable Type or TypePad blog.' , 'description' => 'Install the Movable Type importer to import posts and comments from a Movable Type or TypePad blog.',
'plugin-slug' => 'movabletype-importer', 'plugin-slug' => 'movabletype-importer',
'importer-id' => 'mt', 'importer-id' => 'mt',
], ],
'opml' => [ 'opml' => [
'name' => 'Blogroll' , 'name' => 'Blogroll',
'description' => 'Install the blogroll importer to import links in OPML format.' , 'description' => 'Install the blogroll importer to import links in OPML format.',
'plugin-slug' => 'opml-importer', 'plugin-slug' => 'opml-importer',
'importer-id' => 'opml', 'importer-id' => 'opml',
], ],
'rss' => [ 'rss' => [
'name' => 'RSS' , 'name' => 'RSS',
'description' => 'Install the RSS importer to import posts from an RSS feed.' , 'description' => 'Install the RSS importer to import posts from an RSS feed.',
'plugin-slug' => 'rss-importer', 'plugin-slug' => 'rss-importer',
'importer-id' => 'rss', 'importer-id' => 'rss',
], ],
'tumblr' => [ 'tumblr' => [
'name' => 'Tumblr' , 'name' => 'Tumblr',
'description' => 'Install the Tumblr importer to import posts &amp; media from Tumblr using their API.' , 'description' => 'Install the Tumblr importer to import posts &amp; media from Tumblr using their API.',
'plugin-slug' => 'tumblr-importer', 'plugin-slug' => 'tumblr-importer',
'importer-id' => 'tumblr', 'importer-id' => 'tumblr',
], ],
'wordpress' => [ 'wordpress' => [
'name' => 'WordPress', 'name' => 'WordPress',
'description' => 'Install the WordPress importer to import posts, pages, comments, custom fields, categories, and tags from a WordPress export file.' , 'description' => 'Install the WordPress importer to import posts, pages, comments, custom fields, categories, and tags from a WordPress export file.',
'plugin-slug' => 'wordpress-importer', 'plugin-slug' => 'wordpress-importer',
'importer-id' => 'wordpress', 'importer-id' => 'wordpress',
], ],

View file

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

View file

@ -1,4 +1,9 @@
<?php <?php
/**
* Replaces Pingomatic with IndexNow.
*
* @package FAIR
*/
namespace FAIR\Pings; namespace FAIR\Pings;
@ -6,12 +11,12 @@ namespace FAIR\Pings;
* Bootstrap. * Bootstrap.
*/ */
function bootstrap() { function bootstrap() {
add_filter( 'pre_option_ping_sites', __NAMESPACE__ . '\\remove_pingomatic_from_ping_sites' ); add_filter( 'pre_option_ping_sites', __NAMESPACE__ . '\\remove_pingomatic_from_ping_sites' );
add_filter( 'query_vars', __NAMESPACE__ . '\\register_query_vars' ); add_filter( 'query_vars', __NAMESPACE__ . '\\register_query_vars' );
add_action( 'init', __NAMESPACE__ . '\\get_indexnow_key' ); add_action( 'init', __NAMESPACE__ . '\\get_indexnow_key' );
add_action( 'init', __NAMESPACE__ . '\\add_key_rewrite_rule' ); add_action( 'init', __NAMESPACE__ . '\\add_key_rewrite_rule' );
add_action( 'template_redirect', __NAMESPACE__ . '\\handle_key_file_request' ); add_action( 'template_redirect', __NAMESPACE__ . '\\handle_key_file_request' );
add_action( 'transition_post_status', __NAMESPACE__ . '\\ping_indexnow', 10, 3 ); add_action( 'transition_post_status', __NAMESPACE__ . '\\ping_indexnow', 10, 3 );
} }
/** /**
@ -21,8 +26,8 @@ function bootstrap() {
* @return array Modified array of query vars. * @return array Modified array of query vars.
*/ */
function register_query_vars( $vars ) { function register_query_vars( $vars ) {
$vars[] = 'fair_indexnow_key'; $vars[] = 'fair_indexnow_key';
return $vars; return $vars;
} }
/** /**
@ -31,9 +36,9 @@ function register_query_vars( $vars ) {
* @param string $value The ping_sites option value. * @param string $value The ping_sites option value.
*/ */
function remove_pingomatic_from_ping_sites( $value ) { function remove_pingomatic_from_ping_sites( $value ) {
$value = str_replace( 'http://rpc.pingomatic.com/', '', $value ); $value = str_replace( 'http://rpc.pingomatic.com/', '', $value );
$value = str_replace( "\n\n", "\n", trim( $value, "\n" ) ); $value = str_replace( "\n\n", "\n", trim( $value, "\n" ) );
return $value; return $value;
} }
/** /**
@ -42,57 +47,58 @@ function remove_pingomatic_from_ping_sites( $value ) {
* @return string Unique site key. * @return string Unique site key.
*/ */
function get_indexnow_key() : string { function get_indexnow_key() : string {
$key = get_option( 'fair_indexnow_key' ); $key = get_option( 'fair_indexnow_key' );
if ( ! $key ) { if ( ! $key ) {
// Generate a random key that meets IndexNow requirements. // Generate a random key that meets IndexNow requirements.
// Must be 8-128 hexadecimal characters (a-f, 0-9). // Must be 8-128 hexadecimal characters (a-f, 0-9).
$key = strtolower( wp_generate_password( 40, false, false ) ); $key = strtolower( wp_generate_password( 40, false, false ) );
update_option( 'fair_indexnow_key', $key ); update_option( 'fair_indexnow_key', $key );
// Flush the rewrite rules. // Flush the rewrite rules.
flush_rewrite_rules(); flush_rewrite_rules();
} }
return $key; return $key;
} }
/** /**
* Add rewrite rule for the IndexNow key file. * Add rewrite rule for the IndexNow key file.
*/ */
function add_key_rewrite_rule() { function add_key_rewrite_rule() {
$key = get_indexnow_key(); $key = get_indexnow_key();
add_rewrite_rule( add_rewrite_rule(
'fair-indexnow-' . $key . '$', 'fair-indexnow-' . $key . '$',
'index.php?fair_indexnow_key=' . $key, 'index.php?fair_indexnow_key=' . $key,
'top' 'top'
); );
} }
/** /**
* Handle the IndexNow key file request. * Handle the IndexNow key file request.
*/ */
function handle_key_file_request() { function handle_key_file_request() {
if ( ! get_query_var( 'fair_indexnow_key' ) ) { if ( ! get_query_var( 'fair_indexnow_key' ) ) {
return; return;
} }
$key = get_indexnow_key(); $key = get_indexnow_key();
if ( ! $key || $key !== get_query_var( 'fair_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' );
return; 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( 'Content-Type: text/plain' );
header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', time() + YEAR_IN_SECONDS ) . ' GMT' ); header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', time() + YEAR_IN_SECONDS ) . ' GMT' );
header( 'Cache-Control: public, max-age=' . YEAR_IN_SECONDS ); header( 'Cache-Control: public, max-age=' . YEAR_IN_SECONDS );
// Output the key // Output the key.
echo $key; echo esc_html( $key );
exit; exit;
} }
/** /**
@ -103,66 +109,68 @@ function handle_key_file_request() {
* @param WP_Post $post Post object. * @param WP_Post $post Post object.
*/ */
function ping_indexnow( $new_status, $old_status, $post ) : void { function ping_indexnow( $new_status, $old_status, $post ) : void {
// Only ping for published posts. // Only ping for published posts.
if ( 'publish' !== $new_status ) { if ( 'publish' !== $new_status ) {
return; return;
} }
// Skip revisions and autosaves. // Skip revisions and autosaves.
if ( wp_is_post_revision( $post ) || wp_is_post_autosave( $post ) ) { if ( wp_is_post_revision( $post ) || wp_is_post_autosave( $post ) ) {
return; return;
} }
// Skip non-public post types. // Skip non-public post types.
if ( ! is_post_type_viewable( $post->post_type ) ) { if ( ! is_post_type_viewable( $post->post_type ) ) {
return; return;
} }
$key = get_option( 'fair_indexnow_key' ); $key = get_option( 'fair_indexnow_key' );
if ( ! $key ) { if ( ! $key ) {
return; return;
} }
$url = get_permalink( $post ); $url = get_permalink( $post );
if ( ! $url ) { if ( ! $url ) {
return; return;
} }
// Allow for filtering the URL list. // Allow for filtering the URL list.
$url_list = apply_filters( 'fair_indexnow_url_list', [ $url ] ); $url_list = apply_filters( 'fair_indexnow_url_list', [ $url ] );
// Allow for filtering the key location. // Allow for filtering the key location.
$key_location = apply_filters( 'fair_indexnow_key_location', trailingslashit( home_url( 'fair-indexnow-' . $key ) ) ); $key_location = apply_filters( 'fair_indexnow_key_location', trailingslashit( home_url( 'fair-indexnow-' . $key ) ) );
// The "false" on the end of the x-source-info header determines whether this is a manual submission or not. // The "false" on the end of the x-source-info header determines whether this is a manual submission or not.
$data = [ $data = [
'host' => wp_parse_url( home_url(), PHP_URL_HOST ), 'host' => wp_parse_url( home_url(), PHP_URL_HOST ),
'key' => $key, 'key' => $key,
'keyLocation' => $key_location, 'keyLocation' => $key_location,
'urlList' => $url_list, 'urlList' => $url_list,
]; ];
$request = [ $request = [
'body' => wp_json_encode( $data, JSON_UNESCAPED_SLASHES ), 'body' => wp_json_encode( $data, JSON_UNESCAPED_SLASHES ),
'headers' => [ 'headers' => [
'Content-Type' => 'application/json; charset=utf-8', 'Content-Type' => 'application/json; charset=utf-8',
'x-source-info' => 'https://example.com/fair-wp/indexnow/false', // TODO: replace example.com with the domain we end up using. 'x-source-info' => 'https://example.com/fair-wp/indexnow/false', // TODO: replace example.com with the domain we end up using.
], ],
]; ];
// Ping IndexNow. // Ping IndexNow.
$response = wp_remote_post( $response = wp_remote_post(
'https://api.indexnow.org/indexnow', 'https://api.indexnow.org/indexnow',
$request $request
); );
// Log the response for debugging. As per https://www.indexnow.org/documentation#response, either 200 or 202 is acceptable. // Log the response for debugging. As per https://www.indexnow.org/documentation#response, either 200 or 202 is acceptable.
if ( is_wp_error( $response ) ) { if ( is_wp_error( $response ) ) {
error_log( 'IndexNow ping failed: ' . $response->get_error_message() . print_r( $request, true ) ); /* phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r */
return; error_log( 'IndexNow ping failed: ' . $response->get_error_message() . print_r( $request, true ) );
} return;
}
$status = wp_remote_retrieve_response_code( $response ); $status = wp_remote_retrieve_response_code( $response );
if ( ! in_array( $status, [ 200, 202 ], true ) ) { if ( ! in_array( $status, [ 200, 202 ], true ) ) {
error_log( 'IndexNow ping failed: ' . $status . print_r( $request, 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; namespace FAIR\Repositories;
/**
* Bootstrap.
*/
function bootstrap() { function bootstrap() {
} }

View file

@ -23,8 +23,8 @@ function bootstrap() {
* Replace the call to retrieve generated salt values. * Replace the call to retrieve generated salt values.
* *
* @param bool|array $value Filtered value, or false to proceed. * @param bool|array $value Filtered value, or false to proceed.
* @param array $args * @param array $args HTTP request arguments.
* @param string $url * @param string $url The request URL.
* @return bool|array Replaced value, or false to proceed. * @return bool|array Replaced value, or false to proceed.
*/ */
function replace_salt_generation_via_api( $value, $args, $url ) { function replace_salt_generation_via_api( $value, $args, $url ) {
@ -65,13 +65,13 @@ function get_salt_generation_response() {
function generate_salt_response_body() { function generate_salt_response_body() {
// Grab my key names. // Grab my key names.
$get_key_names = define_salt_keynames(); $get_key_names = define_salt_keynames();
$salt_defines = ''; $salt_defines = '';
// Now loop my key names and add a salt to each one. // Now loop my key names and add a salt to each one.
foreach ( $get_key_names as $keyname ) { foreach ( $get_key_names as $keyname ) {
$salt_defines .= 'define( \'' . $keyname . '\', \'' . generate_salt_string() . '\' );' . "\n"; $salt_defines .= 'define( \'' . $keyname . '\', \'' . generate_salt_string() . '\' );' . "\n";
} }
// Send back the string. // Send back the string.
@ -137,7 +137,7 @@ function generate_string_via_random_int() {
// Set a max amount. // Set a max amount.
$define_max = mb_strlen( CHARACTER_SET, '8bit' ) - 1; $define_max = mb_strlen( CHARACTER_SET, '8bit' ) - 1;
$saltgrain = ''; $saltgrain = '';
// Loop through to generate each character of the string. // Loop through to generate each character of the string.
for ( $i = 0; $i < 64; ++$i ) { for ( $i = 0; $i < 64; ++$i ) {
@ -155,13 +155,13 @@ function generate_string_via_random_int() {
function generate_string_via_openssl_random() { function generate_string_via_openssl_random() {
// Generate some bytes to begin. // Generate some bytes to begin.
$set_bytes = openssl_random_pseudo_bytes( 138 ); $set_bytes = openssl_random_pseudo_bytes( 138 );
// Now encode it to make sure it's a usable string. // Now encode it to make sure it's a usable string.
$saltshaker = base64_encode( $set_bytes ); $saltshaker = base64_encode( $set_bytes );
// Establish the first 64 characters. // Establish the first 64 characters.
$saltgrain = substr( $saltshaker, 0, 64 ); $saltgrain = substr( $saltshaker, 0, 64 );
return esc_attr( $saltgrain ); return esc_attr( $saltgrain );
} }
@ -173,13 +173,13 @@ function generate_string_via_openssl_random() {
*/ */
function generate_string_via_mt_rand() { function generate_string_via_mt_rand() {
$saltgrain = ''; $saltgrain = '';
// Loop through to generate each character of the string. // Loop through to generate each character of the string.
for ( $i = 0; $i < 64; $i++ ) { for ( $i = 0; $i < 64; $i++ ) {
// Randomly select an index from the character set using mt_rand(). // 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. // Append the character to the string.
$saltgrain .= CHARACTER_SET[ $set_index ]; $saltgrain .= CHARACTER_SET[ $set_index ];
@ -212,13 +212,13 @@ function generate_string_via_str_shuffle() {
*/ */
function generate_string_via_substr() { function generate_string_via_substr() {
$saltgrain = ''; $saltgrain = '';
// Loop through to generate each character of the string. // Loop through to generate each character of the string.
for ( $i = 0; $i < 64; $i++ ) { for ( $i = 0; $i < 64; $i++ ) {
// Append the character to the string. // 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 ); return esc_attr( $saltgrain );

View file

@ -7,6 +7,8 @@
namespace FAIR\Settings; namespace FAIR\Settings;
/* phpcs:disable Squiz.PHP.EmbeddedPhp.ContentBeforeOpen, Squiz.PHP.EmbeddedPhp.ContentAfterOpen, Squiz.PHP.EmbeddedPhp.ContentBeforeEnd */
/** /**
* Bootstrap. * Bootstrap.
*/ */
@ -69,17 +71,25 @@ function render_settings_page() {
?> ?>
<div class="wrap fair-settings"> <div class="wrap fair-settings">
<h1><?php esc_html_e( 'FAIR Settings', 'fair' ); ?></h1> <h1><?php esc_html_e( 'FAIR Settings', 'fair' ); ?></h1>
<p><?php printf( <p>
/* translators: %s: AspirePress website URL */ <?php
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' ), printf(
'<a href="https://aspirepress.org/">AspirePress</a>' /* translators: %s: AspirePress website URL */
); ?></p> 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' ),
<p><?php printf( '<a href="https://aspirepress.org/">AspirePress</a>'
/* 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>', </p>
'<a href="https://github.com/fairpm/tsc/blob/main/faqs/README.md">FAQ</a>' <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>
<form method="post"> <form method="post">
<?php wp_nonce_field( 'fair_save_settings' ); ?> <?php wp_nonce_field( 'fair_save_settings' ); ?>
<?php render_avatar_setting( $settings ); ?> <?php render_avatar_setting( $settings ); ?>
@ -138,7 +148,7 @@ function save_settings() : bool {
return false; 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', [] ); $settings = get_option( 'fair_settings', [] );

View file

@ -61,8 +61,8 @@ function bootstrap() {
* Replace the browser version check. * Replace the browser version check.
* *
* @param bool|array $value Filtered value, or false to proceed. * @param bool|array $value Filtered value, or false to proceed.
* @param array $args * @param array $args HTTP request arguments.
* @param string $url * @param string $url The request URL.
* @return bool|array Replaced value, or false to proceed. * @return bool|array Replaced value, or false to proceed.
*/ */
function replace_browser_version_check( $value, $args, $url ) { function replace_browser_version_check( $value, $args, $url ) {
@ -139,7 +139,7 @@ function get_php_branches() {
// Index data by branch. // Index data by branch.
$indexed = []; $indexed = [];
foreach ( $data as $ver ) { foreach ( $data as $ver ) {
if ( empty( $ver['branch' ] ) ) { if ( empty( $ver['branch'] ) ) {
continue; continue;
} }

View file

@ -4,6 +4,8 @@
}, },
"scripts": { "scripts": {
"env": "wp-env", "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: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": "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", "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. // External dependencies.
require_once __DIR__ . '/inc/updater/class-lite.php'; require_once __DIR__ . '/inc/updater/class-lite.php';
// Load translations. /**
* Load translations.
*
* @return void
*/
function load_textdomain() { function load_textdomain() {
load_plugin_textdomain( 'fair', false, dirname( plugin_basename( PLUGIN_FILE ) ) . '/languages' ); load_plugin_textdomain( 'fair', false, dirname( plugin_basename( PLUGIN_FILE ) ) . '/languages' );
} }