Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 34 additions & 7 deletions api.wordpress.org/public_html/core/browse-happy/1.0/index.php
Original file line number Diff line number Diff line change
@@ -1,38 +1,60 @@
<?php
/**
* WordPress.org Browse Happy API endpoint.
*
* This is a standalone, unauthenticated, stateless API endpoint: WordPress is not loaded,
* so request data is never slashed, and there is no session or nonce infrastructure.
*
* phpcs:disable WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
*
* @package BrowseHappy
*/

require dirname( __FILE__ ) . '/parse.php';

// A JSONP callback is a plain JavaScript identifier; anything else is discarded.
$jsonp_filter_args = array(
'options' => array(
'regexp' => '/^[a-zA-Z_][a-zA-Z0-9_]*\z/',
'default' => '',
),
'flags' => FILTER_REQUIRE_SCALAR,
);

$jsonp = '';
if ( ! empty( $_GET['jsonp'] ) ) {
$jsonp = preg_replace( '/[^a-zA-Z0-9_]/', '', $_GET['jsonp'] );
$jsonp = filter_var( $_GET['jsonp'], FILTER_VALIDATE_REGEXP, $jsonp_filter_args );
header( 'Content-Type: application/javascript' );
} else if ( ! empty( $_GET['callback'] ) ) {
$jsonp = preg_replace( '/[^a-zA-Z0-9_]/', '', $_GET['callback'] );
$jsonp = filter_var( $_GET['callback'], FILTER_VALIDATE_REGEXP, $jsonp_filter_args );
header( 'Content-Type: application/javascript' );
}

