Create Release / build (push) Successful in 5s
Fourthwall moved the product title from <h2> to <h1>, which silently emptied [fourthwall_single] and [fourthwall_random]: both gated rendering on a title match that could no longer succeed. Verified against a live store. Parsing - Read schema.org JSON-LD first, falling back to scraped markup field by field, then to og: meta tags. Each field degrades independently, so a future markup change can only affect what it actually touched. - Match CSS classes with a padded contains() predicate instead of @class="...", and select on data-testid where available. Exact class matching is what broke. - Surface sku, availability and currency, which were previously discarded. Product discovery - Enumerate products from sitemap.xml rather than scraping the store page, which only sees the collection it features. [fourthwall_random count="10"] could not return more than the 3 items a featured homepage renders. - New source="auto|sitemap|page" attribute. Off-host <loc> entries are rejected so a hostile sitemap cannot redirect fetches. Caching - Stale-while-revalidate: stale entries are served immediately and refreshed by WP-Cron, so cache expiry never costs a visitor a network round trip. - Refresh scheduling dedupes via wp_next_scheduled and takes a lock, collapsing a stampede to a single job. - Fetch uncached product pages concurrently with curl_multi instead of serially. - Cache lifetime is now configurable (default 60 minutes). - Entries written by earlier versions are read as stale and upgraded in place, so caches turn over without blanking a store. Fixes - Build absolute product links properly. "/collections/all" + "/products/x" produced 404s; a trailing slash produced "//products/x". - Escape the price on output; it was interpolated raw. - strpos() misuse meant an error string at offset 0 was treated as success. - Guard a null description node that would fatal when show_description="true". - Report non-200 responses instead of caching an empty body silently. - Drop the shared /tmp/cookies.txt jar. It was cross-site mutable state and made concurrent fetching unsafe; all endpoints return 200 without it. - Remove the ssl_verify setting. It was a development affordance and its checkbox never worked - unchecked meant "key absent", which read as true. Certificate verification is now pinned on. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
185 lines
6.8 KiB
PHP
185 lines
6.8 KiB
PHP
<?php
|
|
|
|
class fourthwall_settings {
|
|
|
|
public function __construct() {
|
|
|
|
add_action( 'admin_menu', array( $this, 'add_admin_menu' ) );
|
|
add_action( 'admin_init', array( $this, 'init_settings' ) );
|
|
|
|
}
|
|
|
|
public function add_admin_menu() {
|
|
|
|
add_menu_page(
|
|
esc_html__( 'Fourthwall Store Embed', 'fourthwall_text_domain' ),
|
|
esc_html__( 'FourthWall Embed', 'fourthwall_text_domain' ),
|
|
'manage_options',
|
|
'fwembed',
|
|
array( $this, 'fourthwall_page_layout' ),
|
|
'dashicons-store',
|
|
10
|
|
);
|
|
|
|
}
|
|
|
|
public function init_settings() {
|
|
|
|
register_setting(
|
|
'fourthwall_settings_group',
|
|
'fourthwall_settings_name',
|
|
array( 'sanitize_callback' => array( $this, 'sanitize_settings' ) )
|
|
);
|
|
|
|
add_settings_section(
|
|
'fourthwall_settings_name_section',
|
|
'',
|
|
false,
|
|
'fourthwall_settings_name'
|
|
);
|
|
|
|
add_settings_field(
|
|
'fourth_url',
|
|
__( 'Store URL', 'fourthwall_text_domain' ),
|
|
array( $this, 'render_fourth_url_field' ),
|
|
'fourthwall_settings_name',
|
|
'fourthwall_settings_name_section'
|
|
);
|
|
|
|
add_settings_field(
|
|
'cache_ttl',
|
|
__( 'Cache Lifetime', 'fourthwall_text_domain' ),
|
|
array( $this, 'render_cache_ttl_field' ),
|
|
'fourthwall_settings_name',
|
|
'fourthwall_settings_name_section'
|
|
);
|
|
|
|
}
|
|
|
|
/**
|
|
* Sanitize settings before they are stored.
|
|
*
|
|
* @param array $input Raw submitted values
|
|
* @return array Cleaned values
|
|
*/
|
|
public function sanitize_settings( $input ) {
|
|
|
|
$input = is_array( $input ) ? $input : array();
|
|
$output = array();
|
|
|
|
if ( isset( $input['fourth_url'] ) ) {
|
|
$output['fourth_url'] = esc_url_raw( trim( $input['fourth_url'] ) );
|
|
}
|
|
|
|
$ttl = isset( $input['cache_ttl'] ) ? intval( $input['cache_ttl'] ) : 60;
|
|
$output['cache_ttl'] = $ttl < 1 ? 60 : $ttl;
|
|
|
|
return $output;
|
|
|
|
}
|
|
|
|
public function fourthwall_page_layout() {
|
|
|
|
// Check required user capability
|
|
if ( !current_user_can( 'manage_options' ) ) {
|
|
wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'fourthwall_text_domain' ) );
|
|
}
|
|
|
|
// Handle cache clearing
|
|
if (isset($_POST['clear_cache']) && wp_verify_nonce($_POST['_wpnonce'], 'clear_fwembed_cache')) {
|
|
$this->clear_fwembed_cache();
|
|
echo '<div class="notice notice-success"><p>' . __('Cache cleared successfully!', 'fourthwall_text_domain') . '</p></div>';
|
|
}
|
|
|
|
// Admin Page Layout
|
|
echo '<div class="wrap">' . "\n";
|
|
echo ' <h1>' . get_admin_page_title() . '</h1>' . "\n";
|
|
echo ' <form action="options.php" method="post">' . "\n";
|
|
|
|
settings_fields( 'fourthwall_settings_group' );
|
|
do_settings_sections( 'fourthwall_settings_name' );
|
|
submit_button();
|
|
|
|
echo ' </form>' . "\n";
|
|
|
|
// Cache clearing form
|
|
echo ' <form method="post" style="margin-top: 20px;">' . "\n";
|
|
echo ' ' . wp_nonce_field('clear_fwembed_cache', '_wpnonce', true, false) . "\n";
|
|
echo ' <input type="submit" name="clear_cache" class="button button-secondary" value="' . __('Clear Cache', 'fourthwall_text_domain') . '">' . "\n";
|
|
echo ' <p class="description">' . __('Clear the cached Fourthwall content. Use this if products are not updating.', 'fourthwall_text_domain') . '</p>' . "\n";
|
|
echo ' </form>' . "\n";
|
|
|
|
echo ' <div class="shortcode-info">' . "\n";
|
|
echo ' <h3>' . __( 'Shortcode Usage', 'fourthwall_text_domain' ) . '</h3>' . "\n";
|
|
echo ' <p><strong>' . __( 'Display entire store:', 'fourthwall_text_domain' ) . '</strong> <code>[fourthwall]</code></p>' . "\n";
|
|
echo ' <p><strong>' . __( 'Display single product:', 'fourthwall_text_domain' ) . '</strong> <code>[fourthwall_single url="https://your-store.fourthwall.com/products/product-name"]</code></p>' . "\n";
|
|
echo ' <p><strong>' . __( 'With description:', 'fourthwall_text_domain' ) . '</strong> <code>[fourthwall_single url="https://your-store.fourthwall.com/products/product-name" show_description="true"]</code></p>' . "\n";
|
|
echo ' <p><strong>' . __( 'Display random products:', 'fourthwall_text_domain' ) . '</strong> <code>[fourthwall_random count="5"]</code></p>' . "\n";
|
|
echo ' <p><strong>' . __( 'Random from specific URLs:', 'fourthwall_text_domain' ) . '</strong> <code>[fourthwall_random count="3" urls="https://store.com/product1,https://store.com/product2,https://store.com/product3"]</code></p>' . "\n";
|
|
echo ' <p><strong>' . __( 'Random from different store:', 'fourthwall_text_domain' ) . '</strong> <code>[fourthwall_random count="2" store_url="https://different-store.fourthwall.com"]</code></p>' . "\n";
|
|
echo ' <p><strong>' . __( 'Random from the store page instead of the sitemap:', 'fourthwall_text_domain' ) . '</strong> <code>[fourthwall_random count="3" source="page"]</code></p>' . "\n";
|
|
echo ' <p><em>' . __( 'Random products are drawn from your store sitemap, which covers your whole catalog. Set source="page" to only use products shown on the store page itself.', 'fourthwall_text_domain' ) . '</em></p>' . "\n";
|
|
echo ' <p><em>' . __( 'Tip: point the Store URL at your /collections/all page to list every product with [fourthwall].', 'fourthwall_text_domain' ) . '</em></p>' . "\n";
|
|
echo ' </div>' . "\n";
|
|
echo '</div>' . "\n";
|
|
|
|
}
|
|
|
|
function render_fourth_url_field() {
|
|
|
|
// Retrieve data from the database.
|
|
$options = get_option( 'fourthwall_settings_name' );
|
|
|
|
// Set default value.
|
|
$value = isset( $options['fourth_url'] ) ? $options['fourth_url'] : '';
|
|
|
|
// Field output.
|
|
echo '<input type="url" name="fourthwall_settings_name[fourth_url]" class="regular-text fourth_url_field" placeholder="' . esc_attr__( 'https://some-shop.fourthwall.com', 'fourthwall_text_domain' ) . '" value="' . esc_attr( $value ) . '">';
|
|
echo '<p class="description">' . __( 'The fourth wall URL', 'fourthwall_text_domain' ) . '</p>';
|
|
|
|
}
|
|
|
|
function render_cache_ttl_field() {
|
|
|
|
// Retrieve data from the database.
|
|
$options = get_option( 'fourthwall_settings_name' );
|
|
|
|
// Set default value.
|
|
$value = isset( $options['cache_ttl'] ) ? intval( $options['cache_ttl'] ) : 60;
|
|
if ( $value < 1 ) {
|
|
$value = 60;
|
|
}
|
|
|
|
// Field output.
|
|
echo '<input type="number" min="1" step="1" name="fourthwall_settings_name[cache_ttl]" class="small-text" value="' . esc_attr( $value ) . '"> ' . esc_html__( 'minutes', 'fourthwall_text_domain' );
|
|
echo '<p class="description">' . __( 'How long store content stays fresh before it is refreshed (default: 60). Expired content is still shown instantly while it refreshes in the background, so raising this does not slow your pages down.', 'fourthwall_text_domain' ) . '</p>';
|
|
|
|
}
|
|
|
|
/**
|
|
* Clear all Fourthwall embed cache
|
|
*/
|
|
private function clear_fwembed_cache() {
|
|
global $wpdb;
|
|
|
|
// Delete all transients that start with 'fwembed_'
|
|
$wpdb->query(
|
|
$wpdb->prepare(
|
|
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
|
|
'_transient_fwembed_%'
|
|
)
|
|
);
|
|
|
|
// Also delete transient timeouts
|
|
$wpdb->query(
|
|
$wpdb->prepare(
|
|
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
|
|
'_transient_timeout_fwembed_%'
|
|
)
|
|
);
|
|
}
|
|
|
|
}
|
|
|
|
new fourthwall_settings;
|