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
6 changes: 5 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"phpcompatibility/phpcompatibility-wp": "dev-master",
"phpcompatibility/php-compatibility": "dev-develop as 9.99.99",
"automattic/vipwpcs": "^3.0",
"wp-coding-standards/wpcs": "^3.0"
"wp-coding-standards/wpcs": "^3.0",
"phpunit/phpunit": "^9.6"
},
"config": {
"platform": {
Expand All @@ -27,6 +28,9 @@
],
"fix": [
"phpcbf"
],
"test": [
"phpunit"
]
},
"minimum-stability": "dev"
Expand Down
24 changes: 20 additions & 4 deletions php/class-connect.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,22 @@ class Connect extends Settings_Component implements Config, Setup, Notice {
*/
const CLOUDINARY_VARIABLE_REGEX = '^(?:CLOUDINARY_URL=)?cloudinary://[0-9]+:[A-Za-z_\-0-9]+@[A-Za-z]+';

/**
* Sanitize a raw connection URL input.
*
* Strips leading/trailing whitespace and the optional CLOUDINARY_URL= prefix
* (case-insensitive), which users sometimes copy verbatim from their dashboard.
*
* @param string $url The raw URL string.
*
* @return string
*/
public static function sanitize_connection_url( $url ) {
$url = trim( (string) $url );
$url = preg_replace( '/^CLOUDINARY_URL=/i', '', $url );
return trim( $url );
}

/**
* Initiate the plugin resources.
*
Expand Down Expand Up @@ -150,7 +166,7 @@ public function rest_endpoints( $endpoints ) {
*/
public function rest_test_connection( WP_REST_Request $request ) {

$url = $request->get_param( 'cloudinary_url' );
$url = self::sanitize_connection_url( (string) $request->get_param( 'cloudinary_url' ) );
$result = $this->test_connection( $url );

return rest_ensure_response( $result );
Expand Down Expand Up @@ -273,7 +289,7 @@ public function verify_connection( $data ) {
return $data;
}

$data['cloudinary_url'] = str_replace( 'CLOUDINARY_URL=', '', $data['cloudinary_url'] );
$data['cloudinary_url'] = self::sanitize_connection_url( $data['cloudinary_url'] );
$current = $this->plugin->settings->find_setting( 'connect' )->get_value();

// Same URL, return original data.
Expand Down Expand Up @@ -904,7 +920,7 @@ public function upgrade_connection( $old_version ) {
}

// Test upgraded details.
$data['cloudinary_url'] = str_replace( 'CLOUDINARY_URL=', '', $data['cloudinary_url'] );
$data['cloudinary_url'] = self::sanitize_connection_url( $data['cloudinary_url'] );
$test = $this->test_connection( $data['cloudinary_url'] );

if ( 'connection_success' === $test['type'] ) {
Expand Down Expand Up @@ -943,7 +959,7 @@ public function maybe_connection_string_constant( $value, $setting ) {
static $url = null;

if ( empty( $url ) ) {
$url = str_replace( 'CLOUDINARY_URL=', '', CLOUDINARY_CONNECTION_STRING );
$url = self::sanitize_connection_url( CLOUDINARY_CONNECTION_STRING );
}

if ( 'cloudinary_url' === $setting ) {
Expand Down
19 changes: 19 additions & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?xml version="1.0"?>
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.6/phpunit.xsd"
bootstrap="tests/php/bootstrap.php"
colors="true"
verbose="true"
>
<testsuites>
<testsuite name="Unit">
<directory>tests/php</directory>
</testsuite>
</testsuites>
<coverage>
<include>
<directory suffix=".php">php</directory>
</include>
</coverage>
</phpunit>
11 changes: 7 additions & 4 deletions src/js/components/wizard.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const Wizard = {
error: document.getElementById( 'connection-error' ),
success: document.getElementById( 'connection-success' ),
working: document.getElementById( 'connection-working' ),
formatHint: document.getElementById( 'connection-format-hint' ),
},
debounceConnect: null,
updateConnection: document.getElementById( 'update-connection' ),
Expand Down Expand Up @@ -93,13 +94,13 @@ const Wizard = {
} );
connectionInput.addEventListener( 'input', ( ev ) => {
this.lockNext();
const value = connectionInput.value.replace(
'CLOUDINARY_URL=',
''
);
const value = connectionInput.value
.replace( /^CLOUDINARY_URL=/i, '' )
.trim();
this.connection.error.classList.remove( 'active' );
this.connection.success.classList.remove( 'active' );
this.connection.working.classList.remove( 'active' );
this.connection.formatHint.classList.add( 'hidden' );
if ( value.length ) {
this.testing = value;
if ( this.debounceConnect ) {
Expand Down Expand Up @@ -258,10 +259,12 @@ const Wizard = {
showError() {
this.connection.error.classList.add( 'active' );
this.connection.success.classList.remove( 'active' );
this.connection.formatHint.classList.remove( 'hidden' );
},
showSuccess() {
this.connection.error.classList.remove( 'active' );
this.connection.success.classList.add( 'active' );
this.connection.formatHint.classList.add( 'hidden' );
},
show( item ) {
item.classList.remove( 'hidden' );
Expand Down
118 changes: 118 additions & 0 deletions tests/php/bootstrap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?php
/**
* PHPUnit bootstrap file.
*
* Defines the minimal WordPress function stubs needed to load class-connect.php
* in isolation, without a full WordPress installation.
*
* @package Cloudinary
*/

// Autoload the plugin's PHP classes.
spl_autoload_register(
function ( $class ) {
$prefix = 'Cloudinary\\';
if ( 0 !== strpos( $class, $prefix ) ) {
return;
}

$relative = str_replace( $prefix, '', $class );
$relative = strtolower( str_replace( array( '\\', '_' ), array( '/', '-' ), $relative ) );
$file = __DIR__ . '/../../php/' . $relative . '.php';

// Also try class- prefix convention.
if ( ! file_exists( $file ) ) {
$parts = explode( '/', $relative );
$last = array_pop( $parts );
$parts[] = 'class-' . $last;
$file = __DIR__ . '/../../php/' . implode( '/', $parts ) . '.php';
}

if ( file_exists( $file ) ) {
require_once $file;
}
}
);

// ---------------------------------------------------------------------------
// Minimal WordPress stubs so class-connect.php can be parsed without WP core.
// ---------------------------------------------------------------------------

if ( ! function_exists( 'wp_parse_url' ) ) {
/**
* Stub for wp_parse_url().
*
* @param string $url The URL.
* @param int $component Optional PHP_URL_* constant.
* @return array|string|int|null
*/
function wp_parse_url( $url, $component = -1 ) {
return parse_url( $url, $component );
}
}

if ( ! function_exists( 'add_filter' ) ) {
/** Stub for add_filter(). */
function add_filter() {}
}

if ( ! function_exists( 'add_action' ) ) {
/** Stub for add_action(). */
function add_action() {}
}

if ( ! function_exists( '__' ) ) {
/**
* Stub for __().
*
* @param string $text The text.
* @param string $domain Text domain.
* @return string
*/
function __( $text, $domain = 'default' ) {
return $text;
}
}

if ( ! function_exists( 'is_wp_error' ) ) {
/**
* Stub for is_wp_error().
*
* @param mixed $thing Value to check.
* @return bool
*/
function is_wp_error( $thing ) {
return $thing instanceof WP_Error;
}
}

if ( ! class_exists( 'WP_Error' ) ) {
/** Minimal WP_Error stub. */
class WP_Error {
/** @var string */
private $code;
/** @var string */
private $message;

/**
* Constructor.
*
* @param string $code Error code.
* @param string $message Error message.
*/
public function __construct( $code = '', $message = '' ) {
$this->code = $code;
$this->message = $message;
}

/** @return string */
public function get_error_code() {
return $this->code;
}

/** @return string */
public function get_error_message() {
return $this->message;
}
}
}
Loading