if ( empty( $_REQUEST['useragent'] ) ) {
if ( empty( $_REQUEST['useragent'] ) || ! is_string( $_REQUEST['useragent'] ) ) {
return;
}

$user_agent = $_REQUEST['useragent'];
$user_agent = filter_var( $_REQUEST['useragent'], FILTER_UNSAFE_RAW, FILTER_FLAG_STRIP_LOW );
$data = browsehappy_parse_user_agent( $user_agent );

// Collect a sample: One out of every 25.
if ( 0 === strpos( $_SERVER['HTTP_USER_AGENT'], 'WordPress/' ) && 1 === rand( 1, 25 ) ) {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Only used for a prefix comparison, never output or stored.
if ( 0 === strpos( $_SERVER['HTTP_USER_AGENT'] ?? '', 'WordPress/' ) && 1 === rand( 1, 25 ) ) {
require( dirname( dirname( dirname( dirname( __FILE__ ) ) ) ) . '/includes/hyperdb/bb-10-hyper-db.php' );
bh_record_data( $user_agent, $data );
}

if ( $jsonp ) {
header( 'Access-Control-Allow-Origin: *' );
echo $jsonp.'('.json_encode($data).')';
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- validated callback, JSON-encoded payload.
echo $jsonp . '(' . json_encode( $data ) . ')';
} elseif ( defined( 'JSON_RESPONSE' ) ) {
header( 'Access-Control-Allow-Origin: *' );
header( 'Content-Type: application/json' );
echo json_encode( $data );
} else {
header( 'Content-Type: text/plain' );
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- serialized payload served as text/plain.
echo serialize( $data );
}

Expand All @@ -46,7 +68,12 @@
function bh_record_data( $ua, $data ) {
global $wpdb;

list( $wp_ver, $url ) = explode( ';', $_SERVER['HTTP_USER_AGENT'], 2 );
// The requesting client's own user agent, which is recorded alongside the reported one.
$client_ua = filter_var( $_SERVER['HTTP_USER_AGENT'] ?? '', FILTER_UNSAFE_RAW, FILTER_FLAG_STRIP_LOW );

/* Core sends `WordPress/{version}; {site url}`, but the URL may be absent. */
list( $wp_ver, $url ) = array_pad( explode( ';', $client_ua, 2 ), 2, '' );

$wp_ver = substr( $wp_ver, 10, 64 );
$url = rtrim( strtolower( trim( $url ) ), '/' );
$pk = md5( $url . '|' . $ua );
Expand Down
24 changes: 19 additions & 5 deletions api.wordpress.org/public_html/core/browse-happy/1.0/test.php
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
<?php

echo htmlspecialchars( $_SERVER['HTTP_USER_AGENT'], ENT_QUOTES ) . "<br/><br/>";
/**
* Browse Happy user agent parser test page.
*
* This is a standalone diagnostic page: WordPress is not loaded, so request data is
* never slashed and the `esc_*()` escaping helpers are unavailable.
*
* phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash
*
* @package BrowseHappy
*/

include dirname( __FILE__ ) . '/parse.php';
$user_agent = filter_var( $_SERVER['HTTP_USER_AGENT'] ?? '', FILTER_UNSAFE_RAW, FILTER_FLAG_STRIP_LOW );

// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- htmlspecialchars() escapes; no esc_html() here.
echo htmlspecialchars( $user_agent, ENT_QUOTES ) . '<br/><br/>';

$output = browsehappy_parse_user_agent( $_SERVER['HTTP_USER_AGENT'] );
$output = browsehappy_parse_user_agent( $user_agent );

foreach ( $output as $k => $v )
echo htmlspecialchars( $k . ' = ' . ( is_bool( $v ) ? (int) $v : $v ), ENT_QUOTES ) . "<br/>";
foreach ( $output as $k => $v ) {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- htmlspecialchars() escapes; no esc_html() here.
echo htmlspecialchars( $k . ' = ' . ( is_bool( $v ) ? (int) $v : $v ), ENT_QUOTES ) . '<br/>';
}
46 changes: 42 additions & 4 deletions api.wordpress.org/public_html/core/credits/index.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
<?php
/**
* WordPress.org Credits API endpoint.
*
* This is a standalone, unauthenticated, stateless API endpoint: WordPress is not loaded,
* so request data is never slashed, and there is no session or nonce infrastructure.
*
* phpcs:disable WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
*
* @package WordPressdotorg\API\Credits
*/

$api_root = dirname( dirname( __DIR__ ) );

Expand Down Expand Up @@ -32,28 +42,56 @@ function like_escape( $text ) {
endif;

if ( ! empty( $_GET['version'] ) ) {
$version = preg_replace( '/^([.0-9]+).*/', '$1', $_GET['version'] );
$version = preg_replace(
'/^([.0-9]+).*/s',
'$1',
filter_var(
$_GET['version'],
FILTER_VALIDATE_REGEXP,
array(
'options' => array(
'regexp' => '/^[0-9][.0-9]*/',
'default' => '',
),
)
)
);
} elseif ( 'cli' == php_sapi_name() && isset( $argv[1] ) ) {
$version = preg_replace( '/^([.0-9]+).*/', '$1', $argv[1] );
} else {
$version = WP_CORE_LATEST_RELEASE;
}

// A WP locale, e.g. `de_DE_formal` or `es_419`.
$requested_locale = isset( $_GET['locale'] ) ? filter_var(
$_GET['locale'],
FILTER_VALIDATE_REGEXP,
array(
'options' => array(
'regexp' => '/^[A-Za-z0-9_-]+\z/',
'default' => '',
),
)
) : '';

if (
! is_string( $version ) ||
version_compare( $version, '3.2', '<' ) ||
( isset( $_GET['locale'] ) && ! is_string( $_GET['locale'] ) )
( isset( $_GET['locale'] ) && ! is_string( $requested_locale ) )
) {
header( 'HTTP/1.0 400 Bad Request', true, 400 );
die( 'Bad request.' );
}

$locale = false;
// Convert a locale from a WP locale to a GP locale.
if ( ( isset( $_GET['locale'] ) && 'en_US' != $_GET['locale'] ) || ( 'cli' == php_sapi_name() && isset( $argv[2] ) ) ) {
if (
( isset( $_GET['locale'] ) && 'en_US' != $requested_locale ) ||
( 'cli' == php_sapi_name() && isset( $argv[2] ) )
) {
require GLOTPRESS_LOCALES_PATH;

$gp_locale = GP_Locales::by_field( 'wp_locale', isset( $argv[2] ) ? $argv[2] : $_GET['locale'] );
$gp_locale = GP_Locales::by_field( 'wp_locale', isset( $argv[2] ) ? $argv[2] : $requested_locale );
if ( $gp_locale ) {
$locale = $gp_locale;
}
Expand Down
1 change: 1 addition & 0 deletions api.wordpress.org/public_html/core/credits/wp-credits.php
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,7 @@ final public function execute() {
} elseif ( defined( 'JSON_RESPONSE' ) && JSON_RESPONSE ) {
echo json_encode( $results );
} else {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- serialized payload served as text/plain.
echo serialize( $results );
}
}
Expand Down
14 changes: 13 additions & 1 deletion api.wordpress.org/public_html/core/importers/1.0/index.php
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
<?php
/**
* WordPress.org Importers API endpoint.
*
* This is a standalone, unauthenticated, stateless API endpoint: WordPress is not loaded,
* so request data is never slashed, and there is no session or nonce infrastructure.
*
* phpcs:disable WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
*
* @package WordPressdotorg\API\Importers
*/

// Allow playground access.
header( 'Access-Control-Allow-Origin: *' );
header( 'Content-Type: ' . ( defined( 'JSON_RESPONSE' ) ? 'application/json' : 'text/plain' ) );

$version = '';
if ( isset( $_REQUEST['version'] ) ) { // Introduced in WordPress 4.6.
$version = str_replace( '-src', '', $_REQUEST['version'] );
$requested_version = filter_var( $_REQUEST['version'], FILTER_UNSAFE_RAW, FILTER_FLAG_STRIP_LOW );
$version = is_string( $requested_version ) ? str_replace( '-src', '', $requested_version ) : '';
}

if ( version_compare( $version, '5.4-beta', '>=' ) ) {
Expand All @@ -21,6 +32,7 @@
}

$response = array( 'importers' => $popular_importers, 'translated' => false );
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON or serialized payload, not HTML.
echo defined( 'JSON_RESPONSE' ) ? json_encode( $response ) : serialize( $response );

function __( $string ) { return $string; }
Expand Down
11 changes: 11 additions & 0 deletions api.wordpress.org/public_html/core/serve-happy/1.0/include.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
<?php
/**
* Serve Happy API request parsing and response functions.
*
* This is a standalone, unauthenticated, stateless API endpoint: there is no session or
* nonce infrastructure to verify against.
*
* phpcs:disable WordPress.Security.NonceVerification
*
* @package WordPressdotorg\API\Serve_Happy
*/

namespace WordPressdotorg\API\Serve_Happy;

function determine_request( $request = false ) {
Expand Down
55 changes: 48 additions & 7 deletions api.wordpress.org/public_html/core/serve-happy/1.0/index.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
<?php
/**
* WordPress.org Serve Happy API endpoint: PHP version recommendations.
*
* This is a standalone, unauthenticated, stateless API endpoint: WordPress is not loaded,
* so request data is never slashed, and there is no session or nonce infrastructure.
*
* phpcs:disable WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
*
* @package WordPressdotorg\API\Serve_Happy
*/

namespace WordPressdotorg\API\Serve_Happy;

define( 'API_VERSION', '1.0' );
Expand All @@ -15,9 +26,26 @@
)
);

// Output functions
function bail( $error_code, $error_text, $http_code = 400, $http_code_text = false ) {
$server_protocol = $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.1';
/**
* Sends an error response and halts.
*
* @param string $error_code Machine-readable error code.
* @param string $error_text Human-readable error description.
* @param int $http_code Optional. HTTP status code. Default 400.
* @param string|null $http_code_text Optional. HTTP status reason phrase. Default derived from the status code.
*/
function bail( $error_code, $error_text, $http_code = 400, $http_code_text = null ) {
// Only a well-formed protocol version is echoed back into the status header.
$server_protocol = filter_var(
$_SERVER['SERVER_PROTOCOL'] ?? '',
FILTER_VALIDATE_REGEXP,
array(
'options' => array(
'regexp' => '#^HTTP/[0-9]+(\.[0-9]+)?\z#',
'default' => 'HTTP/1.1',
),
)
);
$http_code_texts = [
400 => 'Bad Request',
];
Expand All @@ -39,15 +67,28 @@ function output_response( $data ) {

header( 'Access-Control-Allow-Origin: *' );

if ( !empty( $_GET['callback'] ) ) {
// A JSONP callback is a JavaScript identifier, optionally namespaced; anything else is discarded.
$callback = filter_var(
$_GET['callback'] ?? '',
FILTER_VALIDATE_REGEXP,
array(
'options' => array(
'regexp' => '/^[a-zA-Z_$][a-zA-Z0-9_$]*(\.[a-zA-Z_$][a-zA-Z0-9_$]*)*\z/',
'default' => '',
),
'flags' => FILTER_REQUIRE_SCALAR,
)
);

if ( $callback ) {
call_headers( 'application/javascript' );

echo '/**/' .
preg_replace('/[^a-zA-Z0-9_.]/', '', $_GET['callback'] ) .
'(' . $json_data . ')';
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- validated callback, JSON-encoded payload.
echo '/**/' . $callback . '(' . $json_data . ')';
} else {
call_headers( 'application/json' );

// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON-encoded payload served as application/json.
echo $json_data;
}
}
12 changes: 10 additions & 2 deletions api.wordpress.org/public_html/patterns/1.0/index.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
<?php
/**
* WordPress.org Block Pattern Directory API endpoint.
*
* The request is read here, before `main()` loads WordPress, so it has not been slashed.
*
* phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash
*
* @package WordPressdotorg\API\Patterns
*/

namespace WordPressdotorg\API\Patterns;

Expand All @@ -8,7 +17,7 @@
* This is cached by nginx, so we don't have to worry about the performance costs of loading WP, and don't need to
* do any any object caching.
*/
main( $_SERVER['QUERY_STRING'] );
main( filter_var( $_SERVER['QUERY_STRING'] ?? '', FILTER_UNSAFE_RAW, FILTER_FLAG_STRIP_LOW ) );

/**
* Last minute rewrite of headers, to correct URLs set by the internal API endpoint.
Expand Down Expand Up @@ -41,7 +50,6 @@ function flush_handler( $buffer ) {
return false; // Original buffer will be output with no changes.
}


/**
* Proxy w.org/patterns API endpoints for reliability.
*
Expand Down