From d672947964c467582a1be673c73fc86d5fb1b1ba Mon Sep 17 00:00:00 2001 From: Jeremy Rangel Date: Thu, 9 Jan 2025 23:23:49 -0800 Subject: [PATCH] Initial --- assets/css/admin.css | 164 ++++++++++++++ assets/js/admin.js | 171 +++++++++++++++ includes/class-lcp-paywall-admin.php | 314 +++++++++++++++++++++++++++ includes/class-lcp-paywall-rules.php | 77 +++++++ includes/class-lcp-paywall.php | 141 ++++++++++++ lcp-paywall.php | 28 +++ 6 files changed, 895 insertions(+) create mode 100644 assets/css/admin.css create mode 100644 assets/js/admin.js create mode 100644 includes/class-lcp-paywall-admin.php create mode 100644 includes/class-lcp-paywall-rules.php create mode 100644 includes/class-lcp-paywall.php create mode 100644 lcp-paywall.php diff --git a/assets/css/admin.css b/assets/css/admin.css new file mode 100644 index 0000000..8661fea --- /dev/null +++ b/assets/css/admin.css @@ -0,0 +1,164 @@ +.lcp-post-type-rules { + background: #fff; + padding: 20px; + margin: 20px 0; + border: 1px solid #ccd0d4; + box-shadow: 0 1px 1px rgba(0,0,0,.04); +} + +.post-type-settings { + background: #f9f9f9; + padding: 15px; + margin-bottom: 20px; + border: 1px solid #e5e5e5; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 15px; +} + +.setting-group { + display: flex; + flex-direction: column; + gap: 5px; +} + +.setting-group label { + font-weight: 600; +} + +.setting-group input[type="number"] { + width: 100px; +} + +/* Toggle Switch */ +.switch { + position: relative; + display: inline-block; + width: 50px; + height: 24px; +} + +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +.slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + transition: .4s; + border-radius: 24px; +} + +.slider:before { + position: absolute; + content: ""; + height: 16px; + width: 16px; + left: 4px; + bottom: 4px; + background-color: white; + transition: .4s; + border-radius: 50%; +} + +input:checked + .slider { + background-color: #2271b1; +} + +input:checked + .slider:before { + transform: translateX(26px); +} + +.lcp-rules-container { + margin: 15px 0; + min-height: 50px; + border: 1px solid #ddd; + padding: 10px; + background: #f8f8f8; +} + +.lcp-rule { + background: #fff; + border: 1px solid #ddd; + margin-bottom: 10px; + padding: 15px; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); +} + +.lcp-rule.ui-sortable-helper { + box-shadow: 0 2px 5px rgba(0,0,0,0.2); +} + +.rule-header { + display: flex; + align-items: center; + margin-bottom: 15px; + padding-bottom: 10px; + border-bottom: 1px solid #eee; +} + +.rule-header .dashicons-menu { + cursor: move; + color: #666; + margin-right: 15px; + font-size: 20px; +} + +.rule-header .delete-rule { + margin-left: auto; + color: #dc3232; + border: none; + background: none; + cursor: pointer; + padding: 0; +} + +.rule-header .delete-rule:hover { + color: #aa0000; +} + +.rule-body { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 10px; + align-items: start; +} + +.rule-body select { + width: 100%; + max-width: 200px; +} + +.add-rule { + margin-top: 10px !important; + margin-bottom: 20px !important; +} + +#save-rules { + margin-top: 20px; +} + +.post-type-actions { + display: flex; + gap: 10px; + margin-top: 15px; + padding-top: 15px; + border-top: 1px solid #ddd; +} + +.post-type-actions .button { + height: 35px; + line-height: 33px; + padding: 0 15px; +} + +.save-post-type-rules { + margin-left: auto; +} diff --git a/assets/js/admin.js b/assets/js/admin.js new file mode 100644 index 0000000..7271966 --- /dev/null +++ b/assets/js/admin.js @@ -0,0 +1,171 @@ +document.addEventListener('DOMContentLoaded', function() { + // Initialize Sortable for rule containers + document.querySelectorAll('.lcp-rules-container').forEach(container => { + new Sortable(container, { + handle: '.dashicons-menu', + animation: 150 + }); + }); + + // Add new rule + document.addEventListener('click', function(e) { + if (e.target.matches('.add-rule')) { + const postType = e.target.dataset.postType; + const template = document.querySelector('.lcp-rule-template').innerHTML; + const container = e.target.closest('.lcp-post-type-rules').querySelector('.lcp-rules-container'); + container.insertAdjacentHTML('beforeend', template); + } + }); + + // Delete rule + document.addEventListener('click', function(e) { + // Check if the click was on the delete button or its child icon + if (e.target.matches('.delete-rule') || e.target.closest('.delete-rule')) { + const ruleElement = e.target.closest('.lcp-rule'); + if (ruleElement) { + ruleElement.remove(); + } + } + }); + + // Update taxonomy and terms fields visibility + document.addEventListener('change', function(e) { + if (e.target.matches('.rule-value-type')) { + const ruleBody = e.target.closest('.rule-body'); + if (!ruleBody) return; // Exit if rule-body not found + + const taxonomySelect = ruleBody.querySelector('.rule-taxonomy'); + const termSelect = ruleBody.querySelector('.rule-term'); + + if (!taxonomySelect || !termSelect) return; // Exit if selects not found + + if (e.target.value === 'taxonomy') { + taxonomySelect.style.display = 'inline-block'; + termSelect.style.display = 'inline-block'; + } else { + taxonomySelect.style.display = 'none'; + termSelect.style.display = 'none'; + } + } + }); + + // Update terms when taxonomy changes + document.addEventListener('change', function(e) { + if (e.target.matches('.rule-taxonomy')) { + const termSelect = e.target.nextElementSibling; + if (!termSelect) return; // Exit if term select not found + + const taxonomy = e.target.value; + + fetch(lcpPaywall.ajaxurl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + action: 'lcp_get_taxonomy_terms', + nonce: lcpPaywall.nonce, + taxonomy: taxonomy + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + termSelect.innerHTML = ''; + data.data.forEach(term => { + const option = document.createElement('option'); + option.value = term.term_id; + option.textContent = term.name; + termSelect.appendChild(option); + }); + } + }); + } + }); + + // Save individual post type rules + document.addEventListener('click', function(e) { + if (e.target.matches('.save-post-type-rules')) { + const postTypeSection = e.target.closest('.lcp-post-type-rules'); + const postType = postTypeSection.dataset.postType; + + const rules = { + [postType]: { + settings: { + default_lock_status: postTypeSection.querySelector('.default-lock-status').value, + is_metered: postTypeSection.querySelector('.is-metered').checked, + metered_free_posts: parseInt(postTypeSection.querySelector('.metered-free-posts').value), + metered_interval: parseInt(postTypeSection.querySelector('.metered-interval').value) + }, + rules: [] + } + }; + + postTypeSection.querySelectorAll('.lcp-rule').forEach(ruleElement => { + const rule = { + action: ruleElement.querySelector('.rule-action').value, + condition: ruleElement.querySelector('.rule-condition').value, + operator: ruleElement.querySelector('.rule-operator').value, + value_type: ruleElement.querySelector('.rule-value-type').value, + taxonomy: ruleElement.querySelector('.rule-taxonomy')?.value || '', + term: ruleElement.querySelector('.rule-term')?.value || '' + }; + rules[postType].rules.push(rule); + }); + + fetch(lcpPaywall.ajaxurl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + action: 'lcp_save_rules', + nonce: lcpPaywall.nonce, + rules: JSON.stringify(rules) + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + alert(`Rules for ${postType} saved successfully!`); + } else { + alert(`Failed to save rules for ${postType}. Please try again.`); + } + }) + .catch(() => { + alert(`Failed to save rules for ${postType}. Please try again.`); + }); + } + }); + + // Save settings + document.addEventListener('click', function(e) { + if (e.target.matches('#save-settings')) { + // Get the content from the WordPress editor + const content = wp.editor.getContent('lcp_nag_message'); + + fetch(lcpPaywall.ajaxurl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + action: 'lcp_save_settings', + nonce: lcpPaywall.nonce, + nag_message: content + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + alert('Settings saved successfully!'); + } else { + alert('Failed to save settings. Please try again.'); + } + }) + .catch(() => { + alert('Failed to save settings. Please try again.'); + }); + } + }); +}); diff --git a/includes/class-lcp-paywall-admin.php b/includes/class-lcp-paywall-admin.php new file mode 100644 index 0000000..82c2961 --- /dev/null +++ b/includes/class-lcp-paywall-admin.php @@ -0,0 +1,314 @@ +rules = new LCP_Paywall_Rules(); + + add_action('admin_menu', array($this, 'add_menu_pages')); + add_action('admin_init', array($this, 'register_settings')); + add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_assets')); + add_action('wp_ajax_lcp_save_rules', array($this, 'ajax_save_rules')); + add_action('wp_ajax_lcp_get_taxonomy_terms', array($this, 'ajax_get_taxonomy_terms')); + add_action('wp_ajax_lcp_save_settings', array($this, 'ajax_save_settings')); + } + + public function add_menu_pages() { + add_menu_page( + 'LCP Paywall', + 'LCP Paywall', + 'manage_options', + 'lcp-paywall', + array($this, 'render_rules_page'), + 'dashicons-lock', + 30 + ); + + add_submenu_page( + 'lcp-paywall', + 'Paywall Settings', + 'Settings', + 'manage_options', + 'lcp-paywall-settings', + array($this, 'render_settings_page') + ); + } + + public function register_settings() { + register_setting('lcp_paywall_options', 'lcp_paywall_rules'); + register_setting('lcp_paywall_settings', 'lcp_paywall_settings'); + } + + public function enqueue_admin_assets($hook) { + if (!in_array($hook, array('toplevel_page_lcp-paywall', 'lcp-paywall_page_lcp-paywall-settings'))) { + return; + } + + wp_enqueue_style('lcp-paywall-admin'); + + // Enqueue WordPress editor assets + if ($hook === 'lcp-paywall_page_lcp-paywall-settings') { + wp_enqueue_editor(); + } + + // Enqueue Sortable.js + wp_enqueue_script( + 'sortablejs', + 'https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js', + array(), + '1.15.0', + true + ); + + wp_enqueue_script( + 'lcp-paywall-admin', + LCP_PAYWALL_URL . 'assets/js/admin.js', + array('sortablejs'), + LCP_PAYWALL_VERSION, + true + ); + + wp_localize_script('lcp-paywall-admin', 'lcpPaywall', array( + 'ajaxurl' => admin_url('admin-ajax.php'), + 'nonce' => wp_create_nonce('lcp_paywall_nonce'), + 'operators' => $this->rules->get_operators(), + 'conditions' => $this->rules->get_conditions(), + 'valueTypes' => $this->rules->get_value_types(), + )); + } + + public function render_rules_page() { + if (!current_user_can('manage_options')) { + return; + } + + $post_types = get_post_types(array('public' => true), 'objects'); + $current_rules = $this->rules->get_rules(); + ?> +
+

+ +
+ rules->get_default_post_type_settings($post_type->name); + ?> +
+

labels->name); ?>

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ name]['rules'])) { + foreach ($current_rules[$post_type->name]['rules'] as $rule) { + $this->render_rule_template($post_type, $rule); + } + } + ?> +
+ +
+ + +
+
+ + + +
+
+ +
+

