Skip to content
Merged
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
3 changes: 1 addition & 2 deletions .distignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
apigen.neon
CHANGELOG.md
babel.config.js
composer.json
composer.lock
gulpfile.js
labels.json
Expand All @@ -54,4 +53,4 @@ yarn-lock.json
*.css.map

# plugin release zip
*.zip
*.zip
1 change: 1 addition & 0 deletions bin/verify-release-artifact.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const { execFileSync } = require( 'child_process' );
const pluginRoot = 'content-control';

const requiredPaths = [
'composer.json',
'content-control.php',
'readme.txt',
'dist/settings-page.js',
Expand Down
42 changes: 35 additions & 7 deletions classes/Base/Stream.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@

/**
* HTTP Stream class.
*
* This class writes the `text/event-stream` wire format, not HTML. HTML
* escaping would corrupt SSE framing. Event names are reduced to the
* characters permitted by this implementation, non-string payloads are JSON
* encoded, and every payload line is emitted as a separate `data:` field.
* Content Control uses this only from capability- and nonce-protected
* administrative upgrade handlers.
*/
class Stream {

Expand All @@ -36,7 +43,11 @@ class Stream {
* @param string $stream_name Stream name.
*/
public function __construct( $stream_name = 'stream' ) {
$this->stream_name = $stream_name;
$this->stream_name = sanitize_key( $stream_name );

if ( empty( $this->stream_name ) ) {
$this->stream_name = 'stream';
}
}

/**
Expand Down Expand Up @@ -107,10 +118,8 @@ protected function flush_buffers() {
* @return void
*/
public function send_data( $data ) {
$data = is_string( $data ) ? $data : \wp_json_encode( $data );

// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo "data: {$data}" . PHP_EOL;
echo $this->format_data( $data );
echo PHP_EOL;

$this->flush_buffers();
Expand All @@ -125,17 +134,36 @@ public function send_data( $data ) {
* @return void
*/
public function send_event( $event, $data = '' ) {
$data = is_string( $data ) ? $data : \wp_json_encode( $data );
$event = preg_replace( '/[^a-zA-Z0-9_.:-]/', '', (string) $event );
$event = empty( $event ) ? 'message' : $event;

// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo "event: {$event}" . PHP_EOL;
echo 'event: ' . $event . PHP_EOL;
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo "data: {$data}" . PHP_EOL;
echo $this->format_data( $data );
echo PHP_EOL;

$this->flush_buffers();
}

/**
* Format a value as one or more SSE data fields.
*
* @param mixed $data Data to format.
*
* @return string
*/
protected function format_data( $data ) {
$data = is_string( $data ) ? $data : \wp_json_encode( $data );
$data = is_string( $data ) ? $data : '';
$data = str_replace( [ "\r\n", "\r" ], "\n", $data );
$lines = explode( "\n", $data );

return implode( PHP_EOL, array_map( static function ( $line ) {
return 'data: ' . $line;
}, $lines ) ) . PHP_EOL;
}

/**
* Send an error to the client.
*
Expand Down
23 changes: 11 additions & 12 deletions classes/Controllers/Admin/Reviews.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,17 +90,16 @@ public function installed_on() {
* @return void
*/
public function ajax_handler() {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
if ( ! isset( $_REQUEST['nonce'] ) || ! wp_verify_nonce( wp_unslash( $_REQUEST['nonce'] ), 'content_control_review_action' ) ) {
if ( ! isset( $_REQUEST['nonce'] ) || ! is_string( $_REQUEST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) ), 'content_control_review_action' ) ) {
wp_send_json_error();
}

$args = wp_parse_args( $_REQUEST, [
'group' => $this->get_trigger_group(),
'code' => $this->get_trigger_code(),
'pri' => $this->get_current_trigger( 'pri' ),
'reason' => 'maybe_later',
] );
$args = [
'group' => isset( $_REQUEST['group'] ) && is_string( $_REQUEST['group'] ) ? sanitize_key( wp_unslash( $_REQUEST['group'] ) ) : $this->get_trigger_group(),
'code' => isset( $_REQUEST['code'] ) && is_string( $_REQUEST['code'] ) ? sanitize_key( wp_unslash( $_REQUEST['code'] ) ) : $this->get_trigger_code(),
'pri' => isset( $_REQUEST['pri'] ) && is_numeric( $_REQUEST['pri'] ) ? absint( $_REQUEST['pri'] ) : $this->get_current_trigger( 'pri' ),
'reason' => isset( $_REQUEST['reason'] ) && is_string( $_REQUEST['reason'] ) ? sanitize_key( wp_unslash( $_REQUEST['reason'] ) ) : 'maybe_later',
];

try {
$user_id = get_current_user_id();
Expand Down Expand Up @@ -484,20 +483,20 @@ function dismiss(reason) {
<div class="notice notice-success is-dismissible content-control-notice">

<div class="notice-logo">
<img class="logo" width="110" src="<?php echo esc_attr( plugin()->get_url( 'assets/images/illustration-check.svg' ) ); ?>" />
<img class="logo" width="110" src="<?php echo esc_url( plugin()->get_url( 'assets/images/illustration-check.svg' ) ); ?>" />
</div>

<div class="notice-content">
<p>
<?php
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $trigger['message'];
// Review messages support post-safe inline markup such as emphasis and the star-rating span.
echo wp_kses_post( $trigger['message'] );
?>
~ <a target="_blank" href="https://twitter.com/danieliser" title="Follow Daniel on Twitter">@danieliser</a>
</p>
<ul class="review-actions">
<li>😁
<a class="content-control-dismiss" target="_blank" href="<?php echo esc_attr( $trigger['link'] ); ?>" data-reason="am_now">
<a class="content-control-dismiss" target="_blank" href="<?php echo esc_url( $trigger['link'] ); ?>" data-reason="am_now">
<strong><?php esc_html_e( 'Ok, you deserve it', 'content-control' ); ?></strong>
</a>
</li>
Expand Down
6 changes: 2 additions & 4 deletions classes/Controllers/Admin/Upgrades.php
Original file line number Diff line number Diff line change
Expand Up @@ -220,8 +220,7 @@ private function visit_node( $node, $graph, &$visited, &$sorted ) {
* @return void
*/
public function ajax_handler() {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
if ( ! isset( $_REQUEST['nonce'] ) || ! wp_verify_nonce( wp_unslash( $_REQUEST['nonce'] ), 'content_control_upgrades' ) ) {
if ( ! isset( $_REQUEST['nonce'] ) || ! is_string( $_REQUEST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) ), 'content_control_upgrades' ) ) {
wp_send_json_error( __( 'Invalid nonce.', 'content-control' ) );
}

Expand Down Expand Up @@ -322,8 +321,7 @@ public function ajax_handler() {
* @return void
*/
public function ajax_handler_demo() {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
if ( ! isset( $_REQUEST['nonce'] ) || ! wp_verify_nonce( wp_unslash( $_REQUEST['nonce'] ), 'content_control_upgrades' ) ) {
if ( ! isset( $_REQUEST['nonce'] ) || ! is_string( $_REQUEST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) ), 'content_control_upgrades' ) ) {
wp_send_json_error( __( 'Invalid nonce.', 'content-control' ) );
}

Expand Down
104 changes: 44 additions & 60 deletions classes/Controllers/Frontend/Blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ public function get_block_control_classes( $block ) {

foreach ( $hide_on as $device => $hidden ) {
if ( $hidden ) {
$classes[] = 'cc-hide-on-' . esc_attr( $device );
$classes[] = 'cc-hide-on-' . sanitize_html_class( $device );
}
}
}
Expand All @@ -220,6 +220,9 @@ public function get_block_control_classes( $block ) {
* @return string[]
*/
$classes = apply_filters( 'content_control/get_block_control_classes', $classes, $controls, $block );
$classes = array_filter( $classes, 'is_string' );
$classes = array_map( 'sanitize_html_class', $classes );
$classes = array_filter( $classes );

return array_unique( $classes );
}
Expand Down Expand Up @@ -258,7 +261,7 @@ public function render_block( $block_content, $block ) {
// Enqueue the styles.
// wp_enqueue_style( 'content-control-block-styles' );.

$class_name = implode( ' ', $classes );
$class_name = esc_attr( implode( ' ', $classes ) );

/** Mimicing WP Cores usage in https://github.com/WordPress/wordpress-develop/blob/trunk/src/wp-includes/block-supports/elements.php#L32 */
$html_element_matches = [];
Expand Down Expand Up @@ -293,81 +296,62 @@ public function print_block_styles() {
$media_queries = $this->container->get_option( 'mediaQueries' );

if ( ! $media_queries ) {
?>
<style id="content-control-block-styles">
@media (max-width: 480px) {
.cc-hide-on-mobile {
display: none !important;
}
}
@media (min-width: 481px) and (max-width: 991px) {
.cc-hide-on-tablet {
display: none !important;
}
}
@media (min-width: 992px) {
.cc-hide-on-desktop {
display: none !important;
}
}
</style>
<?php
$styles = "@media (max-width: 480px) {\n\t.cc-hide-on-mobile {\n\t\tdisplay: none !important;\n\t}\n}\n";
$styles .= "@media (min-width: 481px) and (max-width: 991px) {\n\t.cc-hide-on-tablet {\n\t\tdisplay: none !important;\n\t}\n}\n";
$styles .= "@media (min-width: 992px) {\n\t.cc-hide-on-desktop {\n\t\tdisplay: none !important;\n\t}\n}";
$this->enqueue_block_styles( $styles );
return;
}

$mobile_breakpoint = isset( $media_queries['mobile'] ) ? $media_queries['mobile']['breakpoint'] : 640;
$tablet_breakpoint = isset( $media_queries['tablet'] ) ? $media_queries['tablet']['breakpoint'] : 920;
$desktop_breakpoint = isset( $media_queries['desktop'] ) ? $media_queries['desktop']['breakpoint'] : 1440;
$mobile_breakpoint = isset( $media_queries['mobile']['breakpoint'] ) ? absint( $media_queries['mobile']['breakpoint'] ) : 640;
$tablet_breakpoint = isset( $media_queries['tablet']['breakpoint'] ) ? absint( $media_queries['tablet']['breakpoint'] ) : 920;
$desktop_breakpoint = isset( $media_queries['desktop']['breakpoint'] ) ? absint( $media_queries['desktop']['breakpoint'] ) : 1440;

$tablet_start = $mobile_breakpoint + 1;
$desktop_start = $tablet_breakpoint + 1;

$styles[] = <<<CSS
@media (max-width: {$mobile_breakpoint}px) {
.cc-hide-on-mobile {
display: none !important;
}
}
CSS;

$styles[] = <<<CSS
@media (min-width: {$tablet_start}px) and (max-width: {$tablet_breakpoint}px) {
.cc-hide-on-tablet {
display: none !important;
}
}
CSS;

$styles[] = <<<CSS
@media (min-width: {$desktop_start}px) and (max-width: {$desktop_breakpoint}px) {
.cc-hide-on-desktop {
display: none !important;
}
}
CSS;
$styles = [];
$styles[] = sprintf( "@media (max-width: %dpx) {\n\t.cc-hide-on-mobile {\n\t\tdisplay: none !important;\n\t}\n}", $mobile_breakpoint );
$styles[] = sprintf( "@media (min-width: %dpx) and (max-width: %dpx) {\n\t.cc-hide-on-tablet {\n\t\tdisplay: none !important;\n\t}\n}", $tablet_start, $tablet_breakpoint );
$styles[] = sprintf( "@media (min-width: %dpx) and (max-width: %dpx) {\n\t.cc-hide-on-desktop {\n\t\tdisplay: none !important;\n\t}\n}", $desktop_start, $desktop_breakpoint );

unset( $media_queries['mobile'], $media_queries['tablet'], $media_queries['desktop'] );

foreach ( $media_queries as $media_query => $media_query_settings ) {
$breakpoint = $media_query_settings['breakpoint'];
if ( ! is_array( $media_query_settings ) || ! isset( $media_query_settings['breakpoint'] ) ) {
continue;
}

$style = <<<CSS
@media (min-width: {$breakpoint}px) {
.cc-hide-on-{$media_query} {
display: none !important;
}
}
CSS;
$breakpoint = absint( $media_query_settings['breakpoint'] );
$media_query_class = sanitize_html_class( $media_query );

if ( empty( $media_query_class ) ) {
continue;
}

$style = sprintf( "@media (min-width: %dpx) {\n\t.cc-hide-on-%s {\n\t\tdisplay: none !important;\n\t}\n}", $breakpoint, $media_query_class );

$styles[] = apply_filters( 'content_control/block_styles', $style, $media_query, $breakpoint );
}

$styles = implode( "\n", $styles );

?>
<style id="content-control-block-styles">
<?php echo $styles; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
</style>
<?php
$this->enqueue_block_styles( $styles );
}

/**
* Enqueue generated block-control styles.
*
* @param string $styles CSS styles.
*
* @return void
*/
protected function enqueue_block_styles( $styles ) {
$handle = 'content-control-block-styles';

wp_register_style( $handle, false, [], $this->container->get( 'version' ) );
wp_enqueue_style( $handle );
// Preserve valid HTML-like CSS syntax; wp_add_inline_style() handles style-tag safety.
wp_add_inline_style( $handle, $styles );
}
}
11 changes: 11 additions & 0 deletions classes/Controllers/Frontend/Restrictions/PostContent.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@
/**
* Class for handling global restrictions of the post contents.
*
* This controller participates in WordPress's content-filter pipeline and
* intentionally returns rendered HTML. Restricted messages run through the
* same block, embed, shortcode, and media filters as `the_content`. Applying
* KSES after those filters would remove functional forms, embeds, SVG, and
* script-backed shortcode output. Values returned by the documented filters
* are supplied by trusted plugin/theme code.
*
* @package ContentControl
*/
class PostContent extends Controller {
Expand Down Expand Up @@ -111,6 +118,7 @@ public function filter_the_content_if_restricted( $content ) {
$pre_restrict_content = apply_filters( 'content_control/pre_restrict_content', null, $content, $restriction );

if ( null !== $pre_restrict_content ) {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Intentional rendered HTML returned to the_content.
return $pre_restrict_content;
}

Expand Down Expand Up @@ -140,6 +148,7 @@ public function filter_the_content_if_restricted( $content ) {
*
* @return string
*/
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Intentional rendered HTML returned to the_content.
return apply_filters(
$filter_name,
// If the default message is empty, show a generic message.
Expand Down Expand Up @@ -191,6 +200,7 @@ public function filter_the_excerpt_if_restricted( $post_excerpt, $post = null )
$pre_restrict_excerpt = apply_filters( 'content_control/pre_restrict_excerpt', null, $post_excerpt, $restriction );

if ( null !== $pre_restrict_excerpt ) {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Intentional rendered HTML returned to the excerpt filter.
return $pre_restrict_excerpt;
}

Expand Down Expand Up @@ -220,6 +230,7 @@ public function filter_the_excerpt_if_restricted( $post_excerpt, $post = null )
*
* @return string
*/
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Intentional rendered HTML returned to the excerpt filter.
return apply_filters(
$filter_name,
// If the default message is empty, show a generic message.
Expand Down
Loading
Loading