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
4 changes: 4 additions & 0 deletions assets/css/admin.css
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,10 @@
text-decoration: none;
}

.aggregate-it .ai-tabs {
margin: 12px 0 20px;
}

.aggregate-it .ai-preview {
margin: 8px 0;
max-width: 880px;
Expand Down
139 changes: 102 additions & 37 deletions src/Admin/Admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public function register(): void {
add_action( 'admin_post_aggregate_it_approve_hub', [ $this, 'handle_approve_hub' ] );
add_action( 'admin_post_aggregate_it_trash_hub', [ $this, 'handle_trash_hub' ] );
add_action( 'admin_post_aggregate_it_save_settings', [ $this, 'handle_save_settings' ] );
add_action( 'admin_post_aggregate_it_save_rules', [ $this, 'handle_save_rules' ] );
add_action( 'admin_post_aggregate_it_dismiss_setup', [ $this, 'handle_dismiss_setup' ] );
add_action( 'admin_post_aggregate_it_retry_article', [ $this, 'handle_retry_article' ] );
add_action( 'admin_post_aggregate_it_retry_failed', [ $this, 'handle_retry_failed' ] );
Expand Down Expand Up @@ -136,15 +137,6 @@ public function menu(): void {
self::SLUG . '-settings',
[ $this, 'render_settings' ]
);

$this->hooks[] = add_submenu_page(
self::SLUG,
__( 'Tools', 'aggregate-it' ),
__( 'Tools', 'aggregate-it' ),
'manage_options',
self::SLUG . '-tools',
[ $this, 'render_tools' ]
);
}

public function assets( string $hook ): void {
Expand Down Expand Up @@ -765,8 +757,98 @@ private function is_entity_post( int $id ): bool {
}

public function render_settings(): void {
$tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'general'; // phpcs:ignore WordPress.Security.NonceVerification
$tabs = [
'general' => __( 'General', 'aggregate-it' ),
'rules' => __( 'Rules', 'aggregate-it' ),
'tools' => __( 'Tools', 'aggregate-it' ),
];
if ( ! isset( $tabs[ $tab ] ) ) {
$tab = 'general';
}

$settings = $this->plugin->settings();
require AGGREGATE_IT_PATH . 'src/Admin/views/settings.php';

if ( $tab === 'rules' ) {
$public_types = get_post_types( [ 'public' => true ], 'objects' );
unset( $public_types['attachment'] );
$global = $settings->global_rules();
$rtype = isset( $_GET['rtype'] ) ? sanitize_key( wp_unslash( $_GET['rtype'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
if ( ! isset( $public_types[ $rtype ] ) ) {
$saved_types = array_values( array_filter( array_keys( $global ), static fn ( $type ): bool => isset( $public_types[ $type ] ) ) );
$rtype = (string) ( $saved_types[0] ?? ( isset( $public_types['post'] ) ? 'post' : (string) array_key_first( $public_types ) ) );
}
Comment thread
nmbrthirteen marked this conversation as resolved.
$rules = (array) ( $global[ $rtype ] ?? [] );
$saved = isset( $_GET['ai_notice'] ) && sanitize_key( wp_unslash( $_GET['ai_notice'] ) ) === 'saved'; // phpcs:ignore WordPress.Security.NonceVerification
$rule_ops = self::rule_op_labels();
}

if ( $tab === 'tools' ) {
$flash = get_transient( 'aggregate_it_flash' );
if ( $flash ) {
delete_transient( 'aggregate_it_flash' );
}
$flash_type = 'success';
if ( is_array( $flash ) ) {
$flash_type = sanitize_key( (string) ( $flash['type'] ?? 'success' ) );
$flash = (string) ( $flash['message'] ?? '' );
}
$blacklist = $settings->blacklist_raw();
$events = ActivityLog::recent( 200 );
$info = $this->system_info();
}

echo '<div class="wrap aggregate-it">';
echo '<div class="ai-head"><h1>' . esc_html__( 'Settings', 'aggregate-it' ) . '</h1></div>';
echo '<nav class="nav-tab-wrapper ai-tabs">';
foreach ( $tabs as $key => $label ) {
printf(
'<a href="%s" class="nav-tab%s">%s</a>',
esc_url( admin_url( 'admin.php?page=' . self::SLUG . '-settings&tab=' . $key ) ),
$tab === $key ? ' nav-tab-active' : '',
esc_html( $label )
);
}
echo '</nav>';

$tab_embedded = true;
$view = [ 'general' => 'settings.php', 'rules' => 'settings-rules.php', 'tools' => 'tools.php' ][ $tab ];
require AGGREGATE_IT_PATH . 'src/Admin/views/' . $view;

echo '</div>';
}

/** @return array<string,string> rule operator => label */
public static function rule_op_labels(): array {
return [
'always' => __( 'always', 'aggregate-it' ),
'equals' => __( 'equals', 'aggregate-it' ),
'not_equals' => __( 'does not equal', 'aggregate-it' ),
'contains' => __( 'contains', 'aggregate-it' ),
'not_contains' => __( 'does not contain', 'aggregate-it' ),
'empty' => __( 'is empty', 'aggregate-it' ),
'not_empty' => __( 'is not empty', 'aggregate-it' ),
'date_past' => __( 'date is in the past', 'aggregate-it' ),
'date_future' => __( 'date is in the future', 'aggregate-it' ),
'gt' => __( 'greater than', 'aggregate-it' ),
'lt' => __( 'less than', 'aggregate-it' ),
];
}

public function handle_save_rules(): void {
$this->guard( 'aggregate_it_save_rules' );

$rtype = sanitize_key( wp_unslash( $_POST['rtype'] ?? '' ) );
$allowed = get_post_types( [ 'public' => true ], 'names' );
unset( $allowed['attachment'] );
if ( $rtype === '' || ! in_array( $rtype, $allowed, true ) ) {
$this->redirect( self::SLUG . '-settings', '', [ 'tab' => 'rules' ] );
}

$this->plugin->settings()->set_global_rules( $rtype, $this->rules_from_post() );
\AggregateIt\Maintenance\GlobalRulesRefresher::schedule_soon();

$this->redirect( self::SLUG . '-settings', 'saved', [ 'tab' => 'rules', 'rtype' => $rtype ] );
}

public function handle_save_settings(): void {
Expand Down Expand Up @@ -839,23 +921,6 @@ public function render_activity(): void {
require AGGREGATE_IT_PATH . 'src/Admin/views/activity.php';
}

public function render_tools(): void {
$settings = $this->plugin->settings();
$flash = get_transient( 'aggregate_it_flash' );
if ( $flash ) {
delete_transient( 'aggregate_it_flash' );
}
$flash_type = 'success';
if ( is_array( $flash ) ) {
$flash_type = sanitize_key( (string) ( $flash['type'] ?? 'success' ) );
$flash = (string) ( $flash['message'] ?? '' );
}
$blacklist = $settings->blacklist_raw();
$events = ActivityLog::recent( 200 );
$info = $this->system_info();
require AGGREGATE_IT_PATH . 'src/Admin/views/tools.php';
}

public function handle_bulk_add_sources(): void {
$this->guard( 'aggregate_it_bulk_add_sources' );

Expand All @@ -875,15 +940,15 @@ public function handle_bulk_add_sources(): void {

/* translators: %d: number of feeds added */
$this->flash( sprintf( _n( '%d feed added.', '%d feeds added.', $added, 'aggregate-it' ), $added ) );
$this->redirect( self::SLUG . '-tools', '' );
$this->redirect( self::SLUG . '-settings', '', [ 'tab' => 'tools' ] );
}

public function handle_save_blacklist(): void {
$this->guard( 'aggregate_it_save_blacklist' );

$this->plugin->settings()->set( 'blacklist', sanitize_textarea_field( wp_unslash( $_POST['blacklist'] ?? '' ) ) );
$this->flash( __( 'Blacklist saved.', 'aggregate-it' ) );
$this->redirect( self::SLUG . '-tools', '' );
$this->redirect( self::SLUG . '-settings', '', [ 'tab' => 'tools' ] );
}

public function handle_export_config(): void {
Expand Down Expand Up @@ -919,23 +984,23 @@ public function handle_import_config(): void {
$upload = $_FILES['config'] ?? null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
if ( ! is_array( $upload ) ) {
$this->flash( __( 'Choose a JSON or XML file to import.', 'aggregate-it' ), 'error' );
$this->redirect( self::SLUG . '-tools', '' );
$this->redirect( self::SLUG . '-settings', '', [ 'tab' => 'tools' ] );
}

$raw = $this->read_import_upload( $upload );
if ( $raw === null ) {
$this->redirect( self::SLUG . '-tools', '' );
$this->redirect( self::SLUG . '-settings', '', [ 'tab' => 'tools' ] );
}

if ( str_starts_with( ltrim( $raw ), '<' ) ) {
$this->flash( $this->import_wxr_config( $raw ) );
$this->redirect( self::SLUG . '-tools', '' );
$this->redirect( self::SLUG . '-settings', '', [ 'tab' => 'tools' ] );
}

$data = json_decode( $raw, true );
if ( ! is_array( $data ) ) {
$this->flash( __( 'That file is not valid JSON or WordPress XML.', 'aggregate-it' ), 'error' );
$this->redirect( self::SLUG . '-tools', '' );
$this->redirect( self::SLUG . '-settings', '', [ 'tab' => 'tools' ] );
}

if ( isset( $data['aggregate_it_export'] ) ) {
Expand All @@ -952,14 +1017,14 @@ public function handle_import_config(): void {
}
}

$this->redirect( self::SLUG . '-tools', '' );
$this->redirect( self::SLUG . '-settings', '', [ 'tab' => 'tools' ] );
}

public function handle_clear_logs(): void {
$this->guard( 'aggregate_it_clear_logs' );
ActivityLog::clear();
$this->flash( __( 'Activity log cleared.', 'aggregate-it' ) );
$this->redirect( self::SLUG . '-tools', '' );
$this->redirect( self::SLUG . '-settings', '', [ 'tab' => 'tools' ] );
}

public function handle_reset(): void {
Expand Down Expand Up @@ -1000,7 +1065,7 @@ public function handle_reset(): void {
$this->flash( __( 'Nothing was reset.', 'aggregate-it' ) );
}

$this->redirect( self::SLUG . '-tools', '' );
$this->redirect( self::SLUG . '-settings', '', [ 'tab' => 'tools' ] );
}

private function flash( string $message, string $type = 'success' ): void {
Expand Down Expand Up @@ -1142,7 +1207,7 @@ private function import_wxr_config( string $raw ): string {

if ( $xml === false || ! isset( $xml->channel->item ) ) {
$this->flash( __( 'That file is not valid WordPress XML.', 'aggregate-it' ), 'error' );
$this->redirect( self::SLUG . '-tools', '' );
$this->redirect( self::SLUG . '-settings', '', [ 'tab' => 'tools' ] );
}

$sources = [];
Expand Down
4 changes: 3 additions & 1 deletion src/Admin/views/activity.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
};
?>
<div class="wrap aggregate-it">
<h1><?php esc_html_e( 'Activity', 'aggregate-it' ); ?></h1>
<div class="ai-head">
<h1><?php esc_html_e( 'Activity', 'aggregate-it' ); ?></h1>
</div>

<form method="get" class="ai-act-filters">
<input type="hidden" name="page" value="aggregate-it-activity">
Expand Down
124 changes: 124 additions & 0 deletions src/Admin/views/settings-rules.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<?php

namespace AggregateIt\Admin;

defined( 'ABSPATH' ) || exit;

/**
* @var object[] $public_types
* @var string $rtype
* @var array<int,array<string,string>> $rules
* @var array<string,string> $rule_ops
* @var bool $saved
*/

if ( ! $rules ) {
$rules = [ [ 'field' => '', 'op' => 'always', 'value' => '', 'set_key' => '', 'set_value' => '' ] ];
}

$render_rule_row = static function ( array $rule, array $rule_ops ): void {
?>
<tr class="ai-rule">
<td class="ai-rule-handle" title="<?php esc_attr_e( 'Drag to reorder', 'aggregate-it' ); ?>">⠿</td>
<td><input type="text" name="rule_field[]" list="ai-source-fields" value="<?php echo esc_attr( (string) ( $rule['field'] ?? '' ) ); ?>" placeholder="event_date"></td>
<td>
<select name="rule_op[]">
<?php foreach ( $rule_ops as $val => $label ) : ?>
<option value="<?php echo esc_attr( $val ); ?>" <?php selected( (string) ( $rule['op'] ?? 'always' ), $val ); ?>><?php echo esc_html( $label ); ?></option>
<?php endforeach; ?>
</select>
</td>
<td><input type="text" name="rule_value[]" value="<?php echo esc_attr( (string) ( $rule['value'] ?? '' ) ); ?>"></td>
<td><input type="text" name="rule_set_key[]" list="ai-meta-keys" value="<?php echo esc_attr( (string) ( $rule['set_key'] ?? '' ) ); ?>" placeholder="event_status"></td>
<td><input type="text" name="rule_set_value[]" value="<?php echo esc_attr( (string) ( $rule['set_value'] ?? '' ) ); ?>" placeholder="Upcoming or {event_date|Y-m-d}"></td>
<td><button type="button" class="button-link ai-rule-del" title="<?php esc_attr_e( 'Remove', 'aggregate-it' ); ?>">✕</button></td>
</tr>
<?php
};

$base = admin_url( 'admin.php?page=aggregate-it-settings&tab=rules' );
?>
<?php if ( $saved ) : ?>
<div class="notice notice-success is-dismissible"><p><?php esc_html_e( 'Rules saved. They’ll apply to existing posts shortly.', 'aggregate-it' ); ?></p></div>
<?php endif; ?>

<p>
<label for="ai-rtype"><strong><?php esc_html_e( 'Rules for post type', 'aggregate-it' ); ?></strong></label>
<select id="ai-rtype" onchange="window.location.href='<?php echo esc_url( $base ); ?>&rtype='+this.value;">
<?php foreach ( $public_types as $pt ) : ?>
<option value="<?php echo esc_attr( $pt->name ); ?>" <?php selected( $rtype, $pt->name ); ?>><?php echo esc_html( $pt->labels->singular_name ?? $pt->name ); ?> (<?php echo esc_html( $pt->name ); ?>)</option>
<?php endforeach; ?>
</select>
</p>

<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
<input type="hidden" name="action" value="aggregate_it_save_rules">
<input type="hidden" name="rtype" value="<?php echo esc_attr( $rtype ); ?>">
<?php wp_nonce_field( 'aggregate_it_save_rules' ); ?>

<table class="ai-rules-table widefat" id="ai-rules">
<thead><tr>
<th></th>
<th><?php esc_html_e( 'If field (meta key)', 'aggregate-it' ); ?></th>
<th><?php esc_html_e( 'Condition', 'aggregate-it' ); ?></th>
<th><?php esc_html_e( 'Value', 'aggregate-it' ); ?></th>
<th><?php esc_html_e( 'Set meta key', 'aggregate-it' ); ?></th>
<th><?php esc_html_e( 'To value', 'aggregate-it' ); ?></th>
<th></th>
</tr></thead>
<tbody id="ai-rules-body">
<?php foreach ( $rules as $rule ) : ?>
<?php $render_rule_row( (array) $rule, $rule_ops ); ?>
<?php endforeach; ?>
</tbody>
</table>
<p>
<button type="button" class="button" id="ai-rule-add"><?php esc_html_e( 'Add rule', 'aggregate-it' ); ?></button>
<button type="submit" class="button button-primary"><?php esc_html_e( 'Save rules', 'aggregate-it' ); ?></button>
</p>
<datalist id="ai-source-fields"></datalist>
<datalist id="ai-meta-keys"></datalist>
<template id="ai-rule-template"><?php $render_rule_row( [ 'op' => 'always' ], $rule_ops ); ?></template>
</form>

<script>
( function () {
var cfg = window.AggregateItAdmin || {};
var body = document.getElementById( 'ai-rules-body' );
var add = document.getElementById( 'ai-rule-add' );
var tpl = document.getElementById( 'ai-rule-template' );
var rtype = <?php echo wp_json_encode( $rtype ); ?>;

function bind( row ) {
var del = row.querySelector( '.ai-rule-del' );
if ( del ) { del.addEventListener( 'click', function () { row.remove(); } ); }
}
if ( body ) { body.querySelectorAll( 'tr.ai-rule' ).forEach( bind ); }
if ( add && body && tpl ) {
add.addEventListener( 'click', function () {
body.appendChild( tpl.content.cloneNode( true ) );
bind( body.lastElementChild );
if ( window.jQuery && window.jQuery( body ).is( ':data(ui-sortable)' ) ) { window.jQuery( body ).sortable( 'refresh' ); }
} );
}

if ( cfg.root && rtype ) {
fetch( cfg.root + 'post-type-fields?type=' + encodeURIComponent( rtype ), { headers: { 'X-WP-Nonce': cfg.nonce } } )
.then( function ( r ) { return r.json(); } ).then( function ( data ) {
if ( ! data || ! data.fields ) { return; }
[ 'ai-source-fields', 'ai-meta-keys' ].forEach( function ( id ) {
var dl = document.getElementById( id );
if ( ! dl ) { return; }
dl.innerHTML = '';
data.fields.forEach( function ( k ) { var o = document.createElement( 'option' ); o.value = k; dl.appendChild( o ); } );
} );
} ).catch( function () {} );
}

window.addEventListener( 'load', function () {
if ( window.jQuery && window.jQuery.fn.sortable && body ) {
window.jQuery( body ).sortable( { handle: '.ai-rule-handle', items: '> tr', axis: 'y' } );
}
} );
} )();
</script>
6 changes: 0 additions & 6 deletions src/Admin/views/settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,6 @@
$keyword_list = implode( "\n", $settings->keyword_list() );
$has_key = $settings->api_key() !== '';
?>
<div class="wrap aggregate-it">
<div class="ai-head">
<h1><?php esc_html_e( 'Aggregate It — Settings', 'aggregate-it' ); ?></h1>
</div>

<?php if ( $notice === 'saved' ) : ?>
<div class="notice notice-success is-dismissible"><p><?php esc_html_e( 'Settings saved.', 'aggregate-it' ); ?></p></div>
<?php endif; ?>
Expand Down Expand Up @@ -272,4 +267,3 @@ function sync() {
}
} )();
</script>
</div>
Loading
Loading