'spfaq_faq',
'post_status' => 'publish',
'fields' => 'ids',
'no_found_rows' => true,
)
);
foreach ( $ids as $id ) {
$p = get_post( $id );
++$stats[ SPFAQ_API_Mapper::sync_status( $p, SPFAQ_API_Mapper::content_hash( $p ) ) ];
}
echo '
' . esc_html__( 'INTEGRATION', 'spindle-faq' ) . '' . esc_html__( 'Knowledge API', 'spindle-faq' ) . '
' . esc_html__( 'Let companion plugins such as Spindle AI Live Chat read your published FAQs, send back unanswered questions and share helpfulness votes. Nothing unpublished or private is ever shared.', 'spindle-faq' ) . '
';
if ( SPFAQ_Request::has( 'saved' ) ) {
echo '
' . esc_html__( 'Settings saved.', 'spindle-faq' ) . '
';
}
if ( is_array( $shown ) ) {
delete_transient( 'spfaq_api_newkey_' . get_current_user_id() );
echo '
' . esc_html( 'read' === $shown['scope'] ? __( 'New read key', 'spindle-faq' ) : __( 'New write key', 'spindle-faq' ) ) . ': ' . esc_html( $shown['key'] ) . '
' . esc_html__( 'Copy it now. It is stored only as a hash and cannot be shown again.', 'spindle-faq' ) . '
';
}
echo '
' . esc_html__( 'Access', 'spindle-faq' ) . '
';
echo '
' . esc_html__( 'Keys', 'spindle-faq' ) . '
' . esc_html__( 'Send a key as "Authorization: Bearer KEY". The read key can only read. The write key can send suggestions, feedback and sync reports but cannot read.', 'spindle-faq' ) . '
';
foreach ( array(
'read' => __( 'Read key', 'spindle-faq' ),
'write' => __( 'Write key', 'spindle-faq' ),
) as $scope => $label ) {
$has = ! empty( $opt[ $scope . '_hash' ] );
echo '| ' . esc_html( $label ) . ' | ' . esc_html( $has ? __( 'Set', 'spindle-faq' ) : __( 'Not set', 'spindle-faq' ) ) . ' | ';
foreach ( array(
'generate' => $has ? __( 'Replace', 'spindle-faq' ) : __( 'Generate', 'spindle-faq' ),
'revoke' => __( 'Revoke', 'spindle-faq' ),
) as $op => $text ) {
if ( 'revoke' === $op && ! $has ) {
continue;
}
echo ' ';
}
echo ' |
';
}
echo '
';
echo '
' . esc_html__( 'Sync status', 'spindle-faq' ) . '
';
/* translators: 1: indexed count, 2: stale count, 3: pending count, 4: excluded count */
echo esc_html( sprintf( __( 'Indexed: %1$d · Stale: %2$d · Not indexed yet: %3$d · Excluded: %4$d', 'spindle-faq' ), $stats['indexed'], $stats['stale'], $stats['pending'], $stats['excluded'] ) );
echo '
' . esc_html__( 'Companion plugins report what they indexed through the sync endpoint. A FAQ is stale when it changed after it was indexed.', 'spindle-faq' ) . '
';
echo '
' . esc_html__( 'Endpoints', 'spindle-faq' ) . '
';
$routes = array(
array( 'GET', '/knowledge', __( 'Published FAQs. Parameters: page, per_page (max 100), updated_after, category.', 'spindle-faq' ) ),
array( 'GET', '/knowledge/{id}', __( 'One FAQ by stable ID or numeric ID.', 'spindle-faq' ) ),
array( 'GET', '/changes?since=ISO8601', __( 'FAQs changed since a time, plus removals.', 'spindle-faq' ) ),
array( 'POST', '/suggestions', __( 'Body: question, source, count. Creates a draft FAQ for an editor.', 'spindle-faq' ) ),
array( 'POST', '/feedback', __( 'Body: faq_id, vote (yes or no), session. Counts as a vote.', 'spindle-faq' ) ),
array( 'POST', '/sync', __( 'Body: items [{id, hash}]. Reports what was indexed.', 'spindle-faq' ) ),
);
foreach ( $routes as $r ) {
echo '' . esc_html( $r[0] ) . ' | ' . esc_html( $r[1] ) . ' | ' . esc_html( $r[2] ) . ' |
';
}
echo '
' . esc_html__( 'Base URL:', 'spindle-faq' ) . ' ' . esc_html( $base ) . '
';
echo '
';
}
/** Save access settings. */
public static function save_settings() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Permission denied', 'spindle-faq' ) );
}
check_admin_referer( 'spfaq_api_save' );
$opt = SPFAQ_API_Options::options();
$opt['enabled'] = ! empty( $_POST['enabled'] ) ? 1 : 0;
$opt['public_read'] = ! empty( $_POST['public_read'] ) ? 1 : 0;
update_option( SPFAQ_API_Options::OPTION, $opt, false );
wp_safe_redirect( admin_url( 'admin.php?page=spfaq-knowledge&saved=1' ) );
exit;
}
/** Generate or revoke an API key. */
public static function handle_key() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Permission denied', 'spindle-faq' ) );
}
check_admin_referer( 'spfaq_api_key' );
$scope = isset( $_POST['scope'] ) && 'write' === $_POST['scope'] ? 'write' : 'read';
$op = isset( $_POST['op'] ) && 'revoke' === $_POST['op'] ? 'revoke' : 'generate';
$opt = SPFAQ_API_Options::options();
if ( 'revoke' === $op ) {
$opt[ $scope . '_hash' ] = '';
} else {
$key = ( 'read' === $scope ? 'spfk_r_' : 'spfk_w_' ) . wp_generate_password( 40, false, false );
$opt[ $scope . '_hash' ] = SPFAQ_API_Auth::hash_key( $key );
$opt[ $scope . '_created' ] = time();
set_transient(
'spfaq_api_newkey_' . get_current_user_id(),
array(
'scope' => $scope,
'key' => $key,
),
5 * MINUTE_IN_SECONDS
);
}
update_option( SPFAQ_API_Options::OPTION, $opt, false );
wp_safe_redirect( admin_url( 'admin.php?page=spfaq-knowledge' ) );
exit;
}
}