+ +
+

Paywall Nag Message

+

Configure the message that will be shown to users when content is locked.

+ +
+ 'lcp_nag_message', + 'media_buttons' => true, + 'textarea_rows' => 10, + 'teeny' => false + ) + ); + ?> +
+ +

+ +

+
+
+ +
+
+ + + + +
+ +
+ + + + + + + + + + + +
+
+ rules->save_rules($rules)) { + wp_send_json_success('Rules saved successfully'); + } else { + wp_send_json_error('Failed to save rules'); + } + } + + public function ajax_get_taxonomy_terms() { + if (!current_user_can('manage_options')) { + wp_send_json_error('Unauthorized'); + } + + check_ajax_referer('lcp_paywall_nonce', 'nonce'); + + $taxonomy = isset($_POST['taxonomy']) ? sanitize_text_field($_POST['taxonomy']) : ''; + + if (!$taxonomy) { + wp_send_json_error('Invalid taxonomy'); + } + + $terms = $this->rules->get_terms_for_taxonomy($taxonomy); + wp_send_json_success($terms); + } + + public function ajax_save_settings() { + if (!current_user_can('manage_options')) { + wp_send_json_error('Unauthorized'); + } + + check_ajax_referer('lcp_paywall_nonce', 'nonce'); + + $nag_message = isset($_POST['nag_message']) ? wp_kses_post(stripslashes($_POST['nag_message'])) : ''; + + $settings = array( + 'nag_message' => $nag_message + ); + + if (update_option('lcp_paywall_settings', $settings)) { + wp_send_json_success('Settings saved successfully'); + } else { + wp_send_json_error('Failed to save settings'); + } + } +} diff --git a/includes/class-lcp-paywall-rules.php b/includes/class-lcp-paywall-rules.php new file mode 100644 index 0000000..7f65989 --- /dev/null +++ b/includes/class-lcp-paywall-rules.php @@ -0,0 +1,77 @@ +option_name, array()); + } + + public function get_rules_for_post_type($post_type) { + $rules = $this->get_rules(); + return isset($rules[$post_type]) ? $rules[$post_type] : array(); + } + + public function save_rules($rules) { + // Ensure settings exist for each post type + foreach ($rules as $post_type => $data) { + if (!isset($data['settings'])) { + $rules[$post_type]['settings'] = $this->get_default_post_type_settings($post_type); + } + } + return update_option($this->option_name, $rules); + } + + public function get_operators() { + return array( + '=' => 'Equals', + '!=' => 'Not Equals', + 'in' => 'In', + 'notin' => 'Not In', + 'is' => 'Is', + 'isnot' => 'Is Not', + 'olderthan' => 'Older Than', + 'newerthan' => 'Newer Than', + 'hassub' => 'Has Subscription' + ); + } + + public function get_conditions() { + return array( + 'user' => 'User', + 'post' => 'Post' + ); + } + + public function get_value_types() { + return array( + 'logged_in' => 'Logged In', + 'taxonomy' => 'Taxonomy' + ); + } + + public function get_taxonomies_for_post_type($post_type) { + return get_object_taxonomies($post_type, 'objects'); + } + + public function get_terms_for_taxonomy($taxonomy) { + $terms = get_terms(array( + 'taxonomy' => $taxonomy, + 'hide_empty' => false, + )); + + return is_wp_error($terms) ? array() : $terms; + } + + public function get_default_post_type_settings($post_type) { + $rules = $this->get_rules(); + return isset($rules[$post_type]['settings']) ? $rules[$post_type]['settings'] : array( + 'default_lock_status' => 'locked', + 'is_metered' => false, + 'metered_free_posts' => 0, + 'metered_interval' => 0 + ); + } +} diff --git a/includes/class-lcp-paywall.php b/includes/class-lcp-paywall.php new file mode 100644 index 0000000..714d992 --- /dev/null +++ b/includes/class-lcp-paywall.php @@ -0,0 +1,141 @@ +rules = new LCP_Paywall_Rules(); + + if (is_admin()) { + $this->admin = new LCP_Paywall_Admin(); + } + + add_action('init', array($this, 'init_hooks')); + add_filter('the_content', array($this, 'maybe_restrict_content'), 999); + } + + public function init_hooks() { + // Register scripts and styles + add_action('admin_enqueue_scripts', array($this, 'register_admin_assets')); + } + + public function register_admin_assets() { + wp_register_style( + 'lcp-paywall-admin', + LCP_PAYWALL_URL . 'assets/css/admin.css', + array(), + LCP_PAYWALL_VERSION + ); + + wp_register_script( + 'lcp-paywall-admin', + LCP_PAYWALL_URL . 'assets/js/admin.js', + array('jquery', 'jquery-ui-sortable'), + LCP_PAYWALL_VERSION, + true + ); + } + + public function maybe_restrict_content($content) { + if (!is_singular()) return $content; + + $post_type = get_post_type(); + $post_id = get_the_ID(); + + if ($this->should_restrict_access($post_type, $post_id)) { + return $this->get_nag_message(); + } + + return $content; + } + + private function should_restrict_access($post_type, $post_id) { + $post_type_rules = $this->rules->get_rules_for_post_type($post_type); + + // Get default lock status + $default_lock_status = isset($post_type_rules['settings']['default_lock_status']) ? + $post_type_rules['settings']['default_lock_status'] === 'locked' : true; + + // If no rules exist, use default lock status + if (empty($post_type_rules['rules'])) { + return $default_lock_status; + } + + // Check each rule in order until one evaluates to true + foreach ($post_type_rules['rules'] as $rule) { + if ($this->evaluate_rule($rule, $post_id)) { + // First matching rule determines the lock status + return $rule['action'] === 'lock'; + } + } + + // If no rules match, use default lock status + return $default_lock_status; + } + + private function evaluate_rule($rule, $post_id) { + if ($rule['condition'] === 'user') { + return $this->evaluate_user_rule($rule); + } else if ($rule['condition'] === 'post') { + return $this->evaluate_post_rule($rule, $post_id); + } + return false; + } + + private function evaluate_user_rule($rule) { + if ($rule['value_type'] === 'logged_in') { + $is_logged_in = is_user_logged_in(); + + switch ($rule['operator']) { + case '=': + case 'is': + return $is_logged_in; + case '!=': + case 'isnot': + return !$is_logged_in; + default: + return false; + } + } + return false; + } + + private function evaluate_post_rule($rule, $post_id) { + if ($rule['value_type'] === 'taxonomy' && !empty($rule['taxonomy']) && !empty($rule['term'])) { + $terms = wp_get_post_terms($post_id, $rule['taxonomy'], array('fields' => 'ids')); + + if (is_wp_error($terms)) { + return false; + } + + switch ($rule['operator']) { + case '=': + case 'in': + return in_array((int)$rule['term'], $terms); + case '!=': + case 'notin': + return !in_array((int)$rule['term'], $terms); + default: + return false; + } + } + return false; + } + + private function get_nag_message() { + $settings = get_option('lcp_paywall_settings', array()); + $message = isset($settings['nag_message']) ? $settings['nag_message'] : ''; + + if (empty($message)) { + $message = '
+

This content is locked

+

Please upgrade your subscription to access this content.

+
'; + } + + return apply_filters('lcp_paywall_nag_message', $message); + } +} diff --git a/lcp-paywall.php b/lcp-paywall.php new file mode 100644 index 0000000..dd3bc39 --- /dev/null +++ b/lcp-paywall.php @@ -0,0 +1,28 @@ +init(); +} +add_action('plugins_loaded', 'lcp_paywall_init');