Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/query-wrangler/admin/admin.php
+++ b/query-wrangler/admin/admin.php
@@ -91,6 +91,9 @@
$wpdb->insert( $table_name, $values );
+ // The set of callbacks saved in query data may have changed.
+ qw_flush_stored_callbacks();
+
return $wpdb->insert_id;
}
@@ -160,6 +163,9 @@
$query_id ) );
}
+ // The set of callbacks saved in query data may have changed.
+ qw_flush_stored_callbacks();
+
// Refresh meta_keys_cache.
$settings = QW_Settings::get_instance();
if ($settings->get('meta_key_cache_life') !== 'none') {
@@ -178,6 +184,9 @@
$table = $wpdb->prefix . "query_wrangler";
$wpdb->delete( $table, array( 'id' => $query_id ) );
+ // The set of callbacks saved in query data may have changed.
+ qw_flush_stored_callbacks();
+
do_action( 'qw_delete_query', $query_id );
// @todo - move this somewhere that subscribes to the action
@@ -227,6 +236,10 @@
$query['data'] = qw_serialize( $query['data'] );
$wpdb->insert( $table, $query );
+ // An imported query may name callbacks. They become executable only after
+ // an administrator adds them to the Allowed Callbacks setting.
+ qw_flush_stored_callbacks();
+
return $wpdb->insert_id;
}
@@ -378,6 +391,11 @@
* Checking current version of plugin to handle upgrades
*/
function qw_check_version() {
+ // One-time approval of callbacks that predate the allow list. Runs
+ // independently of the version-to-version upgrade functions below so that
+ // it happens no matter which version a site is coming from.
+ qw_seed_allowed_callbacks();
+
if ( $last_version = get_option( 'qw_plugin_version' ) ) {
// compare versions
if ( $last_version < QW_VERSION ) {
@@ -408,6 +426,8 @@
* Ajax callback for meta_key autocomplete
*/
function qw_meta_key_autocomplete() {
+ qw_verify_ajax_request();
+
if ( isset( $_POST['qw_meta_key_autocomplete'] ) ) {
$meta_key = sanitize_text_field( $_POST['qw_meta_key_autocomplete'] );
global $wpdb;
--- a/query-wrangler/admin/ajax.php
+++ b/query-wrangler/admin/ajax.php
@@ -1,20 +1,74 @@
<?php
+/**
+ * Capability required to use Query Wrangler's admin ajax endpoints.
+ */
+define( 'QW_AJAX_CAPABILITY', 'edit_others_posts' );
+
+/**
+ * Nonce action shared by Query Wrangler's admin ajax endpoints.
+ */
+define( 'QW_AJAX_NONCE_ACTION', 'qw-admin-ajax' );
+
+/**
+ * Stop an ajax request that isn't a permitted user acting from our own admin
+ * screen. Sends a response and exits when the request is rejected.
+ */
+function qw_verify_ajax_request() {
+ if ( ! current_user_can( QW_AJAX_CAPABILITY ) ) {
+ wp_send_json_error( array( 'message' => 'You are not allowed to do that.' ), 403 );
+ }
+
+ check_ajax_referer( QW_AJAX_NONCE_ACTION, 'qw_nonce' );
+}
+
+/**
+ * Reduce a posted handler or handler item name to a safe key.
+ *
+ * These values become array keys and form input names, so keep them to the
+ * characters the editor actually generates.
+ *
+ * @param mixed $key
+ *
+ * @return string
+ */
+function qw_sanitize_handler_key( $key ) {
+ if ( ! is_string( $key ) ) {
+ return '';
+ }
+
+ return preg_replace( '/[^a-zA-Z0-9_-]/', '', $key );
+}
+
/*
* Ajax form templates
*/
function qw_form_ajax() {
+ // This endpoint builds handler forms and renders a live query preview,
+ // which executes the query's handlers. Only users who can reach the query
+ // editor may use it, and only from a page that carried our nonce.
+ qw_verify_ajax_request();
+
+ if ( empty( $_POST['form'] ) ) {
+ wp_send_json_error( array( 'message' => 'No form was requested.' ), 400 );
+ }
+
switch ( $_POST['form'] ) {
/*
* Preview, special case
*/
case 'preview':
- $decode = urldecode( $_POST['options'] );
+ $decode = urldecode( isset( $_POST['options'] ) ? $_POST['options'] : '' );
$options = array();
parse_str( $decode, $options );
- $options['qw-query-options']['args']['paged'] = 1;
- $args = array(
- 'options' => $options['qw-query-options'],
- 'query_id' => $_POST['query_id'],
+
+ if ( ! isset( $options[ QW_FORM_PREFIX ] ) || ! is_array( $options[ QW_FORM_PREFIX ] ) ) {
+ wp_send_json_error( array( 'message' => 'No query options were submitted.' ), 400 );
+ }
+
+ $options[ QW_FORM_PREFIX ]['args']['paged'] = 1;
+ $args = array(
+ 'options' => $options[ QW_FORM_PREFIX ],
+ 'query_id' => isset( $_POST['query_id'] ) ? (int) $_POST['query_id'] : 0,
);
print theme( 'query_preview', $args );
exit;
@@ -54,17 +108,25 @@
$template = 'query_filter_sortable';
$all = qw_all_filters();
break;
+
+ default:
+ wp_send_json_error( array( 'message' => 'Unknown form requested.' ), 400 );
}
/*
* Generate handler item forms and data
*/
- $handler = $_POST['handler'];
+ $handler = qw_sanitize_handler_key( isset( $_POST['handler'] ) ? $_POST['handler'] : '' );
$item = array();
- $hook_key = qw_get_hook_key( $all, $_POST );
+ $hook_key = qw_get_hook_key( $all, $_POST );
+
+ if ( ! $handler || ! isset( $all[ $hook_key ] ) ) {
+ wp_send_json_error( array( 'message' => 'Unknown handler item requested.' ), 400 );
+ }
+
$item = $all[ $hook_key ];
- $item['name'] = $_POST['name'];
+ $item['name'] = qw_sanitize_handler_key( isset( $_POST['name'] ) ? $_POST['name'] : '' );
$item['form_prefix'] = qw_make_form_prefix( $handler, $item['name'] );
// handler item's form
@@ -93,6 +155,8 @@
* Random data grabs
*/
function qw_data_ajax() {
+ qw_verify_ajax_request();
+
if ( isset( $_POST['data'] ) ) {
switch ( $_POST['data'] ) {
case 'all_hooks':
--- a/query-wrangler/admin/query-admin-pages.php
+++ b/query-wrangler/admin/query-admin-pages.php
@@ -234,6 +234,7 @@
$meta_value_field_handler = ( isset( $post['qw-meta-value-field-handler'] ) ) ? $post['qw-meta-value-field-handler'] : '';
$shortcode_compat = isset( $post['qw-shortcode-compat'] ) ? $post['qw-shortcode-compat'] : '';
$meta_key_cache_life = $post['qw-meta-keys-cache-life'] ?? 'forever';
+ $allowed_callbacks = $post['qw-allowed-callbacks'] ?? '';
$settings = QW_Settings::get_instance();
$settings->set( 'edit_theme', $post['qw-theme'] );
@@ -243,6 +244,7 @@
$settings->set( 'meta_value_field_handler', $meta_value_field_handler );
$settings->set( 'shortcode_compat', $shortcode_compat );
$settings->set('meta_key_cache_life', $meta_key_cache_life);
+ $settings->set( 'allowed_callbacks', $allowed_callbacks, 'qw_sanitize_callback_list' );
$settings->save();
}
--- a/query-wrangler/admin/templates/form-editor.php
+++ b/query-wrangler/admin/templates/form-editor.php
@@ -2,7 +2,8 @@
action="<?php print admin_url( "admin.php?page=query-wrangler&action=update&edit=$query_id&noheader=true" ); ?>"
method='post'
data-query-id="<?php print $query_id; ?>"
- data-ajax-url="<?php print admin_url( 'admin-ajax.php' ); ?>">
+ data-ajax-url="<?php print admin_url( 'admin-ajax.php' ); ?>"
+ data-ajax-nonce="<?php print esc_attr( wp_create_nonce( QW_AJAX_NONCE_ACTION ) ); ?>">
<?php wp_nonce_field( 'qw-edit_'. $query_id ); ?>
--- a/query-wrangler/admin/templates/form-settings.php
+++ b/query-wrangler/admin/templates/form-settings.php
@@ -7,6 +7,7 @@
* @var bool $shortcode_compat
* @var string $meta_value_field_handler
* @var string $meta_key_cache_life
+ * @var array $allowed_callbacks
* @var array[] $edit_themes
* @var array $meta_value_field_options
*/
@@ -160,5 +161,43 @@
</p>
</td>
</tr>
+
+ <tr>
+ <th>
+ <label for="qw-allowed-callbacks">Allowed Callbacks</label>
+ </th>
+ <td>
+ <p class="description">
+ The <em>Callback</em> field, the <em>Callback</em> filter and
+ the <em>Post IDs</em> filter can execute a PHP function while
+ a query runs. Only functions named here will be executed.
+ One function name per line.
+ </p>
+ <textarea id="qw-allowed-callbacks"
+ name="qw-allowed-callbacks"
+ rows="6"
+ cols="50"
+ class="code"><?php print esc_textarea( implode( "n", (array) $allowed_callbacks ) ); ?></textarea>
+ <p class="description">
+ <strong>Treat this list as executable code.</strong> Anything
+ you add here can be run whenever a query renders, so only add
+ functions you wrote or trust for this purpose. Developers can
+ approve callbacks from code with the
+ <code>qw_allowed_callbacks</code> filter instead.
+ </p>
+ <?php
+ $unapproved = array_diff( qw_stored_callbacks(), qw_allowed_callbacks() );
+ if ( $unapproved ) {
+ ?>
+ <p class="description" style="color: #a02222;">
+ These callbacks are saved in your queries but are not
+ approved, so they are not being executed:
+ <code><?php print esc_html( implode( ', ', $unapproved ) ); ?></code>
+ </p>
+ <?php
+ }
+ ?>
+ </td>
+ </tr>
</table>
</form>
--- a/query-wrangler/includes/callbacks.php
+++ b/query-wrangler/includes/callbacks.php
@@ -0,0 +1,424 @@
+<?php
+/*
+ * Controls for executing administrator-provided callback functions.
+ *
+ * Three handlers let a query name a PHP function to execute while the query
+ * runs: the Callback field, the Callback filter, and the Post IDs filter.
+ * Query options do not always come from the database - the query editor's
+ * live preview builds them out of $_POST - so a function name found in an
+ * options array cannot be trusted on its own.
+ *
+ * Every one of those executions is routed through qw_callback_is_allowed(),
+ * which requires the name to be:
+ *
+ * 1. shaped like a plain function name,
+ * 2. absent from the denylist of dangerous functions,
+ * 3. present in the administrator's allow list,
+ * 4. present in stored, administrator-authored query data,
+ * 5. an existing function.
+ *
+ * Requirement 4 is what stops the live preview from introducing a callback:
+ * $_POST can name any function it likes, but only names an administrator
+ * actually saved will run.
+ */
+
+/**
+ * Option keys whose value is treated as a callback function name.
+ *
+ * @return array
+ */
+function qw_callback_option_keys() {
+ $keys = array(
+ // includes/fields/callback_field.php
+ 'custom_output_callback',
+ // includes/filters/callback.php
+ 'callback',
+ // includes/filters/post_id.php
+ 'post_ids_callback',
+ );
+
+ return apply_filters( 'qw_callback_option_keys', $keys );
+}
+
+/**
+ * Functions that may never run as a query callback, allow list or not.
+ *
+ * This is a backstop, not the primary control. It exists so that a polluted
+ * allow list cannot immediately become code execution.
+ *
+ * @return array
+ */
+function qw_callback_denylist() {
+ $denylist = array(
+ // arbitrary code and command execution
+ 'eval',
+ 'assert',
+ 'create_function',
+ 'exec',
+ 'shell_exec',
+ 'system',
+ 'passthru',
+ 'proc_open',
+ 'popen',
+ 'pcntl_exec',
+ // indirect execution, used to smuggle the above past an allow list
+ 'call_user_func',
+ 'call_user_func_array',
+ 'array_map',
+ 'array_filter',
+ 'array_walk',
+ 'array_walk_recursive',
+ 'array_reduce',
+ 'usort',
+ 'uasort',
+ 'uksort',
+ 'preg_replace_callback',
+ 'preg_replace_callback_array',
+ 'register_shutdown_function',
+ 'register_tick_function',
+ 'set_error_handler',
+ 'set_exception_handler',
+ 'forward_static_call',
+ 'forward_static_call_array',
+ 'iterator_apply',
+ 'add_action',
+ 'add_filter',
+ 'do_action',
+ 'apply_filters',
+ // includes and filesystem writes
+ 'require',
+ 'require_once',
+ 'include',
+ 'include_once',
+ 'file_put_contents',
+ 'fwrite',
+ 'fputs',
+ 'copy',
+ 'rename',
+ 'unlink',
+ 'rmdir',
+ 'chmod',
+ 'move_uploaded_file',
+ // deserialization
+ 'unserialize',
+ 'maybe_unserialize',
+ // destructive or privilege-granting WordPress APIs
+ 'wp_insert_user',
+ 'wp_create_user',
+ 'wp_update_user',
+ 'wp_delete_user',
+ 'wp_set_password',
+ 'wp_set_current_user',
+ 'add_option',
+ 'update_option',
+ 'delete_option',
+ 'wp_delete_post',
+ 'wp_delete_attachment',
+ 'wp_remote_get',
+ 'wp_remote_post',
+ 'wp_remote_request',
+ );
+
+ return apply_filters( 'qw_callback_denylist', $denylist );
+}
+
+/**
+ * Reduce a value to a plain function name, or an empty string.
+ *
+ * Deliberately narrow: no class methods, no closures, no "Class::method"
+ * strings, nothing that call_user_func() would treat as dynamic.
+ *
+ * @param mixed $callback
+ *
+ * @return string
+ */
+function qw_sanitize_callback_name( $callback ) {
+ if ( ! is_string( $callback ) ) {
+ return '';
+ }
+
+ $callback = trim( $callback );
+
+ if ( ! preg_match( '/^[a-zA-Z_x80-xff][a-zA-Z0-9_x80-xff]*$/', $callback ) ) {
+ return '';
+ }
+
+ return $callback;
+}
+
+/**
+ * Function names an administrator has approved for execution.
+ *
+ * @return array
+ */
+function qw_allowed_callbacks() {
+ $allowed = QW_Settings::get_instance()->get( 'allowed_callbacks', array() );
+
+ if ( ! is_array( $allowed ) ) {
+ $allowed = array();
+ }
+
+ /*
+ * Filter the callbacks Query Wrangler is allowed to execute.
+ *
+ * Use this to approve callbacks from code instead of the settings screen:
+ *
+ * add_filter( 'qw_allowed_callbacks', function( $allowed ) {
+ * $allowed[] = 'my_theme_query_field_callback';
+ * return $allowed;
+ * } );
+ */
+ $allowed = apply_filters( 'qw_allowed_callbacks', $allowed );
+
+ $allowed = array_map( 'qw_sanitize_callback_name', (array) $allowed );
+
+ return array_values( array_unique( array_filter( $allowed ) ) );
+}
+
+/**
+ * Turn a newline or comma separated list of names into a clean array.
+ *
+ * Used to sanitize the allow list on its way into the settings option.
+ *
+ * @param mixed $value
+ *
+ * @return array
+ */
+function qw_sanitize_callback_list( $value ) {
+ if ( is_string( $value ) ) {
+ $value = preg_split( '/[s,]+/', $value );
+ }
+
+ $value = array_map( 'qw_sanitize_callback_name', (array) $value );
+
+ return array_values( array_unique( array_filter( $value ) ) );
+}
+
+/**
+ * Every callback name defined in a single query's options array.
+ *
+ * @param array $options
+ *
+ * @return array
+ */
+function qw_extract_callbacks( $options ) {
+ if ( ! is_array( $options ) ) {
+ return array();
+ }
+
+ $keys = qw_callback_option_keys();
+ $found = array();
+
+ array_walk_recursive( $options, function ( $value, $key ) use ( $keys, &$found ) {
+ if ( in_array( $key, $keys, TRUE ) ) {
+ $name = qw_sanitize_callback_name( $value );
+ if ( $name ) {
+ $found[] = $name;
+ }
+ }
+ } );
+
+ return array_values( array_unique( $found ) );
+}
+
+/**
+ * Every callback name found in stored query data.
+ *
+ * Saving a query requires manage_options (admin/query-admin-pages.php), so
+ * anything in this table was authored by an administrator. That makes it the
+ * trust anchor for callback execution.
+ *
+ * @return array
+ */
+function qw_stored_callbacks() {
+ $cached = get_transient( 'qw_stored_callbacks' );
+
+ if ( is_array( $cached ) ) {
+ return $cached;
+ }
+
+ global $wpdb;
+ $table = $wpdb->prefix . 'query_wrangler';
+ $found = array();
+
+ // This runs while rendering the front end. If the table is missing we want
+ // no callbacks rather than a database error on the page.
+ $suppressing = $wpdb->suppress_errors();
+ $rows = $wpdb->get_col( "SELECT data FROM {$table}" );
+ $wpdb->suppress_errors( $suppressing );
+
+ foreach ( (array) $rows as $data ) {
+ $found = array_merge( $found, qw_extract_callbacks( qw_unserialize( $data ) ) );
+ }
+
+ $found = array_values( array_unique( $found ) );
+
+ set_transient( 'qw_stored_callbacks', $found, DAY_IN_SECONDS );
+
+ return $found;
+}
+
+/**
+ * Forget the cached list of stored callbacks.
+ *
+ * Called whenever query data changes.
+ */
+function qw_flush_stored_callbacks() {
+ delete_transient( 'qw_stored_callbacks' );
+}
+
+/**
+ * Whether a callback provided by query options may be executed.
+ *
+ * @param mixed $callback Function name from a query's options.
+ * @param string $context Handler the callback came from, for debug logging.
+ *
+ * @return bool
+ */
+function qw_callback_is_allowed( $callback, $context = 'callback' ) {
+ $name = qw_sanitize_callback_name( $callback );
+
+ if ( ! $name ) {
+ return FALSE;
+ }
+
+ $denied = array_map( 'strtolower', qw_callback_denylist() );
+
+ if ( in_array( strtolower( $name ), $denied, TRUE ) ) {
+ qw_callback_refused( $name, $context, 'the function is on Query Wrangler's denylist' );
+
+ return FALSE;
+ }
+
+ if ( ! in_array( $name, qw_allowed_callbacks(), TRUE ) ) {
+ qw_callback_refused( $name, $context, 'the function is not in the Allowed Callbacks setting' );
+
+ return FALSE;
+ }
+
+ // Options can be built from $_POST by the editor's live preview. Only
+ // names an administrator saved to the database are executable.
+ if ( ! in_array( $name, qw_stored_callbacks(), TRUE ) ) {
+ qw_callback_refused( $name, $context, 'the function is not saved in any query - save the query before previewing it' );
+
+ return FALSE;
+ }
+
+ if ( ! function_exists( $name ) ) {
+ qw_callback_refused( $name, $context, 'the function does not exist' );
+
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+/**
+ * Approve the callbacks a site was already using, once.
+ *
+ * The allow list was introduced after callbacks had been executable for years.
+ * Starting empty would silently break working sites, so on the first run we
+ * approve whatever is already saved in query data - which only an
+ * administrator could have put there. Anything added after this runs has to be
+ * approved deliberately.
+ */
+function qw_seed_allowed_callbacks() {
+ if ( get_option( 'qw_allowed_callbacks_seeded' ) ) {
+ return;
+ }
+
+ $settings = QW_Settings::get_instance();
+ $existing = $settings->get( 'allowed_callbacks', array() );
+ $denied = array_map( 'strtolower', qw_callback_denylist() );
+
+ $seeded = array();
+ foreach ( qw_stored_callbacks() as $callback ) {
+ if ( ! in_array( strtolower( $callback ), $denied, TRUE ) ) {
+ $seeded[] = $callback;
+ }
+ }
+
+ $settings->set( 'allowed_callbacks',
+ array_merge( (array) $existing, $seeded ),
+ 'qw_sanitize_callback_list' );
+ $settings->save();
+
+ update_option( 'qw_allowed_callbacks_seeded', 1 );
+}
+
+/**
+ * Explain a callback's status underneath a handler's form field.
+ *
+ * Callbacks silently doing nothing is confusing, so say plainly whether this
+ * one will run and what to do about it if it won't.
+ *
+ * @param mixed $callback Function name currently entered in the form.
+ */
+function qw_callback_status_notice( $callback ) {
+ $name = qw_sanitize_callback_name( $callback );
+
+ if ( ! $name ) {
+ ?>
+ <p class="description">
+ Callbacks must be approved before Query Wrangler will execute them.
+ Add the function name to <em>Allowed Callbacks</em> on the
+ <a href="<?php print esc_url( admin_url( 'admin.php?page=qw-settings' ) ); ?>">Query
+ Wrangler settings</a> screen.
+ </p>
+ <?php
+
+ return;
+ }
+
+ $reasons = array();
+
+ if ( in_array( strtolower( $name ), array_map( 'strtolower', qw_callback_denylist() ), TRUE ) ) {
+ $reasons[] = 'it is on Query Wrangler's denylist and can never be executed';
+ }
+ else {
+ if ( ! in_array( $name, qw_allowed_callbacks(), TRUE ) ) {
+ $reasons[] = 'it is not listed in <em>Allowed Callbacks</em> on the Query Wrangler settings screen';
+ }
+ if ( ! in_array( $name, qw_stored_callbacks(), TRUE ) ) {
+ $reasons[] = 'it has not been saved yet — save this query, then preview it';
+ }
+ if ( ! function_exists( $name ) ) {
+ $reasons[] = 'no function by that name exists';
+ }
+ }
+
+ if ( ! $reasons ) {
+ ?>
+ <p class="description" style="color: #227122;">
+ <strong>✓ <?php print esc_html( $name ); ?></strong> is
+ approved and will be executed.
+ </p>
+ <?php
+
+ return;
+ }
+ ?>
+ <p class="description" style="color: #a02222;">
+ <strong>✗ <?php print esc_html( $name ); ?></strong> will not be
+ executed because <?php print implode( ', and ', $reasons ); ?>.
+ </p>
+ <?php
+}
+
+/**
+ * Note a refused callback when debugging is on.
+ *
+ * @param string $callback
+ * @param string $context
+ * @param string $reason
+ */
+function qw_callback_refused( $callback, $context, $reason ) {
+ if ( ! defined( 'WP_DEBUG' ) || ! WP_DEBUG ) {
+ return;
+ }
+
+ error_log( sprintf( 'Query Wrangler refused to execute the %s "%s" because %s.',
+ $context,
+ $callback,
+ $reason ) );
+}
No newline at end of file
--- a/query-wrangler/includes/class-qw-settings.php
+++ b/query-wrangler/includes/class-qw-settings.php
@@ -12,6 +12,7 @@
'meta_value_field_handler' => 0,
'shortcode_compat' => 0,
'meta_key_cache_life' => 0, // Forever.
+ 'allowed_callbacks' => array(), // Functions handlers may execute.
);
public $values = array();
--- a/query-wrangler/includes/fields/callback_field.php
+++ b/query-wrangler/includes/fields/callback_field.php
@@ -26,7 +26,9 @@
$echoed = FALSE;
ob_start();
- if ( isset( $field['custom_output_callback'] ) && function_exists( $field['custom_output_callback'] ) ) {
+ if ( isset( $field['custom_output_callback'] ) &&
+ qw_callback_is_allowed( $field['custom_output_callback'], 'callback field' )
+ ) {
if ( isset( $field['include_output_arguments'] ) ) {
$returned = $field['custom_output_callback']( $post,
$field,
@@ -86,13 +88,15 @@
<label class="qw-label">Callback:</label>
<input class='qw-js-title' type="text"
name="<?php print $field['form_prefix']; ?>[custom_output_callback]"
- value="<?php print $custom_output_callback; ?>"/>
+ value="<?php print esc_attr( $custom_output_callback ); ?>"/>
</p>
<p class="description">
Provide an existing function name. This function will be executed
during the loop of this query.
</p>
+
+ <?php qw_callback_status_notice( $custom_output_callback ); ?>
</div>
<div>
<label class='qw-field-checkbox'>
--- a/query-wrangler/includes/filters/callback.php
+++ b/query-wrangler/includes/filters/callback.php
@@ -36,7 +36,7 @@
type='text'
size="46"
name="<?php print $filter['form_prefix']; ?>[callback]"
- value='<?php print $filter['values']['callback']; ?>'/>
+ value='<?php print esc_attr( $filter['values']['callback'] ); ?>'/>
</p>
<p class="description">
The callback function will be provided the $args and $filter variables,
@@ -44,6 +44,7 @@
<br/>Eg, <code>function my_filter_callback($args, $filter){ return
$args; }</code>
</p>
+ <?php qw_callback_status_notice( $filter['values']['callback'] ); ?>
<?php
}
@@ -54,7 +55,9 @@
* @param $filter
*/
function qw_filter_callback_execute( &$args, $filter ) {
- if ( isset( $filter['values']['callback'] ) && function_exists( $filter['values']['callback'] ) ) {
+ if ( isset( $filter['values']['callback'] ) &&
+ qw_callback_is_allowed( $filter['values']['callback'], 'callback filter' )
+ ) {
$args = $filter['values']['callback']( $args, $filter );
}
}
No newline at end of file
--- a/query-wrangler/includes/filters/post_id.php
+++ b/query-wrangler/includes/filters/post_id.php
@@ -31,12 +31,13 @@
?>
<p>
<label>Provide post_ids as a comma separated list:</label>
- <div><input class="qw-js-title" type='text' size="46" name="<?php print $filter['form_prefix']; ?>[post_ids]" value='<?php print $filter['values']['post_ids']; ?>' /></div>
+ <div><input class="qw-js-title" type='text' size="46" name="<?php print $filter['form_prefix']; ?>[post_ids]" value='<?php print esc_attr( $filter['values']['post_ids'] ); ?>' /></div>
</p>
<p>
<label>Or, provide a callback function name that returns an array of post_ids:</label>
- <div><input class="qw-js-title" type="text" size="46" name="<?php print $filter['form_prefix']; ?>[post_ids_callback]" value="<?php print $filter['values']['post_ids_callback']; ?>" /></div>
+ <div><input class="qw-js-title" type="text" size="46" name="<?php print $filter['form_prefix']; ?>[post_ids_callback]" value="<?php print esc_attr( $filter['values']['post_ids_callback'] ); ?>" /></div>
<p class="description">Note: you cannot expose a filter if using a callback.</p>
+ <?php qw_callback_status_notice( $filter['values']['post_ids_callback'] ); ?>
</p>
<p>
<label>How to treat these post IDs.</label>
@@ -53,7 +54,9 @@
}
function qw_generate_query_args_post_id( &$args, $filter ) {
- if ( isset( $filter['values']['post_ids_callback'] ) && function_exists( $filter['values']['post_ids_callback'] ) ) {
+ if ( isset( $filter['values']['post_ids_callback'] ) &&
+ qw_callback_is_allowed( $filter['values']['post_ids_callback'], 'post_ids callback' )
+ ) {
$pids = $filter['values']['post_ids_callback']( $args );
} else {
$values = qw_contextual_tokens_replace( $filter['values']['post_ids'] );
--- a/query-wrangler/query-wrangler.php
+++ b/query-wrangler/query-wrangler.php
@@ -9,7 +9,7 @@
Description: Query Wrangler provides an intuitive interface for creating complex WP queries as pages or widgets. Based on Drupal Views.
Author: Jonathan Daggerhart
Author URI: https://www.daggerhartlab.com
-Version: 1.5.57
+Version: 1.5.58
License: GPLv2
License URI: http://www.gnu.org/licenses/gpl-2.0.html
@@ -32,7 +32,7 @@
*/
// some useful definitions
-define( 'QW_VERSION', 1.557 );
+define( 'QW_VERSION', 1.558 );
define( 'QW_PLUGIN_DIR', dirname( __FILE__ ) );
define( 'QW_PLUGIN_URL', plugins_url( '', __FILE__ ) );
define( 'QW_DEFAULT_THEME', 'views' );
@@ -58,6 +58,8 @@
}
// Wordpress hooks
include_once QW_PLUGIN_DIR . '/includes/hooks.php';
+ // controls for executing administrator-provided callbacks
+ include_once QW_PLUGIN_DIR . '/includes/callbacks.php';
include_once QW_PLUGIN_DIR . '/includes/exposed.php';
include_once QW_PLUGIN_DIR . '/includes/handlers.php';
include_once QW_PLUGIN_DIR . '/includes/class-qw-shortcodes.php';