From a7627eb6e53cd353d0ef4d6d10e5da4092f3b877 Mon Sep 17 00:00:00 2001
From: Ajay D'Souza
Date: Fri, 20 Dec 2024 12:29:27 +0000
Subject: [PATCH 001/119] Update unit-tests.yml
---
.github/workflows/unit-tests.yml | 7 ++-----
1 file changed, 2 insertions(+), 5 deletions(-)
diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml
index 017a66c..3d5f0c1 100644
--- a/.github/workflows/unit-tests.yml
+++ b/.github/workflows/unit-tests.yml
@@ -23,7 +23,7 @@ jobs:
matrix:
# Notes regarding supported versions in WP:
# The base matrix only contains the PHP versions which are supported on all supported WP versions.
- php: ['8.0', '7.3', '7.4']
+ php: ['8.0', '8.1', '7.4']
wp: ['latest']
experimental: [false]
@@ -37,11 +37,8 @@ jobs:
- php: '8.2'
wp: 'latest'
experimental: true
- - php: '8.1'
- wp: 'latest'
- experimental: true
- php: '8.0'
- wp: '5.9'
+ wp: '6.3'
experimental: true
name: "PHP ${{ matrix.php }} - WP ${{ matrix.wp }}"
From 0fd0d74cb565da280809f0d277dd66be32832209 Mon Sep 17 00:00:00 2001
From: Ajay D'Souza
Date: Sat, 21 Dec 2024 16:18:01 +0000
Subject: [PATCH 002/119] Fix default image in related posts.
---
includes/functions.php | 2 +-
knowledgebase.php | 2 +-
readme.txt | 9 +++++++--
3 files changed, 9 insertions(+), 4 deletions(-)
diff --git a/includes/functions.php b/includes/functions.php
index f78cc9b..d1d9a7d 100644
--- a/includes/functions.php
+++ b/includes/functions.php
@@ -153,7 +153,7 @@ function wzkb_get_the_post_thumbnail( $args = array() ) {
$defaults = array(
'post' => get_post(),
- 'thumb_default' => __DIR__ . '/frontend/images/default-thumb.png',
+ 'thumb_default' => plugins_url( 'frontend/images/default-thumb.png', __FILE__ ),
'class' => 'wzkb-relatd-article-thumb',
'size' => 'thumbnail',
'scan_images' => true,
diff --git a/knowledgebase.php b/knowledgebase.php
index 25857d1..6ea55d6 100644
--- a/knowledgebase.php
+++ b/knowledgebase.php
@@ -13,7 +13,7 @@
* Plugin Name: WebberZone Knowledge Base
* Plugin URI: https://github.com/WebberZone/knowledgebase
* Description: Create a multi-product knowledge base on your WordPress site.
- * Version: 2.3.0
+ * Version: 2.3.1-beta1
* Author: WebberZone
* Author URI: https://webberzone.com
* License: GPL-2.0+
diff --git a/readme.txt b/readme.txt
index a9f3e4e..950890c 100644
--- a/readme.txt
+++ b/readme.txt
@@ -5,7 +5,7 @@ Tags: knowledge base, knowledgebase, FAQ, frequently asked questions, support, d
Requires at least: 6.3
Tested up to: 6.7
Requires PHP: 7.4
-Stable tag: 2.3.0
+Stable tag: 2.3.1
License: GPLv2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
@@ -113,11 +113,16 @@ You can report security bugs through the Patchstack Vulnerability Disclosure Pro
== Upgrade Notice ==
-= 2.3.0 =
+= 2.3.1 =
Completely rewritten. Several new features and enhancements.
== Changelog ==
+= 2.3.1
+
+* Bug fix:
+ * Default thumbnail didn't work properly in the related posts.
+
= 2.3.0 =
Release post: [https://webberzone.com/blog/knowledge-base-v2-3-0/](https://webberzone.com/blog/knowledge-base-v2-3-0/)
From 2291867a93425de2a48201bc0e9c4b233525aa5c Mon Sep 17 00:00:00 2001
From: Ajay D'Souza
Date: Fri, 11 Apr 2025 20:09:13 +0100
Subject: [PATCH 003/119] Update PHP and PHPUnit versions, fix test workflow
and improve build process
---
.github/workflows/cs.yml | 4 ++--
.github/workflows/unit-tests.yml | 38 ++++++++++++++++----------------
.github/workflows/zipitup.yml | 10 ++++-----
phpunit/bootstrap.php | 11 ++++-----
phpunit/install.sh | 2 +-
5 files changed, 31 insertions(+), 34 deletions(-)
diff --git a/.github/workflows/cs.yml b/.github/workflows/cs.yml
index e6ed796..39f0dc9 100644
--- a/.github/workflows/cs.yml
+++ b/.github/workflows/cs.yml
@@ -26,7 +26,7 @@ jobs:
- name: Install PHP
uses: shivammathur/setup-php@v2
with:
- php-version: '7.4'
+ php-version: '8.0'
tools: cs2pr
coverage: none
@@ -50,4 +50,4 @@ jobs:
run: vendor/bin/phpcs --report-full --report-checkstyle=./phpcs-report.xml
- name: Show PHPCS results in PR
- run: cs2pr --graceful-warnings ./phpcs-report.xml
+ run: cs2pr --graceful-warnings ./phpcs-report.xml
\ No newline at end of file
diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml
index 3d5f0c1..ce0e304 100644
--- a/.github/workflows/unit-tests.yml
+++ b/.github/workflows/unit-tests.yml
@@ -37,9 +37,12 @@ jobs:
- php: '8.2'
wp: 'latest'
experimental: true
- - php: '8.0'
+ - php: '8.2'
wp: '6.3'
experimental: true
+ - php: '8.0'
+ wp: '5.9'
+ experimental: true
name: "PHP ${{ matrix.php }} - WP ${{ matrix.wp }}"
@@ -68,23 +71,18 @@ jobs:
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
- tools: phpunit-polyfills
+ extensions: mysqli, mysql
coverage: none
- # The PHP 5.6 and 7.0 images don't include mysql[i] by default.
- extensions: mysqli
-
- - name: Set up WordPress
- run: phpunit/install.sh wordpress_test root '' 127.0.0.1:3306 ${{ matrix.wp }}
# On WP 5.2, PHPUnit 5.x, 6.x and 7.x are supported.
# On PHP >= 8.0, PHPUnit 7.5+ is needed, no matter what.
- name: Determine supported PHPUnit version
id: set_phpunit
run: |
- if [[ "${{ matrix.php }}" > "7.4" ]]; then
- echo "PHPUNIT=8.5.*" >> $GITHUB_ENV
+ if [[ "${{ matrix.php }}" > "8.0" ]]; then
+ echo "PHPUNIT=9.*" >> $GITHUB_ENV
else
- echo "PHPUNIT=5.7.*||6.*||7.5.*" >> $GITHUB_ENV
+ echo "PHPUNIT=5.7.*||6.*||7.5.*||8.5.*" >> $GITHUB_ENV
fi
- name: 'Composer: set up PHPUnit'
@@ -96,30 +94,32 @@ jobs:
# @link https://github.com/marketplace/actions/install-composer-dependencies
- name: Install Composer dependencies for PHP < 8.0
if: ${{ matrix.php < 8.0 }}
- uses: "ramsey/composer-install@v3"
+ uses: "ramsey/composer-install@v2"
# For the PHP 8.0 and above, we need to install with ignore platform reqs as not all dependencies allow it yet.
- name: Install Composer dependencies for PHP >= 8.0
if: ${{ matrix.php >= 8.0 }}
- uses: "ramsey/composer-install@v3"
+ uses: "ramsey/composer-install@v2"
with:
composer-options: --ignore-platform-reqs
- - name: 'Run Composer Update'
- run: |
- composer update --ignore-platform-reqs
+ - name: Install Subversion
+ run: sudo apt-get install subversion
+
+ - name: Set up WordPress
+ run: phpunit/install.sh wordpress_test root '' 127.0.0.1:3306 ${{ matrix.wp }}
- name: Tool versions
run: |
php --version
composer --version
- phpunit --version
- which phpunit
+ ./vendor/bin/phpunit --version
+ which ./vendor/bin/phpunit
- name: Run the unit tests - single site
- run: vendor/bin/phpunit
+ run: ./vendor/bin/phpunit
- name: Run the unit tests - multisite
- run: vendor/bin/phpunit
env:
WP_MULTISITE: 1
+ run: ./vendor/bin/phpunit
\ No newline at end of file
diff --git a/.github/workflows/zipitup.yml b/.github/workflows/zipitup.yml
index ef83cfa..e4b9355 100644
--- a/.github/workflows/zipitup.yml
+++ b/.github/workflows/zipitup.yml
@@ -29,15 +29,15 @@ jobs:
- name: Create artifact
uses: montudor/action-zip@v1
with:
- args: zip -X -r build/knowledgebase.zip . -x *.git* node_modules/\* .* "*/\.*" CODE_OF_CONDUCT.md CONTRIBUTING.md ISSUE_TEMPLATE.md PULL_REQUEST_TEMPLATE.md *.dist *.yml composer.* dev-helpers** build** wporg-assets** phpunit**
+ args: zip -X -r build/${{ github.event.repository.name }}.zip . -x *.git* node_modules/\* .* "*/\.*" CODE_OF_CONDUCT.md CONTRIBUTING.md ISSUE_TEMPLATE.md PULL_REQUEST_TEMPLATE.md *.dist *.yml *.neon composer.* package.json dev-helpers** build** wporg-assets** phpunit**
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
- name: knowledgebase
- path: build/knowledgebase.zip
+ name: ${{ github.event.repository.name }}
+ path: build/${{ github.event.repository.name }}.zip
- name: Upload to release
uses: JasonEtco/upload-to-release@master
with:
- args: build/knowledgebase.zip application/zip
+ args: build/${{ github.event.repository.name }}.zip application/zip
env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
\ No newline at end of file
diff --git a/phpunit/bootstrap.php b/phpunit/bootstrap.php
index bf54b88..eaf045a 100644
--- a/phpunit/bootstrap.php
+++ b/phpunit/bootstrap.php
@@ -1,14 +1,8 @@
Date: Fri, 11 Apr 2025 20:10:47 +0100
Subject: [PATCH 004/119] Bump version to 3.0.0 and update PHPUnit requirements
---
composer.json | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/composer.json b/composer.json
index bf81ab6..d25c650 100644
--- a/composer.json
+++ b/composer.json
@@ -1,7 +1,7 @@
{
"name": "webberzone/knowledgebase",
"description": "Fastest way to create a highly-flexible multi-product knowledge base.",
- "version": "2.3.0",
+ "version": "3.0.0",
"type": "wordpress-plugin",
"keywords": [
"knowledge base",
@@ -30,12 +30,18 @@
"dealerdirect/phpcodesniffer-composer-installer": "^1",
"phpcompatibility/phpcompatibility-wp": "^2",
"yoast/phpunit-polyfills": "^3",
- "phpunit/phpunit": "^5.7.21 || ^6.5 || ^7.5"
+ "phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11 || ^12"
},
"config": {
"allow-plugins": {
"phpstan/extension-installer": true,
"dealerdirect/phpcodesniffer-composer-installer": true
}
+ },
+ "scripts": {
+ "phpstan": "vendor/bin/phpstan analyse --memory-limit=2048M",
+ "phpstan-baseline": "vendor/bin/phpstan analyse --generate-baseline --memory-limit=2048M",
+ "phpcs": "vendor/bin/phpcs -p -v -s --standard=phpcs.xml.dist $(find . -name '*.php')",
+ "phpcbf": "vendor/bin/phpcbf -p -v -s --standard=phpcs.xml.dist $(find . -name '*.php')"
}
}
\ No newline at end of file
From e60d2bab1018a1f7997e57ad992c89c7772c2e24 Mon Sep 17 00:00:00 2001
From: Ajay D'Souza
Date: Thu, 24 Apr 2025 23:03:17 +0100
Subject: [PATCH 005/119] New product taxonomy feature
---
includes/admin/class-admin-columns.php | 191 +++-
includes/admin/class-admin.php | 59 +-
includes/admin/class-product-migrator.php | 855 ++++++++++++++++++
.../admin/{settings => }/class-settings.php | 134 ++-
includes/admin/js/admin-scripts.js | 13 +-
includes/admin/js/admin-scripts.min.js | 2 +-
includes/admin/js/product-migrator.js | 188 ++++
includes/admin/js/product-migrator.min.js | 1 +
includes/admin/settings/class-metabox-api.php | 23 +-
.../admin/settings/class-settings-api.php | 373 ++++++--
.../admin/settings/class-settings-form.php | 348 ++++++-
.../settings/class-settings-sanitize.php | 157 +++-
includes/admin/settings/css/admin-style.css | 264 ++++++
.../admin/settings/css/admin-style.min.css | 1 +
.../settings/js/settings-admin-scripts.js | 2 +-
.../settings/js/settings-admin-scripts.min.js | 2 +-
.../admin/settings/js/taxonomy-suggest.js | 4 -
.../admin/settings/js/taxonomy-suggest.min.js | 2 +-
includes/admin/settings/js/tom-select-init.js | 106 +++
.../admin/settings/js/tom-select-init.min.js | 1 +
includes/admin/{settings => }/sidebar.php | 8 +-
includes/autoloader.php | 51 +-
includes/class-cpt.php | 222 +++--
includes/class-main.php | 63 +-
includes/class-options-api.php | 312 +++++++
includes/frontend/class-display.php | 286 ++++--
includes/frontend/class-shortcodes.php | 1 +
includes/frontend/class-template-handler.php | 53 +-
includes/frontend/css/wzkb-styles-rtl.css | 190 ++--
includes/frontend/css/wzkb-styles-rtl.min.css | 2 +-
includes/frontend/css/wzkb-styles.css | 43 +-
includes/frontend/css/wzkb-styles.min.css | 2 +-
.../templates/taxonomy-wzkb_category.php | 9 +-
.../templates/taxonomy-wzkb_product.html | 20 +
.../templates/taxonomy-wzkb_product.php | 60 ++
includes/options-api.php | 157 +---
knowledgebase.php | 22 +-
package.json | 8 +-
phpstan-baseline.neon | 27 +-
readme.txt | 27 +-
uninstall.php | 1 +
41 files changed, 3512 insertions(+), 778 deletions(-)
create mode 100644 includes/admin/class-product-migrator.php
rename includes/admin/{settings => }/class-settings.php (83%)
create mode 100644 includes/admin/js/product-migrator.js
create mode 100644 includes/admin/js/product-migrator.min.js
create mode 100644 includes/admin/settings/css/admin-style.css
create mode 100644 includes/admin/settings/css/admin-style.min.css
create mode 100644 includes/admin/settings/js/tom-select-init.js
create mode 100644 includes/admin/settings/js/tom-select-init.min.js
rename includes/admin/{settings => }/sidebar.php (85%)
create mode 100644 includes/class-options-api.php
create mode 100644 includes/frontend/templates/taxonomy-wzkb_product.html
create mode 100644 includes/frontend/templates/taxonomy-wzkb_product.php
diff --git a/includes/admin/class-admin-columns.php b/includes/admin/class-admin-columns.php
index fa85579..42028c0 100644
--- a/includes/admin/class-admin-columns.php
+++ b/includes/admin/class-admin-columns.php
@@ -1,8 +1,6 @@
'ID',
);
+ // Only add Product column for wzkb_category taxonomy.
+ $screen = get_current_screen();
+ if ( isset( $screen->taxonomy ) && 'wzkb_category' === $screen->taxonomy ) {
+ $new_columns['product'] = __( 'Product', 'knowledgebase' );
+ }
+
return array_merge( $columns, $new_columns );
}
/**
- * Add taxonomy ID to the admin column.
+ * Make the Product column sortable.
+ *
+ * @since 3.0.0
+ *
+ * @param array $columns Array of sortable columns.
+ * @return array Modified array of sortable columns.
+ */
+ public function tax_sortable_columns( $columns ) {
+ $columns['product'] = 'product';
+ return $columns;
+ }
+
+ /**
+ * Add taxonomy ID and Product to the admin column.
*
* @since 2.3.0
*
@@ -67,6 +90,156 @@ public static function tax_columns( $columns ) {
* @return int|string
*/
public static function tax_id( $value, $name, $id ) {
- return 'tax_id' === $name ? $id : $value;
+ if ( 'tax_id' === $name ) {
+ return $id;
+ }
+ if ( 'product' === $name ) {
+ // Get linked product for this section.
+ $product_id = get_term_meta( $id, 'product_id', true );
+ if ( $product_id ) {
+ $product = get_term( $product_id, 'wzkb_product' );
+ if ( $product && ! is_wp_error( $product ) ) {
+ return sprintf(
+ '%s ',
+ esc_url( admin_url( 'edit.php?post_type=wz_knowledgebase&wzkb_product=' . $product->slug ) ),
+ esc_html( $product->name )
+ );
+ }
+ }
+ return '—'; // Em dash if not linked.
+ }
+ return $value;
+ }
+
+ /**
+ * Sort wzkb_category terms by wzkb_product name.
+ *
+ * @since 3.0.0
+ *
+ * @param array $pieces Array of query SQL clauses.
+ * @param array $taxonomies Array of taxonomy names.
+ * @return array Modified clauses.
+ */
+ public function sort_terms_by_product( $pieces, $taxonomies ) {
+ global $wpdb;
+
+ // Only run for wzkb_category in admin.
+ if ( ! is_admin() || ! in_array( 'wzkb_category', $taxonomies, true ) ) {
+ return $pieces;
+ }
+
+ // Check if sorting by product.
+ $orderby = isset( $_GET['orderby'] ) ? sanitize_text_field( wp_unslash( $_GET['orderby'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ if ( 'product' !== $orderby ) {
+ return $pieces;
+ }
+
+ // Get sort order.
+ $order = isset( $_GET['order'] ) ? strtoupper( sanitize_text_field( wp_unslash( $_GET['order'] ) ) ) : 'ASC'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ $order = in_array( $order, array( 'ASC', 'DESC' ), true ) ? $order : 'ASC';
+
+ // Join with termmeta to get product_id.
+ $pieces['join'] .= " LEFT JOIN {$wpdb->termmeta} AS tm ON t.term_id = tm.term_id AND tm.meta_key = 'product_id'";
+
+ // Join with terms and term_taxonomy to get wzkb_product name.
+ $pieces['join'] .= " LEFT JOIN {$wpdb->terms} AS pt ON tm.meta_value = pt.term_id";
+ $pieces['join'] .= " LEFT JOIN {$wpdb->term_taxonomy} AS ptt ON pt.term_id = ptt.term_id AND ptt.taxonomy = 'wzkb_product'";
+
+ // Set the ORDER BY clause with the "ORDER BY" prefix.
+ $pieces['orderby'] = "ORDER BY COALESCE(pt.name, '') $order, t.name $order";
+
+ // Prevent WordPress from appending the order.
+ $pieces['order'] = '';
+
+ return $pieces;
+ }
+
+ /**
+ * Add product filter dropdown to Knowledgebase admin screen.
+ *
+ * @since 3.0.0
+ */
+ public function add_product_filter_dropdown() {
+ global $pagenow;
+
+ // Only run on the edit.php page for wz_knowledgebase post type.
+ if ( 'edit.php' !== $pagenow || ! isset( $_GET['post_type'] ) || 'wz_knowledgebase' !== $_GET['post_type'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ return;
+ }
+
+ // Get all wzkb_product terms.
+ $terms = get_terms(
+ array(
+ 'taxonomy' => 'wzkb_product',
+ 'hide_empty' => false,
+ )
+ );
+
+ if ( empty( $terms ) || is_wp_error( $terms ) ) {
+ return;
+ }
+
+ // Get the currently selected product filter.
+ $selected = isset( $_GET['wzkb_product'] ) ? sanitize_text_field( wp_unslash( $_GET['wzkb_product'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+
+ // Output the dropdown.
+ ?>
+
+
+ %s',
+ esc_attr( $term->slug ),
+ selected( $selected, $term->slug, false ),
+ esc_html( $term->name )
+ );
+ }
+ ?>
+
+ is_main_query() ) {
+ return;
+ }
+
+ $post_type = isset( $_GET['post_type'] ) ? sanitize_text_field( wp_unslash( $_GET['post_type'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ if ( 'wz_knowledgebase' !== $post_type ) {
+ return;
+ }
+
+ // Get the product filter value.
+ $product = isset( $_GET['wzkb_product'] ) ? sanitize_text_field( wp_unslash( $_GET['wzkb_product'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ if ( empty( $product ) ) {
+ return;
+ }
+
+ // Ensure the taxonomy exists.
+ if ( ! taxonomy_exists( 'wzkb_product' ) ) {
+ return;
+ }
+
+ // Add the tax query.
+ $tax_query = array(
+ array(
+ 'taxonomy' => 'wzkb_product',
+ 'field' => is_numeric( $product ) ? 'term_id' : 'slug',
+ 'terms' => is_numeric( $product ) ? absint( $product ) : $product,
+ ),
+ );
+
+ $query->set( 'tax_query', $tax_query );
}
}
diff --git a/includes/admin/class-admin.php b/includes/admin/class-admin.php
index 5098217..29ef4c6 100644
--- a/includes/admin/class-admin.php
+++ b/includes/admin/class-admin.php
@@ -10,7 +10,6 @@
namespace WebberZone\Knowledge_Base\Admin;
use WebberZone\Knowledge_Base\Util\Cache;
-use WebberZone\Knowledge_Base\Admin\Activator;
// If this file is called directly, abort.
if ( ! defined( 'WPINC' ) ) {
@@ -60,6 +59,15 @@ class Admin {
*/
public $admin_columns;
+ /**
+ * Product Migrator class.
+ *
+ * @since 3.0.0
+ *
+ * @var object Product Migrator class.
+ */
+ public $product_migrator;
+
/**
* Main constructor class.
*
@@ -69,10 +77,11 @@ public function __construct() {
$this->hooks();
// Initialise admin classes.
- $this->settings = new Settings\Settings();
- $this->activator = new Activator();
- $this->cache = new Cache();
- $this->admin_columns = new Admin_Columns();
+ $this->settings = new Settings();
+ $this->activator = new Activator();
+ $this->cache = new Cache();
+ $this->admin_columns = new Admin_Columns();
+ $this->product_migrator = new Product_Migrator();
}
/**
@@ -98,28 +107,36 @@ public function admin_enqueue_scripts() {
$minimize = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
wp_register_script(
- 'wzkb-admin-js',
- plugins_url( 'js/admin-scripts' . $minimize . '.js', __FILE__ ),
+ 'wzkb-admin',
+ plugins_url( "js/admin-scripts{$minimize}.js", __FILE__ ),
array( 'jquery', 'jquery-ui-tabs' ),
WZKB_VERSION,
true
);
wp_localize_script(
- 'wzkb-admin-js',
- 'wzkb_admin',
+ 'wzkb-admin',
+ 'WZKBAdminData',
array(
- 'nonce' => wp_create_nonce( 'wzkb_admin_nonce' ),
+ 'ajax_url' => admin_url( 'admin-ajax.php' ),
+ 'security' => wp_create_nonce( 'wzkb-admin' ),
+ 'strings' => array(
+ 'confirm_message' => esc_html__( 'Are you sure you want to clear the cache?', 'knowledgebase' ),
+ 'success_message' => esc_html__( 'Cache cleared successfully!', 'knowledgebase' ),
+ 'fail_message' => esc_html__( 'Failed to clear cache. Please try again.', 'knowledgebase' ),
+ 'request_fail_message' => esc_html__( 'Request failed: ', 'knowledgebase' ),
+ ),
)
);
+
wp_register_style(
- 'wzkb-admin-ui-css',
- plugins_url( 'css/admin' . $minimize . '.css', __FILE__ ),
+ 'wzkb-admin-ui',
+ plugins_url( "css/admin{$minimize}.css", __FILE__ ),
array(),
WZKB_VERSION
);
if ( isset( $_GET['post_type'] ) && 'wz_knowledgebase' === $_GET['post_type'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended
- wp_enqueue_style( 'wzkb-admin-ui-css' );
+ wp_enqueue_style( 'wzkb-admin-ui' );
}
}
@@ -129,9 +146,10 @@ public function admin_enqueue_scripts() {
* @since 2.3.0
*/
public function admin_notices() {
- $kbslug = \wzkb_get_option( 'kb_slug', 'not-set-random-string' );
- $catslug = \wzkb_get_option( 'category_slug', 'not-set-random-string' );
- $tagslug = \wzkb_get_option( 'tag_slug', 'not-set-random-string' );
+ $kb_slug = \wzkb_get_option( 'kb_slug', 'not-set-random-string' );
+ $product_slug = \wzkb_get_option( 'product_slug', 'not-set-random-string' );
+ $cat_slug = \wzkb_get_option( 'category_slug', 'not-set-random-string' );
+ $tag_slug = \wzkb_get_option( 'tag_slug', 'not-set-random-string' );
// Only add the notice if the user is an admin.
if ( ! current_user_can( 'manage_options' ) ) {
@@ -139,13 +157,16 @@ public function admin_notices() {
}
// Only add the notice if the settings cannot be found.
- if ( 'not-set-random-string' === $kbslug || 'not-set-random-string' === $catslug || 'not-set-random-string' === $tagslug ) {
+ if ( 'not-set-random-string' === $kb_slug || 'not-set-random-string' === $product_slug || 'not-set-random-string' === $cat_slug || 'not-set-random-string' === $tag_slug ) {
?>
admin page to update and save the options.', 'knowledgebase' ), esc_url( admin_url( 'edit.php?post_type=wz_knowledgebase&page=wzkb-settings' ) ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
+ printf(
+ /* translators: 1. Link to admin page. */
+ esc_html__( 'Knowledge Base settings for the slug have not been registered. Please visit the admin page to update and save the options.', 'knowledgebase' ),
+ esc_url( admin_url( 'edit.php?post_type=wz_knowledgebase&page=wzkb-settings' ) )
+ );
?>
diff --git a/includes/admin/class-product-migrator.php b/includes/admin/class-product-migrator.php
new file mode 100644
index 0000000..2c867bd
--- /dev/null
+++ b/includes/admin/class-product-migrator.php
@@ -0,0 +1,855 @@
+
+
+ ' . esc_html__( 'New Multi-Products Mode available!', 'knowledgebase' ) . '
+ ' . esc_html__( 'Organize your knowledge base by product with our new Multi-Products mode! You can migrate your existing content using the migration wizard. If you don\'t want to use this feature, you can dismiss this notice by saving the settings page.', 'knowledgebase' ) . '
+
+
+ ' .
+ esc_html__( 'Enable Multi-Products', 'knowledgebase' ) .
+ ' ';
+
+ // Only show Migration Wizard link if migration is not completed yet.
+ if ( ! $migration_complete ) {
+ echo ' ' .
+ esc_html__( 'Migration Wizard', 'knowledgebase' ) .
+ ' ';
+ }
+
+ echo '
+ ';
+ }
+
+ /**
+ * Register the migration wizard admin page (submenu under KB, but no visible link unless you add one).
+ */
+ public function register_migration_wizard_page() {
+ $migration_complete = get_option( 'wzkb_product_migration_complete', false );
+ if ( $migration_complete ) {
+ return;
+ }
+ $this->menu_page = add_submenu_page(
+ 'edit.php?post_type=wz_knowledgebase',
+ esc_html__( 'Product Migration', 'knowledgebase' ),
+ esc_html__( 'Product Migration', 'knowledgebase' ),
+ 'manage_options',
+ 'wzkb-product-migration',
+ array( $this, 'render_migration_wizard' ),
+ );
+ add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
+ }
+
+ /**
+ * Enqueue scripts for migration wizard.
+ *
+ * @param string $hook The current admin screen hook.
+ */
+ public function enqueue_scripts( $hook ) {
+ if ( $this->menu_page !== $hook ) {
+ return;
+ }
+ $minimize = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
+
+ wp_enqueue_script(
+ 'wzkb-product-migrator',
+ plugins_url( "js/product-migrator{$minimize}.js", __FILE__ ),
+ array( 'jquery' ),
+ WZKB_VERSION,
+ true
+ );
+ wp_localize_script(
+ 'wzkb-product-migrator',
+ 'wzkbProductMigrator',
+ array(
+ 'nonce' => wp_create_nonce( 'wzkb_product_migration' ),
+ 'strings' => array(
+ 'migration_failed' => esc_html__( 'Migration failed', 'knowledgebase' ),
+ 'unknown_error' => esc_html__( 'Unknown error', 'knowledgebase' ),
+ 'migration_complete' => esc_html__( 'Migration complete!', 'knowledgebase' ),
+ ),
+ )
+ );
+ }
+
+ /**
+ * Render the migration wizard screen and handle migration logic.
+ */
+ public function render_migration_wizard() {
+
+ $migration_complete = get_option( 'wzkb_product_migration_complete', false );
+
+ // Verify user capabilities.
+ if ( ! current_user_can( 'manage_options' ) ) {
+ wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'knowledgebase' ) );
+ }
+
+ ob_start();
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ false,
+ 'progress' => 0,
+ 'message' => '',
+ 'next_step' => $step,
+ 'state' => $state,
+ 'errors' => array(),
+ 'dry_run' => $dry_run,
+ 'log' => array(),
+ );
+
+ switch ( $step ) {
+ case 0:
+ delete_transient( 'wzkb_migration_log' );
+ delete_transient( 'wzkb_migration_assigned_articles' );
+ delete_transient( 'wzkb_migration_article_counts' );
+
+ $log = array();
+
+ $response_data['message'] = '' . __( 'Initializing migration...', 'knowledgebase' ) . ' ';
+
+ if ( $dry_run ) {
+ $log[] = '' . __( 'Dry run mode: No changes will be made.', 'knowledgebase' ) . ' ';
+ }
+
+ $top_sections = get_terms(
+ array(
+ 'taxonomy' => $this->taxonomy_section,
+ 'hide_empty' => false,
+ 'parent' => 0,
+ 'fields' => 'all',
+ )
+ );
+
+ if ( is_wp_error( $top_sections ) ) {
+ wp_send_json_error(
+ sprintf(
+ /* translators: %s: Error message */
+ __( 'Error fetching sections: %s', 'knowledgebase' ),
+ $top_sections->get_error_message()
+ )
+ );
+ }
+
+ $top_section_ids = array();
+ $section_article_counts = array();
+ $unique_article_ids = array();
+ $total_articles = 0;
+ $total_descendant_count = 0;
+
+ foreach ( $top_sections as $section ) {
+ $section_id = (int) $section->term_id;
+ $top_section_ids[] = $section_id;
+ $descendant_ids = $this->get_all_descendant_section_ids( $section_id, $this->taxonomy_section );
+ $total_descendant_count += count( $descendant_ids );
+ $all_section_ids = array_merge( array( $section_id ), $descendant_ids );
+
+ foreach ( $all_section_ids as $sid ) {
+ $articles = get_posts(
+ array(
+ 'post_type' => $this->post_type_article,
+ 'posts_per_page' => -1,
+ 'fields' => 'ids',
+ 'tax_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
+ array(
+ 'taxonomy' => $this->taxonomy_section,
+ 'field' => 'term_id',
+ 'terms' => $sid,
+ ),
+ ),
+ 'suppress_filters' => true,
+ )
+ );
+ $section_article_counts[ $sid ] = count( $articles );
+ foreach ( $articles as $article_id ) {
+ if ( ! in_array( $article_id, $unique_article_ids, true ) ) {
+ $unique_article_ids[] = $article_id;
+ ++$total_articles;
+ }
+ }
+ }
+ }
+
+ $state['top_section_ids'] = $top_section_ids;
+ $state['section_to_product_map'] = array();
+ $state['total_articles'] = $total_articles;
+ $state['articles_processed'] = 0;
+ $state['sections_mapped'] = 0;
+ $state['top_sections_mapped'] = 0;
+ $state['total_descendant_count'] = $total_descendant_count;
+ $state['sections_deleted'] = 0;
+ $state['processed_section_ids'] = array();
+ $state['last_log_index'] = 0;
+
+ set_transient( 'wzkb_migration_article_counts', $section_article_counts, DAY_IN_SECONDS );
+ delete_transient( 'wzkb_migration_assigned_articles' );
+ set_transient( 'wzkb_migration_log', $log, DAY_IN_SECONDS );
+
+ $response_data['progress'] = 20;
+ $response_data['next_step'] = 1;
+ $response_data['state'] = $state;
+
+ if ( empty( $top_section_ids ) ) {
+ $response_data['done'] = true;
+ $response_data['progress'] = 100;
+ $response_data['message'] = __( 'No top-level sections found. Nothing to migrate.', 'knowledgebase' );
+ $log[] = $response_data['message'];
+ $response_data['log'] = $log;
+ $state['last_log_index'] = count( $log );
+ set_transient( 'wzkb_migration_log', $log, DAY_IN_SECONDS );
+ } else {
+ $response_data['log'] = $log;
+ $state['last_log_index'] = count( $log );
+ set_transient( 'wzkb_migration_log', $log, DAY_IN_SECONDS );
+ }
+ break;
+
+ case 1:
+ $response_data['message'] = '' . __( 'Creating products from top-level sections...', 'knowledgebase' ) . ' ';
+ $log[] = $response_data['message'];
+ $top_section_ids = $state['top_section_ids'] ?? array();
+ $section_to_product_map = $state['section_to_product_map'] ?? array();
+ $simulated_product_ids = $state['simulated_product_ids'] ?? array();
+
+ foreach ( $top_section_ids as $section_id ) {
+ $section = get_term( $section_id, $this->taxonomy_section );
+ if ( ! $section || is_wp_error( $section ) ) {
+ $log[] = sprintf(
+ /* translators: %d: Section ID */
+ __( 'Skipped invalid section ID: %d.', 'knowledgebase' ),
+ $section_id
+ );
+ continue;
+ }
+ $existing_product = get_term_by( 'slug', $section->slug, $this->taxonomy_product );
+ if ( $existing_product ) {
+ $product_id = (int) $existing_product->term_id;
+ $log[] = sprintf(
+ /* translators: 1: Section name, 2: Section ID, 3: Product ID */
+ __( 'Used existing product for section "%1$s" (ID: %2$d) with product ID: %3$d.', 'knowledgebase' ),
+ $section->name,
+ $section_id,
+ $product_id
+ );
+ } else {
+ $product = wp_insert_term(
+ $section->name,
+ $this->taxonomy_product,
+ array(
+ 'description' => $section->description,
+ 'slug' => $section->slug,
+ )
+ );
+ if ( is_wp_error( $product ) ) {
+ continue;
+ }
+ $product_id = (int) $product['term_id'];
+ if ( $dry_run ) {
+ $simulated_product_ids[] = $product_id;
+ }
+ $log[] = sprintf(
+ /* translators: 1: Product name, 2: Product ID, 3: Section name, 4: Section ID */
+ __( 'Created product "%1$s" (ID: %2$d) for section "%3$s" (ID: %4$d).', 'knowledgebase' ),
+ $section->name,
+ $product_id,
+ $section->name,
+ $section_id
+ );
+ }
+ $section_to_product_map[ $section_id ] = $product_id;
+ $state['top_sections_mapped'] = isset( $state['top_sections_mapped'] ) ? $state['top_sections_mapped'] + 1 : 1;
+ }
+
+ $state['section_to_product_map'] = $section_to_product_map;
+ $state['simulated_product_ids'] = $simulated_product_ids;
+ $response_data['log'] = $log;
+ $response_data['state'] = $state;
+ $response_data['progress'] = 20;
+ $response_data['next_step'] = 2;
+ $state['last_log_index'] = count( $log );
+ set_transient( 'wzkb_migration_log', $log, DAY_IN_SECONDS );
+ break;
+
+ case 2:
+ /**
+ * Filters the maximum number of sections processed per migration batch.
+ *
+ * Allows customization of migration batch section limit for server performance tuning.
+ *
+ * @since 3.0.0
+ * @param int $max_sections_per_batch Number of sections processed per batch. Default 3.
+ * @return int Filtered number of sections per batch.
+ */
+ $max_sections_per_batch = apply_filters( 'wzkb_migration_max_sections_per_batch', 3 );
+
+ /**
+ * Filters the maximum number of articles processed per migration batch.
+ *
+ * Allows customization of migration batch article limit for server performance tuning.
+ *
+ * @since 3.0.0
+ * @param int $max_articles_per_batch Number of articles processed per batch. Default 50.
+ * @return int Filtered number of articles per batch.
+ */
+ $max_articles_per_batch = apply_filters( 'wzkb_migration_max_articles_per_batch', 50 );
+
+ $response_data['message'] = '' . __( 'Mapping descendant sections and articles to products...', 'knowledgebase' ) . ' ';
+ $log[] = $response_data['message'];
+ $top_section_ids = $state['top_section_ids'] ?? array();
+ $section_to_product_map = $state['section_to_product_map'] ?? array();
+ $section_article_counts = is_array( get_transient( 'wzkb_migration_article_counts' ) ) ? get_transient( 'wzkb_migration_article_counts' ) : array();
+ $total_articles = $state['total_articles'] ?? 0;
+ $articles_processed = $state['articles_processed'] ?? 0;
+ $sections_mapped = $state['sections_mapped'] ?? 0;
+ $top_sections_mapped = $state['top_sections_mapped'] ?? 0;
+ $already_assigned_articles = is_array( get_transient( 'wzkb_migration_assigned_articles' ) ) ? get_transient( 'wzkb_migration_assigned_articles' ) : array();
+ $current_top_section_index = isset( $state['current_top_section_index'] ) ? (int) $state['current_top_section_index'] : 0;
+ $current_desc_section_index = isset( $state['current_desc_section_index'] ) ? (int) $state['current_desc_section_index'] : 0;
+ $current_article_offset = isset( $state['current_article_offset'] ) ? (int) $state['current_article_offset'] : 0;
+ $descendant_ids = $state['descendant_ids'] ?? array();
+ $last_log_index = isset( $state['last_log_index'] ) ? (int) $state['last_log_index'] : 0;
+
+ $log[] = sprintf(
+ /* translators: 1: Top index, 2: Descendant index, 3: Offset, 4: Descendant count, 5: Top sections count, 6: Articles processed, 7: Total articles */
+ __( 'Incoming State: top_index=%1$d, desc_index=%2$d, offset=%3$d, desc_count=%4$d, top_sections_count=%5$d, articles_processed=%6$d/%7$d', 'knowledgebase' ),
+ $current_top_section_index,
+ $current_desc_section_index,
+ $current_article_offset,
+ count( $descendant_ids ),
+ count( $top_section_ids ),
+ $articles_processed,
+ $total_articles
+ );
+
+ if ( empty( $top_section_ids ) || $current_top_section_index >= count( $top_section_ids ) ) {
+ $response_data['progress'] = 80;
+ $response_data['next_step'] = 3;
+ $response_data['message'] = __( 'No more top-level sections to process. Moving to next step.', 'knowledgebase' );
+ $log[] = $response_data['message'];
+ unset( $state['current_top_section_index'], $state['descendant_ids'], $state['current_desc_section_index'], $state['current_article_offset'] );
+ } else {
+ $section_id = $top_section_ids[ $current_top_section_index ];
+ if ( ! isset( $section_to_product_map[ $section_id ] ) ) {
+ $log[] = sprintf(
+ /* translators: %d: Section ID */
+ __( 'Skipping section ID: %d (No product mapping found).', 'knowledgebase' ),
+ $section_id
+ );
+ $state['current_top_section_index'] = $current_top_section_index + 1;
+ $state['descendant_ids'] = array();
+ $state['current_desc_section_index'] = 0;
+ $state['current_article_offset'] = 0;
+ $response_data['progress'] = round( max( 20, min( 80, ( $current_top_section_index / max( 1, count( $top_section_ids ) ) ) * 60 + 20 ) ), 1 );
+ $response_data['next_step'] = 2;
+ } else {
+ $product_id = (int) $section_to_product_map[ $section_id ];
+
+ if ( empty( $descendant_ids ) ) {
+ $log[] = sprintf(
+ /* translators: %d: Section ID */
+ __( 'Loading descendants for section ID: %d', 'knowledgebase' ),
+ $section_id
+ );
+ $descendant_ids = $this->get_all_descendant_section_ids( $section_id, $this->taxonomy_section );
+ array_unshift( $descendant_ids, $section_id );
+ $log[] = sprintf(
+ /* translators: 1: Section ID, 2: Count */
+ __( 'Found %2$d sections (including top-level) for section ID: %1$d.', 'knowledgebase' ),
+ $section_id,
+ count( $descendant_ids )
+ );
+ $log[] = 'Descendant IDs: ' . implode( ', ', array_slice( $descendant_ids, 1 ) );
+ $current_desc_section_index = 0;
+ $current_article_offset = 0;
+ }
+
+ $sections_processed = 0;
+ $articles_in_batch = 0;
+ $top_sections_count = count( $top_section_ids );
+ while ( $sections_processed < $max_sections_per_batch && $articles_in_batch < $max_articles_per_batch && $current_top_section_index < $top_sections_count ) {
+ if ( $current_desc_section_index >= count( $descendant_ids ) ) {
+ $log[] = sprintf(
+ /* translators: %d: Section ID */
+ __( 'Completed all sections for top section ID: %d. Moving to next top section.', 'knowledgebase' ),
+ $section_id
+ );
+ ++$current_top_section_index;
+ $descendant_ids = array();
+ $current_desc_section_index = 0;
+ $current_article_offset = 0;
+ break;
+ }
+
+ $current_section_id = $descendant_ids[ $current_desc_section_index ];
+ $section_term = get_term( $current_section_id, $this->taxonomy_section );
+ $log[] = sprintf(
+ /* translators: 1: Section name, 2: Section ID */
+ __( 'Processing section "%1$s" (ID: %2$d)', 'knowledgebase' ),
+ $section_term->name ?? $current_section_id,
+ $current_section_id
+ );
+
+ // Only count as a descendant section if it's not the top-level section itself.
+ if ( $current_section_id !== $section_id ) {
+ if ( ! $dry_run ) {
+ update_term_meta( $current_section_id, 'product_id', $product_id );
+ }
+
+ // Check if we've already counted this section before incrementing.
+ if ( ! isset( $state['processed_section_ids'][ $current_section_id ] ) ) {
+ ++$sections_mapped;
+ $state['processed_section_ids'][ $current_section_id ] = true;
+ }
+
+ $log[] = sprintf(
+ /* translators: 1: Section name, 2: Section ID, 3: Product ID */
+ __( 'Linked section "%1$s" (ID: %2$d) to product ID: %3$d.', 'knowledgebase' ),
+ $section_term->name ?? $current_section_id,
+ $current_section_id,
+ $product_id
+ );
+ }
+
+ $articles_remaining = isset( $section_article_counts[ $current_section_id ] ) ? $section_article_counts[ $current_section_id ] - $current_article_offset : $max_articles_per_batch;
+ $articles_to_fetch = min( $max_articles_per_batch, $max_articles_per_batch - $articles_in_batch, $articles_remaining );
+
+ if ( $articles_to_fetch <= 0 ) {
+ $log[] = sprintf(
+ /* translators: %d: Section ID */
+ __( 'No articles remaining for section ID: %d.', 'knowledgebase' ),
+ $current_section_id
+ );
+ ++$current_desc_section_index;
+ $current_article_offset = 0;
+ ++$sections_processed;
+ continue;
+ }
+
+ $articles = get_posts(
+ array(
+ 'post_type' => $this->post_type_article,
+ 'posts_per_page' => $articles_to_fetch,
+ 'offset' => $current_article_offset,
+ 'fields' => 'ids',
+ 'tax_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
+ array(
+ 'taxonomy' => $this->taxonomy_section,
+ 'field' => 'term_id',
+ 'terms' => $current_section_id,
+ ),
+ ),
+ 'suppress_filters' => true,
+ )
+ );
+
+ $assigned_articles = array();
+ foreach ( $articles as $article_id ) {
+ $article_id = absint( $article_id );
+ if ( isset( $already_assigned_articles[ $product_id ][ $article_id ] ) ) {
+ $log[] = sprintf(
+ /* translators: 1: Article ID, 2: Product ID */
+ __( 'Skipped article ID: %1$d (already assigned to product ID: %2$d).', 'knowledgebase' ),
+ $article_id,
+ $product_id
+ );
+ continue;
+ }
+ if ( ! $dry_run ) {
+ wp_set_object_terms( $article_id, $product_id, $this->taxonomy_product, true );
+ }
+ $assigned_articles[] = sprintf( '%s (ID: %d)', get_the_title( $article_id ), $article_id );
+ $already_assigned_articles[ $product_id ][ $article_id ] = true;
+ ++$articles_processed;
+
+ if ( ! isset( $state['processed_article_ids'][ $article_id ] ) ) {
+ $state['processed_article_ids'][ $article_id ] = true;
+ }
+ }
+
+ if ( ! empty( $assigned_articles ) ) {
+ $product_term = get_term( $product_id, $this->taxonomy_product );
+ $product_name = $product_term->name ?? __( 'Unknown Product', 'knowledgebase' );
+ $log[] = sprintf(
+ /* translators: 1: Product name, 2: Product ID, 3: List of articles */
+ __( 'Assigned articles to product "%1$s" (ID: %2$d): %3$s', 'knowledgebase' ),
+ $product_name,
+ $product_id,
+ implode( ', ', $assigned_articles )
+ );
+ } else {
+ $log[] = sprintf(
+ /* translators: %d: Section ID */
+ __( 'No new articles assigned for section ID: %d.', 'knowledgebase' ),
+ $current_section_id
+ );
+ }
+
+ $articles_count = count( $articles );
+ $articles_in_batch += $articles_count;
+ $current_article_offset += $articles_count;
+
+ if ( $articles_count < $articles_to_fetch || $current_article_offset >= $section_article_counts[ $current_section_id ] ) {
+ $log[] = sprintf(
+ /* translators: 1: Section ID, 2: Article count */
+ __( 'Finished processing articles for section ID: %1$d (found %2$d articles).', 'knowledgebase' ),
+ $current_section_id,
+ $articles_count
+ );
+ ++$current_desc_section_index;
+ $current_article_offset = 0;
+ ++$sections_processed;
+ }
+ }
+
+ if ( $current_top_section_index < count( $top_section_ids ) ) {
+ $response_data['progress'] = $total_articles > 0
+ ? round( max( 20, min( 80, ( count( $state['processed_article_ids'] ?? array() ) / $total_articles ) * 60 + 20 ) ), 1 )
+ : round( max( 20, min( 80, ( $current_top_section_index / max( 1, count( $top_section_ids ) ) ) * 60 + 20 ) ), 1 );
+ $response_data['next_step'] = 2;
+ }
+ }
+ }
+
+ set_transient( 'wzkb_migration_assigned_articles', $already_assigned_articles, DAY_IN_SECONDS );
+ set_transient( 'wzkb_migration_log', $log, DAY_IN_SECONDS );
+
+ $state['current_top_section_index'] = $current_top_section_index;
+ $state['descendant_ids'] = $descendant_ids;
+ $state['current_desc_section_index'] = $current_desc_section_index;
+ $state['current_article_offset'] = $current_article_offset;
+ $state['articles_processed'] = count( $state['processed_article_ids'] ?? array() );
+ $state['sections_mapped'] = $sections_mapped;
+ $state['top_sections_mapped'] = $top_sections_mapped;
+
+ $log[] = sprintf(
+ /* translators: 1: Top index, 2: Descendant index, 3: Offset, 4: Descendant count, 5: Sections mapped, 6: Top sections mapped, 7: Articles processed, 8: Total articles */
+ __( 'Outgoing State: top_index=%1$d, desc_index=%2$d, offset=%3$d, desc_count=%4$d, sections_mapped=%5$d, top_sections_mapped=%6$d, articles_processed=%7$d/%8$d', 'knowledgebase' ),
+ $current_top_section_index,
+ $current_desc_section_index,
+ $current_article_offset,
+ count( $descendant_ids ),
+ $sections_mapped,
+ $top_sections_mapped,
+ $state['articles_processed'],
+ $total_articles
+ );
+
+ $response_data['log'] = array_slice( $log, $last_log_index );
+ $state['last_log_index'] = count( $log );
+ $response_data['state'] = $state;
+ set_transient( 'wzkb_migration_log', $log, DAY_IN_SECONDS );
+
+ break;
+
+ case 3:
+ $last_log_index = isset( $state['last_log_index'] ) ? (int) $state['last_log_index'] : 0;
+ $response_data['message'] = '' . __( 'Deleting old top-level sections...', 'knowledgebase' ) . ' ';
+ $log[] = $response_data['message'];
+ $top_section_ids = $state['top_section_ids'] ?? array();
+ $sections_deleted = 0;
+ foreach ( $top_section_ids as $section_id ) {
+ if ( ! $dry_run ) {
+ $result = wp_delete_term( $section_id, $this->taxonomy_section );
+ if ( ! is_wp_error( $result ) ) {
+ ++$sections_deleted;
+ }
+ } else {
+ ++$sections_deleted;
+ }
+ $log[] = sprintf(
+ /* translators: %d: Section ID */
+ __( 'Deleting top-level section ID: %d.', 'knowledgebase' ),
+ $section_id
+ );
+ }
+ $state['sections_deleted'] = $sections_deleted;
+
+ if ( $dry_run && ! empty( $state['simulated_product_ids'] ) ) {
+ foreach ( $state['simulated_product_ids'] as $sim_product_id ) {
+ wp_delete_term( $sim_product_id, $this->taxonomy_product );
+ $log[] = sprintf(
+ /* translators: 1: Product ID */
+ __( 'Deleted simulated product with ID: %1$d after dry run.', 'knowledgebase' ),
+ $sim_product_id
+ );
+ }
+ $state['simulated_product_ids'] = array();
+ }
+
+ $log[] = sprintf(
+ /* translators: 1: Descendant sections, 2: Top-level sections, 3: Sections deleted, 4: Total sections, 5: Articles processed */
+ __( 'Mapped %1$d descendant sections and %2$d top-level sections (total %4$d sections), processed %5$d articles, deleted %3$d top-level sections.', 'knowledgebase' ),
+ $state['sections_mapped'],
+ $state['top_sections_mapped'],
+ $sections_deleted,
+ $state['sections_mapped'] + $state['top_sections_mapped'],
+ $state['articles_processed'] ?? 0
+ );
+
+ $log[] = sprintf(
+ /* translators: %d: Expected descendant count */
+ __( 'Expected descendant count from initial scan: %d', 'knowledgebase' ),
+ $state['total_descendant_count'] ?? 0
+ );
+
+ if ( ! $dry_run ) {
+ update_option( 'wzkb_product_migration_complete', time() );
+ }
+
+ $response_data['progress'] = 100;
+ $response_data['done'] = true;
+ $response_data['state'] = $state;
+ $response_data['message'] = '' . ( $dry_run ? __( 'Dry run complete! No changes were made.', 'knowledgebase' ) : __( 'Migration complete!', 'knowledgebase' ) ) . ' ';
+ $response_data['log'] = array_slice( $log, $last_log_index );
+ $state['last_log_index'] = count( $log );
+ $response_data['state'] = $state;
+
+ delete_transient( 'wzkb_migration_assigned_articles' );
+ delete_transient( 'wzkb_migration_article_counts' );
+ delete_transient( 'wzkb_migration_log' );
+
+ break;
+
+ default:
+ $response_data['message'] = __( 'Unknown migration step.', 'knowledgebase' );
+ $response_data['errors'][] = $response_data['message'];
+ $response_data['done'] = true;
+ $response_data['log'] = array_slice( $log, $state['last_log_index'] ?? 0 );
+ $state['last_log_index'] = count( $log );
+ $response_data['state'] = $state;
+
+ break;
+ }
+
+ wp_send_json_success( $response_data );
+ }
+
+ /**
+ * Helper function: Recursively get all descendant section IDs for a given section.
+ *
+ * @param int $parent_id The parent section ID.
+ * @param string $taxonomy The taxonomy name.
+ *
+ * @return int[] Array of descendant section IDs.
+ */
+ private function get_all_descendant_section_ids( $parent_id, $taxonomy ) {
+ $descendants = array();
+ $children = get_terms(
+ array(
+ 'taxonomy' => $taxonomy,
+ 'hide_empty' => false,
+ 'parent' => $parent_id,
+ 'fields' => 'ids',
+ )
+ );
+ if ( is_wp_error( $children ) || empty( $children ) ) {
+ return $descendants;
+ }
+ foreach ( $children as $child_id ) {
+ $descendants[] = (int) $child_id;
+ $descendants = array_merge( $descendants, $this->get_all_descendant_section_ids( (int) $child_id, $taxonomy ) );
+ }
+ return $descendants;
+ }
+
+ /**
+ * Check if the notice has been dismissed for 90 days.
+ *
+ * @return bool True if the notice has been dismissed, false otherwise.
+ */
+ public static function is_dismissed() {
+ $dismissed = get_user_meta( get_current_user_id(), 'wzkb_product_notice_dismissed', true );
+ if ( empty( $dismissed ) ) {
+ return false;
+ }
+ return ( time() - (int) $dismissed ) < 90 * DAY_IN_SECONDS;
+ }
+
+ /**
+ * Check if we are on a KB admin screen.
+ *
+ * @return bool True if we are on a KB admin screen, false otherwise.
+ */
+ public static function is_kb_admin_screen() {
+ $screen = get_current_screen();
+ return isset( $screen->post_type ) && 'wz_knowledgebase' === $screen->post_type;
+ }
+
+ /**
+ * AJAX handler for dismissing the product notice.
+ *
+ * @return void
+ */
+ public function dismiss_product_notice() {
+ check_ajax_referer( 'wzkb_dismiss_product_notice', 'nonce' );
+ if ( ! current_user_can( 'manage_options' ) ) {
+ wp_send_json_error();
+ }
+ update_user_meta( get_current_user_id(), 'wzkb_product_notice_dismissed', time() );
+ wp_send_json_success();
+ }
+
+ /**
+ * Display an error message in a consistent format.
+ *
+ * @param string $message The error message.
+ */
+ public function display_error( $message ) {
+ echo '' . esc_html( $message ) . '
';
+ }
+
+ /**
+ * Display a success message in a consistent format.
+ *
+ * @param string $message The success message.
+ */
+ public function display_success( $message ) {
+ echo '' . esc_html( $message ) . '
';
+ }
+
+ /**
+ * Display a warning message in a consistent format.
+ *
+ * @param string $message The warning message.
+ */
+ public function display_warning( $message ) {
+ echo '' . esc_html( $message ) . '
';
+ }
+}
diff --git a/includes/admin/settings/class-settings.php b/includes/admin/class-settings.php
similarity index 83%
rename from includes/admin/settings/class-settings.php
rename to includes/admin/class-settings.php
index 346b4a0..2274110 100644
--- a/includes/admin/settings/class-settings.php
+++ b/includes/admin/class-settings.php
@@ -7,9 +7,7 @@
* @package WebberZone\Knowledge_Base
*/
-namespace WebberZone\Knowledge_Base\Admin\Settings;
-
-use WebberZone\Knowledge_Base\Util\Helpers;
+namespace WebberZone\Knowledge_Base\Admin;
// If this file is called directly, abort.
if ( ! defined( 'WPINC' ) ) {
@@ -19,7 +17,7 @@
/**
* Class to register the settings.
*
- * @since 2.3.0
+ * @since 2.3.0
*/
class Settings {
@@ -173,7 +171,49 @@ public function initialise_settings() {
'upgraded_settings' => array(),
);
- $this->settings_api = new Settings_API( $this->settings_key, self::$prefix, $args );
+ $this->settings_api = new Settings\Settings_API( $this->settings_key, self::$prefix, $args );
+ }
+
+ /**
+ * Get settings defaults.
+ *
+ * @since 3.0.0
+ *
+ * @return array Default settings.
+ */
+ public static function settings_defaults() {
+ $defaults = array();
+
+ // Get all registered settings.
+ $settings = self::get_registered_settings();
+
+ // Loop through each section.
+ foreach ( $settings as $section => $section_settings ) {
+ // Loop through each setting in the section.
+ foreach ( $section_settings as $setting ) {
+ if ( isset( $setting['id'] ) ) {
+ // When checkbox is set to true, set this to 1.
+ if ( 'checkbox' === $setting['type'] && ! empty( $setting['options'] ) ) {
+ $defaults[ $setting['id'] ] = 1;
+ } elseif ( in_array( $setting['type'], array( 'textarea', 'css', 'html', 'text', 'url', 'csv', 'color', 'numbercsv', 'postids', 'posttypes', 'number', 'wysiwyg', 'file', 'password' ), true ) && isset( $setting['default'] ) ) {
+ $defaults[ $setting['id'] ] = $setting['default'];
+ } elseif ( in_array( $setting['type'], array( 'multicheck', 'radio', 'select', 'radiodesc', 'thumbsizes', 'repeater' ), true ) && isset( $setting['default'] ) ) {
+ $defaults[ $setting['id'] ] = $setting['default'];
+ } else {
+ $defaults[ $setting['id'] ] = '';
+ }
+ }
+ }
+ }
+
+ /**
+ * Filter the default settings array.
+ *
+ * @since 1.0.0
+ *
+ * @param array $defaults Default settings.
+ */
+ return apply_filters( self::$prefix . '_settings_defaults', $defaults );
}
/**
@@ -289,76 +329,100 @@ public static function get_registered_settings() {
*/
public static function settings_general() {
$settings = array(
- 'slug_header' => array(
+ 'multi_product_header' => array(
+ 'id' => 'multi_product_header',
+ 'name' => '' . esc_html__( 'Multi-Product Mode', 'knowledgebase' ) . ' ',
+ 'desc' => '',
+ 'type' => 'header',
+ 'options' => '',
+ ),
+ 'multi_product' => array(
+ 'id' => 'multi_product',
+ 'name' => esc_html__( 'Enable Multi-Product Mode', 'knowledgebase' ),
+ 'desc' => esc_html__(
+ 'Enable this option to use a dedicated “Products” menu to organize your knowledge base articles and sections by product. This system allows you to assign each article or section to one or more products, making it easier to manage documentation for different software, hardware, or service lines. If your knowledge base does not need this level of organization, you can leave this option disabled. This is a transitional feature for advanced organization and future compatibility.',
+ 'knowledgebase'
+ ),
+ 'type' => 'checkbox',
+ 'options' => false,
+ ),
+ 'slug_header' => array(
'id' => 'slug_header',
- 'name' => '' . esc_html__( 'Slug options', 'knowledgebase' ) . ' ',
+ 'name' => '' . esc_html__( 'Knowledge Base Permalink', 'knowledgebase' ) . ' ',
'desc' => '',
'type' => 'header',
),
- 'kb_slug' => array(
+ 'kb_slug' => array(
'id' => 'kb_slug',
'name' => esc_html__( 'Knowledge Base slug', 'knowledgebase' ),
'desc' => esc_html__( 'This will set the opening path of the URL of the knowledge base and is set when registering the custom post type', 'knowledgebase' ),
'type' => 'text',
'options' => 'knowledgebase',
),
- 'category_slug' => array(
+ 'product_slug' => array(
+ 'id' => 'product_slug',
+ 'name' => esc_html__( 'Product slug', 'knowledgebase' ),
+ 'desc' => esc_html__( 'This slug forms part of the URL for product pages when Multi-Product Mode is enabled. The value is used when registering the custom taxonomy.', 'knowledgebase' ),
+ 'type' => 'text',
+ 'options' => 'kb/product',
+ ),
+ 'category_slug' => array(
'id' => 'category_slug',
- 'name' => esc_html__( 'Category slug', 'knowledgebase' ),
- 'desc' => esc_html__( 'Each category is a section of the knowledge base. This setting is used when registering the custom category and forms a part of the URL when browsing category archives', 'knowledgebase' ),
+ 'name' => esc_html__( 'Section slug', 'knowledgebase' ),
+ 'desc' => esc_html__( 'Each section is a section of the knowledge base. This setting is used when registering the custom section and forms a part of the URL when browsing section archives', 'knowledgebase' ),
'type' => 'text',
- 'options' => 'section',
+ 'options' => 'kb/section',
),
- 'tag_slug' => array(
+ 'tag_slug' => array(
'id' => 'tag_slug',
- 'name' => esc_html__( 'Tag slug', 'knowledgebase' ),
+ 'name' => esc_html__( 'Tags slug', 'knowledgebase' ),
'desc' => esc_html__( 'Each article can have multiple tags. This setting is used when registering the custom tag and forms a part of the URL when browsing tag archives', 'knowledgebase' ),
'type' => 'text',
- 'options' => 'kb-tags',
+ 'options' => 'kb/tags',
),
- 'cache' => array(
+ 'cache' => array(
'id' => 'cache',
'name' => esc_html__( 'Enable cache', 'knowledgebase' ),
'desc' => esc_html__( 'Cache the output of the WP_Query lookups to speed up retrieval of the knowledgebase. Recommended for large knowledge bases', 'knowledgebase' ),
'type' => 'checkbox',
'options' => false,
),
- 'uninstall_header' => array(
+ 'uninstall_header' => array(
'id' => 'uninstall_header',
'name' => '' . esc_html__( 'Uninstall options', 'knowledgebase' ) . ' ',
'desc' => '',
'type' => 'header',
'options' => '',
),
- 'uninstall_options' => array(
+ 'uninstall_options' => array(
'id' => 'uninstall_options',
'name' => esc_html__( 'Delete options on uninstall', 'knowledgebase' ),
'desc' => esc_html__( 'Check this box to delete the settings on this page when the plugin is deleted via the Plugins page in your WordPress Admin', 'knowledgebase' ),
'type' => 'checkbox',
'options' => true,
),
- 'uninstall_data' => array(
+ 'uninstall_data' => array(
'id' => 'uninstall_data',
- 'name' => esc_html__( 'Delete all knowledge base posts on uninstall', 'knowledgebase' ),
+ 'name' => esc_html__( 'Delete all content on uninstall', 'knowledgebase' ),
'desc' => esc_html__( 'Check this box to delete all the posts, categories and tags created by the plugin. There is no way to restore the data if you choose this option', 'knowledgebase' ),
'type' => 'checkbox',
'options' => false,
),
- 'feed_header' => array(
+ 'feed_header' => array(
'id' => 'feed_header',
'name' => '' . esc_html__( 'Feed options', 'knowledgebase' ) . ' ',
'desc' => '',
'type' => 'header',
'options' => '',
),
- 'include_in_feed' => array(
+ 'include_in_feed' => array(
'id' => 'include_in_feed',
'name' => esc_html__( 'Include in feed', 'knowledgebase' ),
'desc' => esc_html__( 'Adds the knowledge base articles to the main RSS feed for your site', 'knowledgebase' ),
'type' => 'checkbox',
'options' => true,
),
- 'disable_kb_feed' => array(
+ 'disable_kb_feed' => array(
'id' => 'disable_kb_feed',
'name' => esc_html__( 'Disable KB feed', 'knowledgebase' ),
/* translators: 1: Opening link tag, 2: Closing link tag. */
@@ -674,26 +738,8 @@ public function admin_enqueue_scripts( $hook ) {
if ( ! isset( $this->settings_api->settings_page ) || $hook !== $this->settings_api->settings_page ) {
return;
}
- wp_localize_script(
- 'wz-admin-js',
- 'wzkb_admin',
- array()
- );
- wp_enqueue_script( 'knowledgebase-admin-js' );
- wp_enqueue_style( 'knowledgebase-admin-ui-css' );
- wp_enqueue_style( 'wp-spinner' );
- wp_localize_script(
- 'knowledgebase-admin-js',
- 'wzkb_admin_data',
- array(
- 'ajax_url' => admin_url( 'admin-ajax.php' ),
- 'security' => wp_create_nonce( 'wzkb-admin' ),
- 'confirm_message' => esc_html__( 'Are you sure you want to clear the cache?', 'knowledgebase' ),
- 'success_message' => esc_html__( 'Cache cleared successfully!', 'knowledgebase' ),
- 'fail_message' => esc_html__( 'Failed to clear cache. Please try again.', 'knowledgebase' ),
- 'request_fail_message' => esc_html__( 'Request failed: ', 'knowledgebase' ),
- )
- );
+ wp_enqueue_script( 'wzkb-admin' );
+ wp_enqueue_style( 'wzkb-admin-ui' );
}
/**
@@ -709,6 +755,8 @@ public function change_settings_on_save( $settings ) {
// Delete the cache.
\WebberZone\Knowledge_Base\Util\Cache::delete();
+ flush_rewrite_rules( true );
+
return $settings;
}
}
diff --git a/includes/admin/js/admin-scripts.js b/includes/admin/js/admin-scripts.js
index 35421f1..800be06 100644
--- a/includes/admin/js/admin-scripts.js
+++ b/includes/admin/js/admin-scripts.js
@@ -1,7 +1,7 @@
jQuery(document).ready(
function ($) {
$('button[name="wzkb_cache_clear"]').on('click', function () {
- if (confirm(wzkb_admin_data.confirm_message)) {
+ if (confirm(WZKBAdminData.strings.confirm_message)) {
var $button = $(this);
$button.prop('disabled', true).append(' ');
clearCache($button);
@@ -10,17 +10,18 @@ jQuery(document).ready(
// Function to clear the cache.
function clearCache($button) {
- $.post(wzkb_admin_data.ajax_url, {
+ $.post(WZKBAdminData.ajax_url, {
action: 'wzkb_clear_cache',
- security: wzkb_admin_data.security
+ security: WZKBAdminData.security
}, function (response) {
if (response.success) {
- alert(response.data.message);
+ alert(WZKBAdminData.strings.success_message);
} else {
- alert(wzkb_admin_data.fail_message);
+ alert(WZKBAdminData.strings.fail_message);
}
}).fail(function (jqXHR, textStatus) {
- alert(wzkb_admin_data.request_fail_message + textStatus);
+ alert(WZKBAdminData.strings.fail_message);
+ console.log(WZKBAdminData.strings.request_fail_message + textStatus);
}).always(function () {
$button.prop('disabled', false).find('.spinner').remove();
});
diff --git a/includes/admin/js/admin-scripts.min.js b/includes/admin/js/admin-scripts.min.js
index 204169a..e328fb8 100644
--- a/includes/admin/js/admin-scripts.min.js
+++ b/includes/admin/js/admin-scripts.min.js
@@ -1 +1 @@
-jQuery(document).ready((function(a){a('button[name="wzkb_cache_clear"]').on("click",(function(){if(confirm(wzkb_admin_data.confirm_message)){var n=a(this);n.prop("disabled",!0).append(' '),function(n){a.post(wzkb_admin_data.ajax_url,{action:"wzkb_clear_cache",security:wzkb_admin_data.security},(function(a){a.success?alert(a.data.message):alert(wzkb_admin_data.fail_message)})).fail((function(a,n){alert(wzkb_admin_data.request_fail_message+n)})).always((function(){n.prop("disabled",!1).find(".spinner").remove()}))}(n)}}));var n=0;function i(){n=0}a("form *").change((function(){n=1})),window.onbeforeunload=function(){if(1==n)return!0},a("input[name='submit']").click(i),a("input[id='search-submit']").click(i),a("input[id='doaction']").click(i),a("input[id='doaction2']").click(i),a("input[name='filter_action']").click(i),a((function(){a("#post-body-content").tabs({create:function(n,i){a(i.tab.find("a")).addClass("nav-tab-active")},activate:function(n,i){a(i.oldTab.find("a")).removeClass("nav-tab-active"),a(i.newTab.find("a")).addClass("nav-tab-active")}})}))}));
\ No newline at end of file
+jQuery(document).ready((function(a){a('button[name="wzkb_cache_clear"]').on("click",(function(){if(confirm(WZKBAdminData.strings.confirm_message)){var n=a(this);n.prop("disabled",!0).append(' '),function(n){a.post(WZKBAdminData.ajax_url,{action:"wzkb_clear_cache",security:WZKBAdminData.security},(function(a){a.success?alert(WZKBAdminData.strings.success_message):alert(WZKBAdminData.strings.fail_message)})).fail((function(a,n){alert(WZKBAdminData.strings.fail_message)})).always((function(){n.prop("disabled",!1).find(".spinner").remove()}))}(n)}}));var n=0;function i(){n=0}a("form *").change((function(){n=1})),window.onbeforeunload=function(){if(1==n)return!0},a("input[name='submit']").click(i),a("input[id='search-submit']").click(i),a("input[id='doaction']").click(i),a("input[id='doaction2']").click(i),a("input[name='filter_action']").click(i),a((function(){a("#post-body-content").tabs({create:function(n,i){a(i.tab.find("a")).addClass("nav-tab-active")},activate:function(n,i){a(i.oldTab.find("a")).removeClass("nav-tab-active"),a(i.newTab.find("a")).addClass("nav-tab-active")}})}))}));
\ No newline at end of file
diff --git a/includes/admin/js/product-migrator.js b/includes/admin/js/product-migrator.js
new file mode 100644
index 0000000..c55affc
--- /dev/null
+++ b/includes/admin/js/product-migrator.js
@@ -0,0 +1,188 @@
+/* global ajaxurl, wzkbProductMigrator */
+(function ($) {
+ 'use strict';
+
+ var step = 0;
+
+ var migrationState = {};
+ var lastTopSectionIndex = -1;
+
+ /**
+ * Updates the migration progress bar.
+ *
+ * @param {number} percent - The percentage of completion.
+ * @param {string} message - The message to display.
+ */
+ function updateProgressBar(percent, message) {
+ $('#wzkb-migration-progress-bar').css('width', percent + '%');
+ $('#wzkb-migration-progress-bar').html(percent + '%');
+ $('#wzkb-migration-progress-text').html(message);
+ }
+
+ /**
+ * Displays an error message.
+ *
+ * @param {string} message - The error message.
+ */
+ function showError(message) {
+ $('#wzkb-migration-errors').append('' + message + ' ');
+ }
+
+ /**
+ * Appends a log message.
+ *
+ * @param {string} html - The log message HTML.
+ */
+ function appendToLog(html) {
+ if (html) {
+ var timestamp = new Date().toLocaleString();
+ var $log = $('#wzkb-migration-log');
+ $log.append('[' + timestamp + '] ' + html + '
');
+ // Scroll to bottom
+ $log.scrollTop($log[0].scrollHeight);
+ // Force render
+ setTimeout(function () { $log[0].offsetHeight; }, 0);
+ }
+ }
+
+ /**
+ * Proceeds to the next migration step.
+ */
+ function nextMigrationStep() {
+ var stateToSend = JSON.parse(JSON.stringify(migrationState));
+ // Track consecutive identical indices
+ if (stateToSend.current_top_section_index !== undefined && stateToSend.current_top_section_index === lastTopSectionIndex) {
+ if (!window.wzkbMigrationLoopCount) {
+ window.wzkbMigrationLoopCount = 0;
+ }
+ window.wzkbMigrationLoopCount++;
+ if (window.wzkbMigrationLoopCount >= 5) {
+ console.warn('Warning: current_top_section_index has not changed after 5 attempts:', stateToSend.current_top_section_index);
+ window.wzkbMigrationLoopCount = 0; // Reset to avoid spamming.
+ }
+ } else {
+ window.wzkbMigrationLoopCount = 0; // Reset on change.
+ }
+ lastTopSectionIndex = stateToSend.current_top_section_index;
+
+ $.ajax({
+ url: ajaxurl,
+ method: 'POST',
+ data: {
+ action: 'wzkb_product_migration_batch',
+ _nonce: wzkbProductMigrator.nonce,
+ step: step,
+ dry_run: $('#wzkb-dry-run').is(':checked') ? 1 : 0,
+ state: stateToSend
+ },
+ success: function (response) {
+ if (!response.success) {
+ showError(response.data || wzkbProductMigrator.strings.unknown_error);
+ updateProgressBar(100, wzkbProductMigrator.strings.migration_failed);
+ $('#wzkb-migration-start').prop('disabled', false);
+ return;
+ }
+
+ if (response.data.log && Array.isArray(response.data.log)) {
+ response.data.log.forEach(function (line) { appendToLog(line); });
+ }
+
+ if (response.data.message) {
+ appendToLog(response.data.message);
+ }
+
+ if (response.data.progress) {
+ updateProgressBar(response.data.progress, response.data.message);
+ }
+
+ if (response.data.errors && response.data.errors.length) {
+ response.data.errors.forEach(showError);
+ }
+
+ if (response.data.dry_run && response.data.done) {
+ updateProgressBar(100, response.data.message);
+ $('#wzkb-migration-start').prop('disabled', false);
+ return;
+ }
+
+ if (response.data.done) {
+ var completionMessage = response.data.message || wzkbProductMigrator.strings.migration_complete;
+ updateProgressBar(100, completionMessage);
+ $('#wzkb-migration-start').prop('disabled', false);
+ return;
+ }
+
+ step = response.data.next_step;
+ migrationState = JSON.parse(JSON.stringify(response.data.state));
+
+ setTimeout(nextMigrationStep, 100);
+ },
+ error: function (xhr, status, error) {
+ console.error('AJAX error:', status, error);
+ showError(wzkbProductMigrator.strings.ajax_error + ': ' + error);
+ updateProgressBar(100, wzkbProductMigrator.strings.migration_failed);
+ $('#wzkb-migration-start').prop('disabled', false);
+ }
+ });
+ }
+
+ $(document).ready(function () {
+ $('#wzkb-migration-start').prop('disabled', true);
+ $('#wzkb-backup-confirm').on('change', function () {
+ $('#wzkb-migration-start').prop('disabled', !this.checked);
+ });
+
+ $('#wzkb-migration-start').on('click', function (e) {
+ e.preventDefault();
+ step = 0;
+ migrationState = {};
+ lastTopSectionIndex = -1;
+ $('#wzkb-migration-progress-bar').css('width', '0%');
+ $('#wzkb-migration-progress-text').html('');
+ $('#wzkb-migration-errors').empty();
+ $('#wzkb-migration-log').empty();
+ $(this).prop('disabled', true);
+ updateProgressBar(0, wzkbProductMigrator.strings.starting_migration);
+
+ nextMigrationStep();
+ });
+
+ // Copy log to clipboard functionality
+ $('#wzkb-copy-log').on('click', function () {
+ var $button = $(this);
+ var $log = $('#wzkb-migration-log');
+ var logText = '';
+
+ // Extract text from each paragraph with line breaks
+ $log.find('p').each(function () {
+ logText += $(this).text() + '\n\n';
+ });
+
+ // Trim extra line breaks at the end
+ logText = logText.trim();
+
+ if (!logText) {
+ $button.html(' Empty Log');
+ setTimeout(function () {
+ $button.html(' Copy Log');
+ }, 2000);
+ return;
+ }
+
+ // Modern approach using Clipboard API
+ navigator.clipboard.writeText(logText).then(function () {
+ $button.html(' Copied!');
+ setTimeout(function () {
+ $button.html(' Copy Log');
+ }, 2000);
+ }).catch(function (err) {
+ console.error('Failed to copy: ', err);
+ $button.html(' Failed!');
+ setTimeout(function () {
+ $button.html(' Copy Log');
+ }, 2000);
+ });
+ });
+ });
+
+})(jQuery);
\ No newline at end of file
diff --git a/includes/admin/js/product-migrator.min.js b/includes/admin/js/product-migrator.min.js
new file mode 100644
index 0000000..74ab410
--- /dev/null
+++ b/includes/admin/js/product-migrator.min.js
@@ -0,0 +1 @@
+!function(t){"use strict";var o=0,i={},a=-1;function r(o,i){t("#wzkb-migration-progress-bar").css("width",o+"%"),t("#wzkb-migration-progress-bar").html(o+"%"),t("#wzkb-migration-progress-text").html(i)}function n(o){t("#wzkb-migration-errors").append(""+o+" ")}function s(o){if(o){var i=(new Date).toLocaleString(),a=t("#wzkb-migration-log");a.append("["+i+"] "+o+"
"),a.scrollTop(a[0].scrollHeight),setTimeout((function(){a[0].offsetHeight}),0)}}function e(){var d=JSON.parse(JSON.stringify(i));void 0!==d.current_top_section_index&&d.current_top_section_index===a?(window.wzkbMigrationLoopCount||(window.wzkbMigrationLoopCount=0),window.wzkbMigrationLoopCount++,window.wzkbMigrationLoopCount>=5&&(window.wzkbMigrationLoopCount=0)):window.wzkbMigrationLoopCount=0,a=d.current_top_section_index,t.ajax({url:ajaxurl,method:"POST",data:{action:"wzkb_product_migration_batch",_nonce:wzkbProductMigrator.nonce,step:o,dry_run:t("#wzkb-dry-run").is(":checked")?1:0,state:d},success:function(a){return a.success?(a.data.log&&Array.isArray(a.data.log)&&a.data.log.forEach((function(t){s(t)})),a.data.message&&s(a.data.message),a.data.progress&&r(a.data.progress,a.data.message),a.data.errors&&a.data.errors.length&&a.data.errors.forEach(n),a.data.dry_run&&a.data.done?(r(100,a.data.message),void t("#wzkb-migration-start").prop("disabled",!1)):a.data.done?(r(100,a.data.message||wzkbProductMigrator.strings.migration_complete),void t("#wzkb-migration-start").prop("disabled",!1)):(o=a.data.next_step,i=JSON.parse(JSON.stringify(a.data.state)),void setTimeout(e,100))):(n(a.data||wzkbProductMigrator.strings.unknown_error),r(100,wzkbProductMigrator.strings.migration_failed),void t("#wzkb-migration-start").prop("disabled",!1))},error:function(o,i,a){n(wzkbProductMigrator.strings.ajax_error+": "+a),r(100,wzkbProductMigrator.strings.migration_failed),t("#wzkb-migration-start").prop("disabled",!1)}})}t(document).ready((function(){t("#wzkb-migration-start").prop("disabled",!0),t("#wzkb-backup-confirm").on("change",(function(){t("#wzkb-migration-start").prop("disabled",!this.checked)})),t("#wzkb-migration-start").on("click",(function(n){n.preventDefault(),o=0,i={},a=-1,t("#wzkb-migration-progress-bar").css("width","0%"),t("#wzkb-migration-progress-text").html(""),t("#wzkb-migration-errors").empty(),t("#wzkb-migration-log").empty(),t(this).prop("disabled",!0),r(0,wzkbProductMigrator.strings.starting_migration),e()})),t("#wzkb-copy-log").on("click",(function(){var o=t(this),i=t("#wzkb-migration-log"),a="";if(i.find("p").each((function(){a+=t(this).text()+"\n\n"})),!(a=a.trim()))return o.html(' Empty Log'),void setTimeout((function(){o.html(' Copy Log')}),2e3);navigator.clipboard.writeText(a).then((function(){o.html(' Copied!'),setTimeout((function(){o.html(' Copy Log')}),2e3)})).catch((function(t){o.html(' Failed!'),setTimeout((function(){o.html(' Copy Log')}),2e3)}))}))}))}(jQuery);
\ No newline at end of file
diff --git a/includes/admin/settings/class-metabox-api.php b/includes/admin/settings/class-metabox-api.php
index c635466..f5c1b63 100644
--- a/includes/admin/settings/class-metabox-api.php
+++ b/includes/admin/settings/class-metabox-api.php
@@ -16,8 +16,6 @@
/**
* ATA Metabox class to register the metabox for ata_snippets post type.
- *
- * @since 3.5.0
*/
#[\AllowDynamicProperties]
class Metabox_API {
@@ -126,14 +124,14 @@ public function add_meta_boxes() {
*/
public function admin_enqueue_scripts( $hook ) {
if ( in_array( $hook, array( 'post.php', 'post-new.php' ), true ) || get_current_screen()->post_type === $this->post_type ) {
- self::enqueue_scripts_styles();
+ $this->enqueue_scripts_styles();
}
}
/**
* Enqueues all scripts, styles, settings, and templates necessary to use the Settings API.
*/
- public static function enqueue_scripts_styles() {
+ public function enqueue_scripts_styles() {
$minimize = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
@@ -156,10 +154,10 @@ public static function enqueue_scripts_styles() {
);
// Enqueue WZ Admin JS.
- wp_enqueue_script( 'wz-admin-js' );
- wp_enqueue_script( 'wz-codemirror-js' );
- wp_enqueue_script( 'wz-taxonomy-suggest-js' );
- wp_enqueue_script( 'wz-media-selector-js' );
+ wp_enqueue_script( 'wz-' . $this->prefix . '-admin' );
+ wp_enqueue_script( 'wz-' . $this->prefix . '-codemirror' );
+ wp_enqueue_script( 'wz-' . $this->prefix . '-taxonomy-suggest' );
+ wp_enqueue_script( 'wz-' . $this->prefix . '-media-selector' );
}
/**
@@ -190,7 +188,12 @@ public function save( $post_id ) {
return;
}
- $settings_sanitize = new Settings_Sanitize();
+ $settings_sanitize = new Settings_Sanitize(
+ array(
+ 'settings_key' => $this->settings_key,
+ 'prefix' => $this->prefix,
+ )
+ );
$posted = $_POST[ $this->settings_key ]; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash
@@ -298,7 +301,7 @@ public function html( $post ) {
echo '';
/**
- * Action triggered when displaying Better Search meta box.
+ * Action triggered when displaying the metabox.
*
* @param object $post Post object.
*/
diff --git a/includes/admin/settings/class-settings-api.php b/includes/admin/settings/class-settings-api.php
index 0402f7c..f28d615 100644
--- a/includes/admin/settings/class-settings-api.php
+++ b/includes/admin/settings/class-settings-api.php
@@ -18,9 +18,8 @@
/**
* Settings API wrapper class
*
- * @version 2.5.2
+ * @version 2.7.0
*/
-#[\AllowDynamicProperties]
class Settings_API {
/**
@@ -28,7 +27,7 @@ class Settings_API {
*
* @var string
*/
- const VERSION = '2.5.2';
+ public const VERSION = '2.7.0';
/**
* Settings Key.
@@ -180,22 +179,6 @@ public function hooks() {
add_action( 'admin_init', array( $this, 'admin_init' ) );
add_filter( 'admin_footer_text', array( $this, 'admin_footer_text' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ) );
- add_filter( 'admin_body_class', array( $this, 'admin_body_class' ) );
- }
-
- /**
- * Filters the CSS classes for the body tag in the admin.
- *
- * @param string $classes Space-separated list of CSS classes.
- * @return string Space-separated list of CSS classes.
- */
- public function admin_body_class( $classes ) {
- $current_screen = get_current_screen();
-
- if ( in_array( $current_screen->id, $this->menu_pages, true ) ) {
- $classes .= ' ' . $this->prefix . '-dashboard-page';
- }
- return $classes;
}
/**
@@ -335,6 +318,8 @@ public function set_upgraded_settings( $upgraded_settings = array() ) {
* Add a menu page to the WordPress admin area.
*
* @param array $menu Array of settings for the menu page.
+ *
+ * @return string|false The resulting page’s hook_suffix, or false if the user does not have the capability required.
*/
public function add_custom_menu_page( $menu ) {
$defaults = array(
@@ -357,6 +342,8 @@ public function add_custom_menu_page( $menu ) {
);
$menu = wp_parse_args( $menu, $defaults );
+ $menu_page = false;
+
switch ( $menu['type'] ) {
case 'submenu':
$menu_page = add_submenu_page(
@@ -381,8 +368,6 @@ public function add_custom_menu_page( $menu ) {
case 'pages':
case 'comments':
$f = 'add_' . $menu['type'] . '_page';
-
- $menu_page = null;
if ( function_exists( $f ) ) {
$menu_page = $f(
$menu['page_title'],
@@ -478,43 +463,71 @@ public function admin_enqueue_scripts( $hook ) {
// Settings API scripts.
wp_register_script(
- 'wz-admin-js',
+ 'wz-' . $this->prefix . '-admin',
plugins_url( 'js/settings-admin-scripts' . $minimize . '.js', __FILE__ ),
array( 'jquery' ),
self::VERSION,
true
);
wp_register_script(
- 'wz-codemirror-js',
+ 'wz-' . $this->prefix . '-codemirror',
plugins_url( 'js/apply-codemirror' . $minimize . '.js', __FILE__ ),
array( 'jquery' ),
self::VERSION,
true
);
wp_register_script(
- 'wz-taxonomy-suggest-js',
+ 'wz-' . $this->prefix . '-taxonomy-suggest',
plugins_url( 'js/taxonomy-suggest' . $minimize . '.js', __FILE__ ),
array( 'jquery' ),
self::VERSION,
true
);
wp_register_script(
- 'wz-media-selector-js',
+ 'wz-' . $this->prefix . '-media-selector',
plugins_url( 'js/media-selector' . $minimize . '.js', __FILE__ ),
array( 'jquery' ),
self::VERSION,
true
);
+ wp_register_style(
+ 'wz-' . $this->prefix . '-admin',
+ plugins_url( 'css/admin-style' . $minimize . '.css', __FILE__ ),
+ array(),
+ self::VERSION
+ );
+
+ // Top Select scripts and styles.
+ wp_register_style(
+ 'wz-' . $this->prefix . '-tom-select',
+ 'https://cdn.jsdelivr.net/npm/tom-select@2.3.1/dist/css/tom-select.min.css',
+ array(),
+ '2.3.1'
+ );
+ wp_register_script(
+ 'wz-' . $this->prefix . '-tom-select',
+ 'https://cdn.jsdelivr.net/npm/tom-select@2.3.1/dist/js/tom-select.complete.min.js',
+ array( 'jquery' ),
+ '2.3.1',
+ true
+ );
+ wp_register_script(
+ 'wz-' . $this->prefix . '-tom-select-init',
+ plugin_dir_url( __FILE__ ) . 'js/tom-select-init' . $minimize . '.js',
+ array( 'jquery', 'wz-' . $this->prefix . '-tom-select' ),
+ self::VERSION,
+ true
+ );
if ( $hook === $this->settings_page ) {
- self::enqueue_scripts_styles();
+ $this->enqueue_scripts_styles();
}
}
/**
* Enqueues all scripts, styles, settings, and templates necessary to use the Settings API.
*/
- public static function enqueue_scripts_styles() {
+ public function enqueue_scripts_styles() {
wp_enqueue_style( 'wp-color-picker' );
@@ -534,9 +547,28 @@ public static function enqueue_scripts_styles() {
)
);
- wp_enqueue_script( 'wz-admin-js' );
- wp_enqueue_script( 'wz-codemirror-js' );
- wp_enqueue_script( 'wz-taxonomy-suggest-js' );
+ wp_enqueue_script( 'wz-' . $this->prefix . '-admin' );
+ wp_enqueue_script( 'wz-' . $this->prefix . '-codemirror' );
+ wp_enqueue_script( 'wz-' . $this->prefix . '-taxonomy-suggest' );
+ wp_enqueue_script( 'wz-' . $this->prefix . '-media-selector' );
+
+ // Enqueue Tom Select.
+ wp_enqueue_style( 'wz-' . $this->prefix . '-tom-select' );
+ wp_enqueue_script( 'wz-' . $this->prefix . '-tom-select' );
+
+ // Localize Tom Select settings.
+ wp_localize_script(
+ 'wz-' . $this->prefix . '-tom-select-init',
+ 'WZTomSelectSettings',
+ array(
+ 'action' => $this->prefix . '_kit_search',
+ 'nonce' => wp_create_nonce( $this->prefix . '_kit_search' ),
+ 'endpoint' => 'forms',
+ )
+ );
+ wp_enqueue_script( 'wz-' . $this->prefix . '-tom-select-init' );
+
+ wp_enqueue_style( 'wz-' . $this->prefix . '-admin' );
}
/**
@@ -574,26 +606,7 @@ public function admin_init() {
foreach ( $settings as $setting ) {
- $args = wp_parse_args(
- $setting,
- array(
- 'section' => $section,
- 'id' => null,
- 'name' => '',
- 'desc' => '',
- 'type' => null,
- 'default' => '',
- 'options' => '',
- 'max' => null,
- 'min' => null,
- 'step' => null,
- 'size' => null,
- 'field_class' => '',
- 'field_attributes' => '',
- 'placeholder' => '',
- 'pro' => false,
- )
- );
+ $args = self::parse_field_args( $setting, $section );
$id = $args['id'];
$name = $args['name'];
@@ -666,8 +679,12 @@ public function settings_defaults() {
$options[ $option['id'] ] = 0;
}
// If an option is set.
- if ( in_array( $option['type'], array( 'textarea', 'css', 'html', 'text', 'url', 'csv', 'color', 'numbercsv', 'postids', 'posttypes', 'number', 'wysiwyg', 'file', 'password' ), true ) && isset( $option['options'] ) ) {
- $options[ $option['id'] ] = $option['options'];
+ if ( in_array( $option['type'], array( 'textarea', 'css', 'html', 'text', 'url', 'csv', 'color', 'numbercsv', 'postids', 'posttypes', 'number', 'wysiwyg', 'file', 'password' ), true ) ) {
+ if ( isset( $option['default'] ) ) {
+ $options[ $option['id'] ] = $option['default'];
+ } elseif ( isset( $option['options'] ) ) {
+ $options[ $option['id'] ] = $option['options'];
+ }
}
if ( in_array( $option['type'], array( 'multicheck', 'radio', 'select', 'radiodesc', 'thumbsizes' ), true ) && isset( $option['default'] ) ) {
$options[ $option['id'] ] = $option['default'];
@@ -717,6 +734,58 @@ public function settings_reset() {
delete_option( $this->settings_key );
}
+ /**
+ * Get sanitization callback for given Settings key.
+ *
+ * @param string $key Settings key.
+ *
+ * @return mixed Callback function or false if callback isn't found.
+ */
+ public function get_sanitize_callback( $key = '' ) {
+ if ( empty( $key ) ) {
+ return false;
+ }
+
+ $settings_sanitize = new Settings_Sanitize(
+ array(
+ 'settings_key' => $this->settings_key,
+ 'prefix' => $this->prefix,
+ )
+ );
+
+ // Iterate over registered fields and see if we can find proper callback.
+ foreach ( $this->registered_settings as $section => $settings ) {
+ foreach ( $settings as $setting ) {
+ if ( $setting['id'] !== $key ) {
+ continue;
+ }
+
+ // Return the callback name.
+ $sanitize_callback = false;
+
+ if ( isset( $setting['sanitize_callback'] ) && is_callable( $setting['sanitize_callback'] ) ) {
+ $sanitize_callback = $setting['sanitize_callback'];
+ return $sanitize_callback;
+ }
+
+ if ( is_callable( array( $settings_sanitize, 'sanitize_' . $setting['type'] . '_field' ) ) ) {
+ // For repeater fields, create a closure to pass the field configuration.
+ if ( 'repeater' === $setting['type'] ) {
+ return function ( $value ) use ( $settings_sanitize, $setting ) {
+ return $settings_sanitize->sanitize_repeater_field( $value, $setting );
+ };
+ }
+ $sanitize_callback = array( $settings_sanitize, 'sanitize_' . $setting['type'] . '_field' );
+ return $sanitize_callback;
+ }
+
+ return $sanitize_callback;
+ }
+ }
+
+ return false;
+ }
+
/**
* Sanitize the form data being submitted.
*
@@ -724,7 +793,6 @@ public function settings_reset() {
* @return array Sanitized array
*/
public function settings_sanitize( $input ) {
-
// This should be set if a form is submitted, so let's save it in the $referrer variable.
if ( empty( $_POST['_wp_http_referer'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
return $input;
@@ -765,7 +833,6 @@ public function settings_sanitize( $input ) {
// Loop through each setting being saved and pass it through a sanitization filter.
foreach ( $settings_types as $key => $type ) {
-
/**
* Skip settings that are not really settings.
*
@@ -778,12 +845,18 @@ public function settings_sanitize( $input ) {
}
if ( array_key_exists( $key, $output ) ) {
-
$sanitize_callback = $this->get_sanitize_callback( $key );
// If callback is set, call it.
if ( $sanitize_callback ) {
- $output[ $key ] = call_user_func( $sanitize_callback, $output[ $key ] );
+ // Pass the field configuration for repeater fields.
+ if ( 'repeater' === $type && isset( $this->registered_settings[ $key ] ) ) {
+ $output[ $key ] = call_user_func( $sanitize_callback, $output[ $key ], $this->registered_settings[ $key ] );
+ } elseif ( 'sensitive' === $type ) {
+ $output[ $key ] = call_user_func( $sanitize_callback, $output[ $key ], $key );
+ } else {
+ $output[ $key ] = call_user_func( $sanitize_callback, $output[ $key ] );
+ }
continue;
}
}
@@ -805,52 +878,11 @@ public function settings_sanitize( $input ) {
* Filter the settings array before it is returned.
*
* @param array $output Settings array.
- * @param array $input Input settings array.
+ * @param array $input Input settings array.
*/
return apply_filters( $this->prefix . '_settings_sanitize', $output, $input );
}
- /**
- * Get sanitization callback for given Settings key.
- *
- * @param string $key Settings key.
- *
- * @return mixed Callback function or false if callback isn't found.
- */
- public function get_sanitize_callback( $key = '' ) {
- if ( empty( $key ) ) {
- return false;
- }
-
- $settings_sanitize = new Settings_Sanitize();
-
- // Iterate over registered fields and see if we can find proper callback.
- foreach ( $this->registered_settings as $section => $settings ) {
- foreach ( $settings as $setting ) {
- if ( $setting['id'] !== $key ) {
- continue;
- }
-
- // Return the callback name.
- $sanitize_callback = false;
-
- if ( isset( $setting['sanitize_callback'] ) && is_callable( $setting['sanitize_callback'] ) ) {
- $sanitize_callback = $setting['sanitize_callback'];
- return $sanitize_callback;
- }
-
- if ( is_callable( array( $settings_sanitize, 'sanitize_' . $setting['type'] . '_field' ) ) ) {
- $sanitize_callback = array( $settings_sanitize, 'sanitize_' . $setting['type'] . '_field' );
- return $sanitize_callback;
- }
-
- return $sanitize_callback;
- }
- }
-
- return false;
- }
-
/**
* Render the settings page.
*/
@@ -858,7 +890,6 @@ public function plugin_settings() {
ob_start();
?>
- prefix . '_settings_page_header_before' ); ?>
translation_strings['page_header'] ); ?>
prefix . '_settings_page_header' ); ?>
@@ -874,7 +905,12 @@ public function plugin_settings() {
@@ -1021,4 +1057,141 @@ public function settings_help() {
$screen->add_help_tab( $tab );
}
}
+
+ /**
+ * Parse field arguments with defaults.
+ *
+ * @since 1.0.0
+ *
+ * @param array $field Field arguments.
+ * @param string $section Section name.
+ *
+ * @return array Parsed field arguments.
+ */
+ public static function parse_field_args( $field, $section = '' ) {
+ $defaults = array(
+ 'id' => null,
+ 'name' => '',
+ 'desc' => '',
+ 'type' => 'text',
+ 'size' => null,
+ 'options' => '',
+ 'default' => '',
+ 'min' => 0,
+ 'max' => 999999,
+ 'step' => 1,
+ 'field_class' => '',
+ 'field_attributes' => array(),
+ 'placeholder' => '',
+ 'readonly' => false,
+ 'required' => false,
+ 'disabled' => false,
+ 'pro' => false,
+ 'section' => $section,
+ );
+
+ $field = wp_parse_args( $field, $defaults );
+
+ // Add required indicator to field name if the field is required.
+ if ( ! empty( $field['required'] ) && true === $field['required'] ) {
+ $field['name'] = sprintf( '%s
* ', $field['name'], esc_attr__( 'Required', 'glue-link' ) );
+ }
+
+ return $field;
+ }
+
+ /**
+ * Get the encryption key for API key encryption/decryption.
+ *
+ * @return string The encryption key.
+ */
+ private static function get_encryption_key() {
+ return defined( 'AUTH_SALT' ) ? AUTH_SALT : ( defined( 'SECURE_AUTH_SALT' ) ? SECURE_AUTH_SALT : hash( 'sha256', __NAMESPACE__ . 'knowledgebase_encryption_fallback' ) );
+ }
+
+ /**
+ * Encrypts an API key using either OpenSSL or Sodium, if available.
+ *
+ * @param string $key The API key to encrypt.
+ * @return string The encrypted API key, or the plain text key if no secure method is available.
+ */
+ public static function encrypt_api_key( $key ) {
+ if ( empty( $key ) ) {
+ return '';
+ }
+
+ // Use OpenSSL if available.
+ if ( extension_loaded( 'openssl' ) ) {
+ $iv_length = openssl_cipher_iv_length( 'aes-256-cbc' );
+ $iv = openssl_random_pseudo_bytes( $iv_length );
+ $encrypted = openssl_encrypt( $key, 'aes-256-cbc', self::get_encryption_key(), 0, $iv );
+
+ // Store IV + ciphertext in hex format.
+ return 'enc:' . bin2hex( $iv . $encrypted );
+ }
+
+ // Use Sodium (libsodium) if OpenSSL is unavailable.
+ if ( extension_loaded( 'sodium' ) ) {
+ $sodium_key = substr( hash( 'sha256', self::get_encryption_key(), true ), 0, SODIUM_CRYPTO_SECRETBOX_KEYBYTES );
+ $nonce = random_bytes( SODIUM_CRYPTO_SECRETBOX_NONCEBYTES );
+ $encrypted = sodium_crypto_secretbox( $key, $nonce, $sodium_key );
+
+ return 'enc:' . sodium_bin2hex( $nonce . $encrypted );
+ }
+
+ return $key;
+ }
+
+ /**
+ * Decrypts an API key using either OpenSSL or Sodium, if available.
+ *
+ * @param string $encrypted_key The encrypted API key to decrypt.
+ * @return string The decrypted API key, or the encrypted key if no secure method is available.
+ */
+ public static function decrypt_api_key( $encrypted_key ) {
+ if ( empty( $encrypted_key ) ) {
+ return '';
+ }
+
+ // If the key doesn't start with 'enc:', it's not encrypted.
+ if ( strpos( $encrypted_key, 'enc:' ) !== 0 ) {
+ return $encrypted_key;
+ }
+
+ // Remove the 'enc:' prefix.
+ $encrypted_key = substr( $encrypted_key, 4 );
+
+ // Try OpenSSL decryption.
+ if ( extension_loaded( 'openssl' ) ) {
+ $data = hex2bin( $encrypted_key );
+ if ( false === $data ) {
+ return '';
+ }
+
+ $iv_length = openssl_cipher_iv_length( 'aes-256-cbc' );
+ $iv = mb_substr( $data, 0, $iv_length, '8bit' );
+ $ciphertext = mb_substr( $data, $iv_length, null, '8bit' );
+
+ $decrypted = openssl_decrypt( $ciphertext, 'aes-256-cbc', self::get_encryption_key(), 0, $iv );
+ return false === $decrypted ? '' : $decrypted;
+ }
+
+ // Try Sodium (libsodium) decryption.
+ if ( extension_loaded( 'sodium' ) ) {
+ $sodium_key = substr( hash( 'sha256', self::get_encryption_key(), true ), 0, SODIUM_CRYPTO_SECRETBOX_KEYBYTES );
+ $decoded = sodium_hex2bin( $encrypted_key );
+
+ if ( ! $decoded ) {
+ return '';
+ }
+
+ $nonce = mb_substr( $decoded, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, '8bit' );
+ $ciphertext = mb_substr( $decoded, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, null, '8bit' );
+ $decrypted = sodium_crypto_secretbox_open( $ciphertext, $nonce, $sodium_key );
+
+ return false === $decrypted ? '' : $decrypted;
+ }
+
+ return $encrypted_key;
+ }
}
diff --git a/includes/admin/settings/class-settings-form.php b/includes/admin/settings/class-settings-form.php
index 35ddff1..f42c8d4 100644
--- a/includes/admin/settings/class-settings-form.php
+++ b/includes/admin/settings/class-settings-form.php
@@ -17,8 +17,6 @@
/**
* Generates the settings form.
- *
- * @version 2.5.0
*/
class Settings_Form {
@@ -50,6 +48,7 @@ class Settings_Form {
* Array or string of arguments. Default is blank array.
* @type string $settings_key Settings key.
* @type string $prefix Prefix.
+ * @type string $checkbox_modified_text Text to show to indicate a checkbox has been modified from its default value.
* }
*/
public function __construct( $args ) {
@@ -73,11 +72,7 @@ public function __construct( $args ) {
* @return string Description of the field.
*/
public function get_field_description( $args ) {
- if ( ! empty( $args['desc'] ) ) {
- $desc = '
' . wp_kses_post( $args['desc'] ) . '
';
- } else {
- $desc = '';
- }
+ $desc = ! empty( $args['desc'] ) ? '
' . wp_kses_post( $args['desc'] ) . '
' : '';
/**
* After Settings Output filter
@@ -86,15 +81,16 @@ public function get_field_description( $args ) {
* @param array $args Arguments array.
*/
$desc = apply_filters( $this->prefix . '_setting_field_description', $desc, $args );
+
return $desc;
}
/**
* Get the value of a settings field.
*
- * @param string $option Settings field name.
- * @param string $default_value Default text if it's not found.
- * @return string
+ * @param string $option Settings field name.
+ * @param mixed $default_value Default value if option is not found.
+ * @return mixed
*/
public function get_option( $option, $default_value = '' ) {
@@ -107,6 +103,39 @@ public function get_option( $option, $default_value = '' ) {
return $default_value;
}
+ /**
+ * Get field ID and name attributes.
+ *
+ * @param array $args Field arguments.
+ * @return array Array containing field_id and field_name.
+ */
+ protected function get_field_attributes( $args ) {
+ $id = sanitize_key( $args['id'] );
+ if ( isset( $args['_repeater_id'] ) && isset( $args['_index'] ) ) {
+ $field_id = sprintf(
+ '%s-%s-%s-fields-%s',
+ $this->settings_key,
+ $args['_repeater_id'],
+ $args['_index'],
+ $id
+ );
+ $field_name = sprintf(
+ '%s[%s][%s][fields][%s]',
+ $this->settings_key,
+ $args['_repeater_id'],
+ $args['_index'],
+ $id
+ );
+ } else {
+ $field_id = $this->settings_key . '-' . $id;
+ $field_name = $this->settings_key . '[' . $id . ']';
+ }
+
+ return array(
+ 'field_id' => $field_id,
+ 'field_name' => $field_name,
+ );
+ }
/**
* Miscellaneous callback funcion
*
@@ -156,23 +185,25 @@ public function callback_descriptive_text( $args ) {
* @param array $args Array of arguments.
*/
public function callback_text( $args ) {
-
$value = isset( $args['value'] ) ? $args['value'] : $this->get_option( $args['id'], $args['options'] );
$size = sanitize_html_class( isset( $args['size'] ) ? $args['size'] : 'regular' );
$class = sanitize_html_class( $args['field_class'] );
$placeholder = empty( $args['placeholder'] ) ? '' : ' placeholder="' . $args['placeholder'] . '"';
$disabled = ( ! empty( $args['disabled'] ) || $args['pro'] ) ? ' disabled="disabled"' : '';
$readonly = ( isset( $args['readonly'] ) && true === $args['readonly'] ) ? ' readonly="readonly"' : '';
- $attributes = $disabled . $readonly;
+ $required = ( isset( $args['required'] ) && true === $args['required'] ) ? ' required' : '';
+ $attributes = $disabled . $readonly . $required;
foreach ( (array) $args['field_attributes'] as $attribute => $val ) {
$attributes .= sprintf( ' %1$s="%2$s"', $attribute, esc_attr( $val ) );
}
+ $field_attributes = $this->get_field_attributes( $args );
+
$html = sprintf(
- '
',
- $this->settings_key,
- sanitize_key( $args['id'] ),
+ '
',
+ $field_attributes['field_id'],
+ $field_attributes['field_name'],
$class . ' ' . $size . '-text',
esc_attr( stripslashes( $value ) ),
$attributes,
@@ -242,7 +273,8 @@ public function callback_textarea( $args ) {
$placeholder = empty( $args['placeholder'] ) ? '' : ' placeholder="' . $args['placeholder'] . '"';
$disabled = ( ! empty( $args['disabled'] ) || $args['pro'] ) ? ' disabled="disabled"' : '';
$readonly = ( isset( $args['readonly'] ) && true === $args['readonly'] ) ? ' readonly="readonly"' : '';
- $attributes = $disabled . $readonly;
+ $required = ( isset( $args['required'] ) && true === $args['required'] ) ? ' required' : '';
+ $attributes = $disabled . $readonly . $required;
$html = sprintf(
'
',
@@ -327,7 +359,11 @@ public function callback_multicheck( $args ) {
$disabled = ( ! empty( $args['disabled'] ) || $args['pro'] ) ? ' disabled="disabled"' : '';
if ( ! empty( $args['options'] ) ) {
- $html .= sprintf( '
', $this->settings_key, sanitize_key( $args['id'] ) );
+ $html .= sprintf(
+ '
',
+ $this->settings_key,
+ sanitize_key( $args['id'] )
+ );
foreach ( $args['options'] as $key => $option ) {
if ( in_array( $key, $value_array, true ) ) {
@@ -504,6 +540,9 @@ public function callback_number( $args ) {
$size = isset( $args['size'] ) ? $args['size'] : 'regular';
$placeholder = empty( $args['placeholder'] ) ? '' : ' placeholder="' . esc_attr( $args['placeholder'] ) . '"';
$disabled = ( ! empty( $args['disabled'] ) || $args['pro'] ) ? ' disabled="disabled"' : '';
+ $readonly = ( isset( $args['readonly'] ) && true === $args['readonly'] ) ? ' readonly="readonly"' : '';
+ $required = ( isset( $args['required'] ) && true === $args['required'] ) ? ' required' : '';
+ $attributes = $disabled . $readonly . $required;
$html = sprintf(
'
',
@@ -515,7 +554,7 @@ public function callback_number( $args ) {
esc_attr( stripslashes( $value ) ),
$placeholder,
$this->settings_key,
- $disabled
+ $attributes
);
$html .= $this->get_field_description( $args );
@@ -532,24 +571,29 @@ public function callback_number( $args ) {
* @return void
*/
public function callback_select( $args ) {
- $value = isset( $args['value'] ) ? $args['value'] : $this->get_option( $args['id'], $args['default'] );
- $disabled = ( ! empty( $args['disabled'] ) || $args['pro'] ) ? ' disabled="disabled"' : '';
+ $value = isset( $args['value'] ) ? $args['value'] : $this->get_option( $args['id'], $args['default'] );
+ $class = sanitize_html_class( $args['field_class'] );
+ $disabled = ( ! empty( $args['disabled'] ) || $args['pro'] ) ? ' disabled="disabled"' : '';
+ $required = ( isset( $args['required'] ) && true === $args['required'] ) ? ' required' : '';
+ $attributes = $disabled . $required;
+
+ foreach ( (array) $args['field_attributes'] as $attribute => $val ) {
+ $attributes .= sprintf( ' %1$s="%2$s"', $attribute, esc_attr( $val ) );
+ }
if ( isset( $args['chosen'] ) ) {
- $chosen = 'class="chosen"';
- } else {
- $chosen = '';
+ $class .= ' chosen';
}
$html = sprintf(
- '
',
+ '
',
$this->settings_key,
sanitize_key( $args['id'] ),
- $chosen,
- $disabled
+ $class,
+ $attributes
);
- foreach ( $args['options'] as $option => $name ) {
+ foreach ( (array) $args['options'] as $option => $name ) {
$html .= sprintf( '
%3$s ', sanitize_key( $option ), selected( $option, $value, false ), $name );
}
@@ -675,7 +719,7 @@ public function callback_wysiwyg( $args ) {
$value = isset( $args['value'] ) ? $args['value'] : $this->get_option( $args['id'], $args['options'] );
$size = isset( $args['size'] ) ? $args['size'] : '500px';
- echo '
';
+ printf( '
', esc_attr( $size ) );
$editor_settings = array(
'teeny' => true,
@@ -689,7 +733,7 @@ public function callback_wysiwyg( $args ) {
wp_editor( $value, $args['section'] . '-' . $args['id'], $editor_settings );
- echo '
';
+ printf( '
' );
echo $this->get_field_description( $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
@@ -732,15 +776,257 @@ public function callback_password( $args ) {
$class = sanitize_html_class( $args['field_class'] );
$html = sprintf(
- '
',
+ '
',
$class . ' ' . $size . '-text',
$this->settings_key,
sanitize_key( $args['id'] ),
- esc_attr( $value )
+ esc_attr( $value ),
+ ! empty( $value ) ? 'placeholder="' . esc_attr__( 'Previously saved', 'glue-link' ) . '"' : ''
);
$html .= $this->get_field_description( $args );
/** This filter has been defined in class-settings-api.php */
echo apply_filters( $this->prefix . '_after_setting_output', $html, $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
+
+ /**
+ * Callback for repeater field.
+ *
+ * @param array $args Array of arguments.
+ * @return void
+ */
+ public function callback_repeater( $args ) {
+ $value = isset( $args['value'] ) ? (array) $args['value'] : $this->get_option( $args['id'], array() );
+ $value = ! empty( $value ) && is_array( $value ) ? $value : array();
+
+ $class = ! empty( $args['field_class'] ) ? sanitize_html_class( $args['field_class'] ) : '';
+ $disabled = ( ! empty( $args['disabled'] ) || ! empty( $args['pro'] ) ) ? ' disabled="disabled"' : '';
+ $readonly = ( isset( $args['readonly'] ) && true === $args['readonly'] ) ? ' readonly="readonly"' : '';
+ $attributes = $disabled . $readonly;
+
+ // Process additional field attributes.
+ foreach ( (array) $args['field_attributes'] as $attribute => $val ) {
+ $attributes .= sprintf( ' %1$s="%2$s"', sanitize_key( $attribute ), esc_attr( $val ) );
+ }
+
+ ?>
+
>
+
+ $item ) {
+ $this->render_repeater_item( $args, $index, $item );
+ }
+ }
+ ?>
+
+
+
+
+
+
+
+
+
+ get_field_description( $args );
+
+ /** This filter has been defined in class-settings-api.php */
+ echo apply_filters( $this->prefix . '_after_setting_output', $html, $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
+ }
+
+ /**
+ * Render a single repeater item.
+ *
+ * @param array $args Repeater field arguments.
+ * @param string|int $index Current item index.
+ * @param array|null $item Item data if exists.
+ * @return void
+ */
+ private function render_repeater_item( $args, $index, $item = null ) {
+ if ( empty( $args['fields'] ) || ! is_array( $args['fields'] ) ) {
+ return;
+ }
+
+ ?>
+
+
+
+ isset( $item['fields'][ $field_id ] ) ? $item['fields'][ $field_id ] : ( isset( $field['default'] ) ? $field['default'] : '' ),
+ '_repeater_id' => $args['id'],
+ '_index' => $index,
+ )
+ );
+ $field_args = Settings_API::parse_field_args( $field_args, $args['section'] );
+
+ if ( ! isset( $field['type'] ) || ! is_string( $field['type'] ) ) {
+ continue;
+ }
+ ?>
+
+
+
+
+ $callback( $field_args );
+ } else {
+ do_action( "{$this->prefix}_repeater_field_{$field['type']}", $field_args, $index );
+ }
+ ?>
+
+
+
+
+
+
+
+
+
+ get_option( $args['id'], $args['options'] );
+ $decrypted_key = Settings_API::decrypt_api_key( $encrypted_key );
+
+ $args['value'] = $decrypted_key ? str_repeat( '*', strlen( $decrypted_key ) - 4 ) . substr( $decrypted_key, -4 ) : '';
+
+ $this->callback_text( $args );
+ }
}
diff --git a/includes/admin/settings/class-settings-sanitize.php b/includes/admin/settings/class-settings-sanitize.php
index 93f31c9..79f6613 100644
--- a/includes/admin/settings/class-settings-sanitize.php
+++ b/includes/admin/settings/class-settings-sanitize.php
@@ -20,10 +20,56 @@
*/
class Settings_Sanitize {
+ /**
+ * Settings Key.
+ *
+ * @var string Settings Key.
+ */
+ public $settings_key;
+
+ /**
+ * Prefix which is used for creating the unique filters and actions.
+ *
+ * @var string Prefix.
+ */
+ public $prefix;
+
/**
* Main constructor class.
+ *
+ * @param mixed $args {
+ * Array or string of arguments. Default is blank array.
+ * @type string $settings_key Settings key.
+ * @type string $prefix Prefix.
+ * }
*/
- public function __construct() {
+ public function __construct( $args ) {
+ $defaults = array(
+ 'settings_key' => '',
+ 'prefix' => '',
+ );
+ $args = wp_parse_args( $args, $defaults );
+
+ foreach ( $args as $name => $value ) {
+ $this->$name = $value;
+ }
+ }
+
+ /**
+ * Get the value of a settings field.
+ *
+ * @param string $option Settings field name.
+ * @param mixed $default_value Default value if option is not found.
+ * @return mixed
+ */
+ public function get_option( $option, $default_value = '' ) {
+ $options = \get_option( $this->settings_key );
+
+ if ( isset( $options[ $option ] ) ) {
+ return $options[ $option ];
+ }
+
+ return $default_value;
}
/**
@@ -194,6 +240,115 @@ public function sanitize_color_field( $value ) {
return sanitize_hex_color( $value );
}
+ /**
+ * Sanitize email fields.
+ *
+ * @param string $value The field value.
+ * @return string Sanitized value
+ */
+ public function sanitize_email_field( $value ) {
+ return sanitize_email( $value );
+ }
+
+ /**
+ * Sanitize URL fields.
+ *
+ * @param string $value The field value.
+ * @return string Sanitized value
+ */
+ public function sanitize_url_field( $value ) {
+ return esc_url_raw( $value );
+ }
+
+ /**
+ * Sanitize sensitive fields.
+ *
+ * @param string $value The field value.
+ * @param string|array $key The field key.
+ * @return string Sanitized value
+ */
+ public function sanitize_sensitive_field( $value, $key ) {
+ if ( is_array( $key ) ) {
+ if ( isset( $key['id'] ) ) {
+ $key = $key['id'];
+ } else {
+ return $value;
+ }
+ }
+
+ $stored_encrypted_key = $this->get_option( $key );
+
+ // If input is masked, return existing encrypted key.
+ if ( empty( $value ) || strpos( $value, '**' ) !== false ) {
+ return $stored_encrypted_key;
+ }
+
+ return Settings_API::encrypt_api_key( $value );
+ }
+
+ /**
+ * Sanitize repeater field.
+ *
+ * @param array $value Array of repeater values.
+ * @param array $field Field configuration array.
+ * @return array Sanitized array
+ */
+ public function sanitize_repeater_field( $value, $field = array() ) {
+ if ( ! is_array( $value ) ) {
+ return array();
+ }
+
+ $sanitized_value = array();
+
+ // Get the subfields configuration.
+ $subfields = ! empty( $field['fields'] ) ? $field['fields'] : array();
+
+ foreach ( $value as $index => $row ) {
+ // Ensure we have a valid row structure.
+ if ( ! isset( $row['fields'] ) || ! is_array( $row['fields'] ) ) {
+ continue;
+ }
+
+ $sanitized_row = array(
+ 'fields' => array(),
+ );
+
+ foreach ( $row['fields'] as $field_key => $field_value ) {
+ $field_key = sanitize_key( $field_key );
+
+ // Skip if field_key is not in our subfields configuration.
+ $field_config = null;
+ foreach ( $subfields as $subfield ) {
+ if ( isset( $subfield['id'] ) && $subfield['id'] === $field_key ) {
+ $field_config = $subfield;
+ break;
+ }
+ }
+
+ if ( null === $field_config ) {
+ continue;
+ }
+
+ // Get the field type from the subfield configuration.
+ $field_type = isset( $field_config['type'] ) ? $field_config['type'] : 'text';
+
+ // Call the appropriate sanitization method.
+ $sanitize_method = 'sanitize_' . $field_type . '_field';
+ if ( method_exists( $this, $sanitize_method ) ) {
+ $sanitized_row['fields'][ $field_key ] = $this->$sanitize_method( $field_value, $field_config );
+ } else {
+ $sanitized_row['fields'][ $field_key ] = $this->sanitize_text_field( $field_value );
+ }
+ }
+
+ if ( ! empty( $sanitized_row['fields'] ) ) {
+ $sanitized_value[ $index ] = $sanitized_row;
+ }
+ }
+
+ return $sanitized_value;
+ }
+
/**
* Convert a string to CSV.
*
diff --git a/includes/admin/settings/css/admin-style.css b/includes/admin/settings/css/admin-style.css
new file mode 100644
index 0000000..2e585a3
--- /dev/null
+++ b/includes/admin/settings/css/admin-style.css
@@ -0,0 +1,264 @@
+/* General styles for the repeater wrapper */
+.wz-repeater-wrapper {
+ position: relative;
+ margin-bottom: 1.5rem;
+}
+
+/* Repeater items container */
+.wz-repeater-wrapper .wz-repeater-item {
+ display: flex;
+ flex-direction: column;
+ background: #f9f9f9;
+ border: 1px solid #ddd;
+ padding: 1rem;
+ margin-bottom: 1rem;
+ border-radius: 4px;
+}
+
+.wz-repeater-item .repeater-item-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ border-bottom: 1px solid #ddd;
+ padding-bottom: 0.5rem;
+ cursor: pointer;
+ font-weight: bold;
+}
+
+/* Header for repeater field */
+.wz-repeater-field-header {
+ margin-bottom: 0.5rem;
+}
+
+.wz-repeater-field-label {
+ font-weight: bold;
+ font-size: 0.9rem;
+}
+
+/* Input field styles */
+.wz-repeater-field-input {
+ margin-bottom: 1rem;
+}
+
+/* Repeater item actions (buttons) */
+.repeater-item-actions {
+ display: flex;
+ gap: 0.5rem;
+ justify-content: flex-start;
+ margin-top: 1rem;
+}
+
+.repeater-item-actions .button {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.25rem;
+}
+
+.repeater-item-actions .dashicons {
+ font-size: 16px;
+}
+
+/* Move up/down button styles */
+.repeater-item-actions .move-up,
+.repeater-item-actions .move-down {
+ background: #f4f4f4;
+ color: #333;
+ border: 1px solid #ddd;
+ border-radius: 3px;
+ padding: 0.5rem;
+}
+
+.repeater-item-actions .move-up:hover,
+.repeater-item-actions .move-down:hover {
+ background: #e0e0e0;
+}
+
+/* Remove button styles */
+.repeater-item-actions .remove-item {
+ background: #fdd;
+ border-color: #fbb;
+}
+
+.repeater-item-actions .remove-item:hover {
+ background: #fbb;
+}
+
+/* Add item button styling */
+.wz-repeater-wrapper .add-item {
+ background-color: #0073aa;
+ color: #fff;
+ padding: 0.5rem 1rem;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 1rem;
+ margin-top: 1rem;
+}
+
+.wz-repeater-wrapper .add-item:hover {
+ background-color: #005177;
+ color: #fff;
+ border-color: #004063;
+}
+
+/* Template styling for new items (hidden template) */
+.repeater-template {
+ display: none;
+}
+
+/* Responsiveness for smaller screens */
+@media screen and (max-width: 768px) {
+ .wz-repeater-wrapper {
+ margin-bottom: 1rem;
+ }
+
+ .wz-repeater-item {
+ flex-direction: column;
+ }
+
+ .repeater-item-actions {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+
+ .repeater-item-actions .button {
+ margin-bottom: 0.5rem;
+ }
+}
+
+/* Field input responsive tweaks */
+@media screen and (max-width: 480px) {
+ .wz-repeater-field-label {
+ font-size: 0.85rem;
+ }
+
+ .wz-repeater-field-input {
+ font-size: 0.85rem;
+ }
+
+ .repeater-item-actions .button {
+ font-size: 0.85rem;
+ }
+
+ .add-item {
+ font-size: 1rem;
+ padding: 0.5rem;
+ }
+}
+
+/* Repeater Fields */
+.wz-repeater-item {
+ background: #fff;
+ border: 1px solid #ccd0d4;
+ box-shadow: 0 1px 1px rgba(0, 0, 0, .04);
+ margin-bottom: 15px;
+ border-radius: 3px;
+}
+
+.repeater-item-content {
+ padding: 15px;
+}
+
+.wz-repeater-field {
+ margin-bottom: 15px;
+}
+
+.wz-repeater-field:last-child {
+ margin-bottom: 0;
+}
+
+.wz-repeater-field-header {
+ margin-bottom: 8px;
+}
+
+.wz-repeater-field-label {
+ display: block;
+ font-weight: 600;
+ margin-bottom: 4px;
+}
+
+.wz-repeater-field-description {
+ color: #666;
+ font-style: italic;
+ margin: 4px 0 0;
+ font-size: 13px;
+}
+
+.wz-repeater-field-input input[type="text"],
+.wz-repeater-field-input input[type="url"],
+.wz-repeater-field-input input[type="number"],
+.wz-repeater-field-input input[type="password"],
+.wz-repeater-field-input input[type="email"],
+.wz-repeater-field-input textarea,
+.wz-repeater-field-input select {
+ width: 100%;
+ max-width: 100%;
+}
+
+.repeater-item-footer {
+ background: #f5f5f5;
+ border-top: 1px solid #ddd;
+ padding: 10px 15px;
+ text-align: right;
+}
+
+.repeater-item-footer .remove-item {
+ color: #b32d2e;
+}
+
+.repeater-item-footer .remove-item:hover {
+ color: #dc3232;
+}
+
+.repeater-item-footer .dashicons {
+ margin-top: 4px;
+}
+
+/* Required field indicator */
+.required {
+ color: #d63638;
+ font-weight: bold;
+ margin-left: 2px;
+ font-size: 1.3em;
+}
+
+/* Tom Select Customizations */
+.ts-wrapper.loading .spinner {
+ display: inline-block;
+ visibility: visible;
+ float: none;
+ margin: 5px auto;
+}
+
+.ts-wrapper .no-results {
+ padding: 5px 10px;
+ color: #666;
+ font-style: italic;
+}
+
+.ts-wrapper.form-control {
+ border: 1px solid #8c8f94;
+ border-radius: 4px;
+ min-height: 30px;
+}
+
+.ts-wrapper.focus {
+ border-color: #2271b1;
+ box-shadow: 0 0 0 1px #2271b1;
+ outline: 2px solid transparent;
+}
+
+.ts-dropdown {
+ border: 1px solid #8c8f94;
+ border-radius: 4px;
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
+}
+
+.ts-dropdown .option {
+ padding: 6px 10px;
+}
+
+.ts-dropdown .active {
+ background-color: #2271b1;
+ color: #fff;
+}
\ No newline at end of file
diff --git a/includes/admin/settings/css/admin-style.min.css b/includes/admin/settings/css/admin-style.min.css
new file mode 100644
index 0000000..1dbdfcb
--- /dev/null
+++ b/includes/admin/settings/css/admin-style.min.css
@@ -0,0 +1 @@
+.wz-repeater-wrapper{position:relative;margin-bottom:1.5rem;}.wz-repeater-wrapper .wz-repeater-item{display:flex;flex-direction:column;background:#f9f9f9;border:1px solid #ddd;padding:1rem;margin-bottom:1rem;border-radius:4px;}.wz-repeater-item .repeater-item-header{display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid #ddd;padding-bottom:.5rem;cursor:pointer;font-weight:bold;}.wz-repeater-field-header{margin-bottom:.5rem;}.wz-repeater-field-label{font-weight:bold;font-size:.9rem;}.wz-repeater-field-input{margin-bottom:1rem;}.repeater-item-actions{display:flex;gap:.5rem;justify-content:flex-start;margin-top:1rem;}.repeater-item-actions .button{display:inline-flex;align-items:center;gap:.25rem;}.repeater-item-actions .dashicons{font-size:16px;}.repeater-item-actions .move-up,.repeater-item-actions .move-down{background:#f4f4f4;color:#333;border:1px solid #ddd;border-radius:3px;padding:.5rem;}.repeater-item-actions .move-up:hover,.repeater-item-actions .move-down:hover{background:#e0e0e0;}.repeater-item-actions .remove-item{background:#fdd;border-color:#fbb;}.repeater-item-actions .remove-item:hover{background:#fbb;}.wz-repeater-wrapper .add-item{background-color:#0073aa;color:#fff;padding:.5rem 1rem;border:none;border-radius:4px;cursor:pointer;font-size:1rem;margin-top:1rem;}.wz-repeater-wrapper .add-item:hover{background-color:#005177;color:#fff;border-color:#004063;}.repeater-template{display:none;}@media screen and (max-width:768px){.wz-repeater-wrapper{margin-bottom:1rem}.wz-repeater-item{flex-direction:column}.repeater-item-actions{flex-direction:column;align-items:flex-start}.repeater-item-actions .button{margin-bottom:.5rem}}@media screen and (max-width:480px){.wz-repeater-field-label{font-size:.85rem}.wz-repeater-field-input{font-size:.85rem}.repeater-item-actions .button{font-size:.85rem}.add-item{font-size:1rem;padding:.5rem}}.wz-repeater-item{background:#fff;border:1px solid #ccd0d4;box-shadow:0 1px 1px rgba(0,0,0,.04);margin-bottom:15px;border-radius:3px;}.repeater-item-content{padding:15px;}.wz-repeater-field{margin-bottom:15px;}.wz-repeater-field:last-child{margin-bottom:0;}.wz-repeater-field-header{margin-bottom:8px;}.wz-repeater-field-label{display:block;font-weight:600;margin-bottom:4px;}.wz-repeater-field-description{color:#666;font-style:italic;margin:4px 0 0;font-size:13px;}.wz-repeater-field-input input[type="text"],.wz-repeater-field-input input[type="url"],.wz-repeater-field-input input[type="number"],.wz-repeater-field-input input[type="password"],.wz-repeater-field-input input[type="email"],.wz-repeater-field-input textarea,.wz-repeater-field-input select{width:100%;max-width:100%;}.repeater-item-footer{background:#f5f5f5;border-top:1px solid #ddd;padding:10px 15px;text-align:right;}.repeater-item-footer .remove-item{color:#b32d2e;}.repeater-item-footer .remove-item:hover{color:#dc3232;}.repeater-item-footer .dashicons{margin-top:4px;}.required{color:#d63638;font-weight:bold;margin-left:2px;font-size:1.3em;}.ts-wrapper.loading .spinner{display:inline-block;visibility:visible;float:none;margin:5px auto;}.ts-wrapper .no-results{padding:5px 10px;color:#666;font-style:italic;}.ts-wrapper.form-control{border:1px solid #8c8f94;border-radius:4px;min-height:30px;}.ts-wrapper.focus{border-color:#2271b1;box-shadow:0 0 0 1px #2271b1;outline:2px solid transparent;}.ts-dropdown{border:1px solid #8c8f94;border-radius:4px;box-shadow:0 2px 5px rgba(0,0,0,.1);}.ts-dropdown .option{padding:6px 10px;}.ts-dropdown .active{background-color:#2271b1;color:#fff;}
\ No newline at end of file
diff --git a/includes/admin/settings/js/settings-admin-scripts.js b/includes/admin/settings/js/settings-admin-scripts.js
index 85de344..f886f74 100644
--- a/includes/admin/settings/js/settings-admin-scripts.js
+++ b/includes/admin/settings/js/settings-admin-scripts.js
@@ -58,7 +58,7 @@ jQuery(document).ready(function ($) {
});
});
- // Initialise ColorPicker.
+ // Initialize ColorPicker.
$('.color-field').each(function (i, element) {
$(element).wpColorPicker();
});
diff --git a/includes/admin/settings/js/settings-admin-scripts.min.js b/includes/admin/settings/js/settings-admin-scripts.min.js
index 17ab277..96a0aba 100644
--- a/includes/admin/settings/js/settings-admin-scripts.min.js
+++ b/includes/admin/settings/js/settings-admin-scripts.min.js
@@ -1 +1 @@
-jQuery(document).ready(function(t){function n(){i=1}function e(){if(1==i)return!0}function a(){i=0}t(".file-browser").on("click",function(n){n.preventDefault();var e=t(this),a=wp.media.frames.file_frame=wp.media({title:e.data("uploader_title"),button:{text:e.data("uploader_button_text")},multiple:!1});a.on("select",function(){attachment=a.state().get("selection").first().toJSON(),e.prev(".file-url").val(attachment.url).change()}),a.open()});var i=0;t("form").on("change","input, textarea, select",n),window.onbeforeunload=e,t('input[name="submit"], input#search-submit, input#doaction, input#doaction2, input[name="filter_action"]').on("click",a),t(function(){t("#post-body-content").tabs({create:function(n,e){t(e.tab.find("a")).addClass("nav-tab-active")},activate:function(n,e){t(e.oldTab.find("a")).removeClass("nav-tab-active"),t(e.newTab.find("a")).addClass("nav-tab-active")}})}),t(".color-field").each(function(n,e){t(e).wpColorPicker()}),t(".reset-default-thumb").on("click",function(){t("#wzkb_settings\\[thumb_default\\]").val(wzkb_admin.thumb_default)})});
\ No newline at end of file
+jQuery(document).ready((function(t){t(".file-browser").on("click",(function(e){e.preventDefault();var n=t(this),a=wp.media.frames.file_frame=wp.media({title:n.data("uploader_title"),button:{text:n.data("uploader_button_text")},multiple:!1});a.on("select",(function(){attachment=a.state().get("selection").first().toJSON(),n.prev(".file-url").val(attachment.url).change()})),a.open()}));var e=0;t("form").on("change","input, textarea, select",(function(){e=1})),window.onbeforeunload=function(){if(1==e)return!0},t('input[name="submit"], input#search-submit, input#doaction, input#doaction2, input[name="filter_action"]').on("click",(function(){e=0})),t((function(){t("#post-body-content").tabs({create:function(e,n){t(n.tab.find("a")).addClass("nav-tab-active")},activate:function(e,n){t(n.oldTab.find("a")).removeClass("nav-tab-active"),t(n.newTab.find("a")).addClass("nav-tab-active")}})})),t(".color-field").each((function(e,n){t(n).wpColorPicker()})),t(".reset-default-thumb").on("click",(function(){t("#wzkb_settings\\[thumb_default\\]").val(wzkb_admin.thumb_default)}))}));
\ No newline at end of file
diff --git a/includes/admin/settings/js/taxonomy-suggest.js b/includes/admin/settings/js/taxonomy-suggest.js
index c5a9eb9..4c59a2b 100644
--- a/includes/admin/settings/js/taxonomy-suggest.js
+++ b/includes/admin/settings/js/taxonomy-suggest.js
@@ -105,8 +105,4 @@ jQuery(document).ready(function($) {
$( '.category_autocomplete' ).each( function ( i, element ) {
$( element ).WZTagsSuggest();
});
-
- $('.widget-liquid-right, #customize-controls').on( 'click', '.category_autocomplete', function() {
- $( '.category_autocomplete' ).WZTagsSuggest();
- });
});
diff --git a/includes/admin/settings/js/taxonomy-suggest.min.js b/includes/admin/settings/js/taxonomy-suggest.min.js
index cf097e7..6988548 100644
--- a/includes/admin/settings/js/taxonomy-suggest.min.js
+++ b/includes/admin/settings/js/taxonomy-suggest.min.js
@@ -1 +1 @@
-jQuery(document).ready((function(t){t.fn.WZTagsSuggest=function(e){var o,n,a=t(this),u=(e=e||{}).taxonomy||a.attr("data-wp-taxonomy")||"category",c=e.tag_search||a.attr("data-wp-action")||"wz_tags_search";function i(t){return t.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/)}function r(t){return i(t).pop()}delete e.taxonomy,delete e.tag_search,e=t.extend({minLength:2,position:{my:"left top+2",at:"left bottom",collision:"none"},source:function(e,a){var i;n!==e.term?(i=r(e.term),n!==e.term?(t.ajax({type:"POST",dataType:"json",url:ajaxurl,data:{action:c,tax:u,q:i}}).done((function(t){o=t,a(t)})),n=e.term):a(o)):a(o)},search:function(){if(r(this.value).length<2)return!1},focus:function(t,e){t.preventDefault()},select:function(t,e){var o=i(this.value),n=e.item.value;return-1!==n.indexOf(",")&&(n='"'+n+'"'),o.pop(),o.push(n),o.push(""),this.value=o.join(", "),!1}},e),a.on("keydown",(function(e){e.keyCode===t.ui.keyCode.TAB&&t(this).autocomplete("instance").menu.active&&e.preventDefault()})).autocomplete(e)},t(".category_autocomplete").each((function(e,o){t(o).WZTagsSuggest()})),t(".widget-liquid-right, #customize-controls").on("click",".category_autocomplete",(function(){t(".category_autocomplete").WZTagsSuggest()}))}));
\ No newline at end of file
+jQuery(document).ready((function(t){t.fn.WZTagsSuggest=function(e){var n,a,o=t(this),u=(e=e||{}).taxonomy||o.attr("data-wp-taxonomy")||"category",i=e.tag_search||o.attr("data-wp-action")||"wz_tags_search";function c(t){return t.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/)}function r(t){return c(t).pop()}delete e.taxonomy,delete e.tag_search,e=t.extend({minLength:2,position:{my:"left top+2",at:"left bottom",collision:"none"},source:function(e,o){var c;a!==e.term?(c=r(e.term),a!==e.term?(t.ajax({type:"POST",dataType:"json",url:ajaxurl,data:{action:i,tax:u,q:c}}).done((function(t){n=t,o(t)})),a=e.term):o(n)):o(n)},search:function(){if(r(this.value).length<2)return!1},focus:function(t,e){t.preventDefault()},select:function(t,e){var n=c(this.value),a=e.item.value;return-1!==a.indexOf(",")&&(a='"'+a+'"'),n.pop(),n.push(a),n.push(""),this.value=n.join(", "),!1}},e),o.on("keydown",(function(e){e.keyCode===t.ui.keyCode.TAB&&t(this).autocomplete("instance").menu.active&&e.preventDefault()})).autocomplete(e)},t(".category_autocomplete").each((function(e,n){t(n).WZTagsSuggest()}))}));
\ No newline at end of file
diff --git a/includes/admin/settings/js/tom-select-init.js b/includes/admin/settings/js/tom-select-init.js
new file mode 100644
index 0000000..009f2b0
--- /dev/null
+++ b/includes/admin/settings/js/tom-select-init.js
@@ -0,0 +1,106 @@
+/* global ajaxurl, WZTomSelectSettings, jQuery, TomSelect */
+
+(function ($) {
+ 'use strict';
+
+ function initTomSelect() {
+ const elements = document.querySelectorAll('.ts_autocomplete');
+
+ elements.forEach(function (element) {
+ const prefix = element.getAttribute('data-wp-prefix') || 'WZ';
+ const settingsKey = `${prefix}TomSelectSettings`;
+ const settings = window[settingsKey] || WZTomSelectSettings;
+
+ const action = element.getAttribute('data-wp-action') || settings.action;
+ const nonce = element.getAttribute('data-wp-nonce') || settings.nonce;
+ const endpoint = element.getAttribute('data-wp-endpoint') || settings.endpoint;
+ const forms = settings.forms;
+ const tags = settings.tags;
+ const custom_fields = settings.custom_fields;
+ const strings = settings.strings;
+
+ const options = endpoint === 'forms' ? forms : (endpoint === 'tags' ? tags : (endpoint === 'custom_fields' ? custom_fields : []));
+
+ if (!options || !Array.isArray(options)) {
+ console.error('Invalid options for endpoint:', endpoint);
+ return;
+ }
+
+ const formattedOptions = options.map(item => ({ value: item.id, text: item.name }));
+
+ const savedIds = element.value.split(',').map(id => id.trim()).filter(Boolean);
+
+ // Get any custom config from data attributes
+ let customConfig = {};
+ const configAttr = element.getAttribute('data-ts-config');
+
+ if (configAttr) {
+ try {
+ customConfig = JSON.parse(configAttr);
+ console.log('Parsed custom config:', customConfig); // Debug log
+ } catch (e) {
+ console.error('Error parsing custom config:', configAttr, e);
+ }
+ }
+
+ // Default config
+ const defaultConfig = {
+ plugins: ['dropdown_input', 'clear_button', 'remove_button'],
+ valueField: 'value',
+ labelField: 'text',
+ searchField: ['text', 'value'],
+ options: formattedOptions,
+ items: savedIds,
+ persist: true,
+ createOnBlur: false,
+ create: false,
+ render: {
+ no_results: (data, escape) => `
${strings.no_results.replace('%s', escape(data.input))}
`,
+ option: (data, escape) => `
${escape(data.text)} (${escape(data.value)})
`,
+ item: (data, escape) => `
${escape(data.text)} (${escape(data.value)})
`
+ },
+ load: function (query, callback) {
+ if (!query.length) {
+ callback();
+ return;
+ }
+
+ $.ajax({
+ url: ajaxurl,
+ type: 'POST',
+ dataType: 'json',
+ data: {
+ action: action,
+ nonce: nonce,
+ q: query,
+ endpoint: endpoint
+ },
+ error: function () {
+ callback();
+ },
+ success: function (res) {
+ if (res.success && Array.isArray(res.data)) {
+ callback(res.data.map(item => ({ value: item.id, text: item.name })));
+ } else {
+ callback();
+ }
+ }
+ });
+ }
+ };
+
+ // Merge default config with custom config
+ const finalConfig = { ...defaultConfig, ...customConfig };
+ console.log('Final config:', finalConfig); // Debug log
+
+ // Initialize Tom Select with merged config
+ try {
+ new TomSelect(element, finalConfig);
+ } catch (error) {
+ console.error('Tom Select initialization error:', error);
+ }
+ });
+ }
+
+ $(document).ready(initTomSelect);
+})(jQuery);
diff --git a/includes/admin/settings/js/tom-select-init.min.js b/includes/admin/settings/js/tom-select-init.min.js
new file mode 100644
index 0000000..2a93861
--- /dev/null
+++ b/includes/admin/settings/js/tom-select-init.min.js
@@ -0,0 +1 @@
+!function(t){"use strict";t(document).ready((function(){document.querySelectorAll(".ts_autocomplete").forEach((function(e){const a=e.getAttribute("data-wp-prefix")||"WZ",n=window[`${a}TomSelectSettings`]||WZTomSelectSettings,i=e.getAttribute("data-wp-action")||n.action,o=e.getAttribute("data-wp-nonce")||n.nonce,r=e.getAttribute("data-wp-endpoint")||n.endpoint,s=n.forms,c=n.tags,u=n.custom_fields,l=n.strings,d="forms"===r?s:"tags"===r?c:"custom_fields"===r?u:[];if(!d||!Array.isArray(d))return;const p=d.map((t=>({value:t.id,text:t.name}))),m=e.value.split(",").map((t=>t.trim())).filter(Boolean);let f={};const v=e.getAttribute("data-ts-config");if(v)try{f=JSON.parse(v)}catch(t){}const g={...{plugins:["dropdown_input","clear_button","remove_button"],valueField:"value",labelField:"text",searchField:["text","value"],options:p,items:m,persist:!0,createOnBlur:!1,create:!1,render:{no_results:(t,e)=>`
${l.no_results.replace("%s",e(t.input))}
`,option:(t,e)=>`
${e(t.text)} (${e(t.value)})
`,item:(t,e)=>`
${e(t.text)} (${e(t.value)})
`},load:function(e,a){e.length?t.ajax({url:ajaxurl,type:"POST",dataType:"json",data:{action:i,nonce:o,q:e,endpoint:r},error:function(){a()},success:function(t){t.success&&Array.isArray(t.data)?a(t.data.map((t=>({value:t.id,text:t.name})))):a()}}):a()}},...f};try{new TomSelect(e,g)}catch(t){}}))}))}(jQuery);
\ No newline at end of file
diff --git a/includes/admin/settings/sidebar.php b/includes/admin/sidebar.php
similarity index 85%
rename from includes/admin/settings/sidebar.php
rename to includes/admin/sidebar.php
index 1e036e0..c15558f 100644
--- a/includes/admin/settings/sidebar.php
+++ b/includes/admin/sidebar.php
@@ -9,7 +9,7 @@
-
+
@@ -20,7 +20,7 @@
\ No newline at end of file
diff --git a/includes/autoloader.php b/includes/autoloader.php
index 9d58ad8..bb87e86 100644
--- a/includes/autoloader.php
+++ b/includes/autoloader.php
@@ -1,6 +1,6 @@
_x( 'Knowledge Base', 'Post Type General Name', 'knowledgebase' ),
- 'singular_name' => _x( 'Knowledge Base', 'Post Type Singular Name', 'knowledgebase' ),
- 'menu_name' => __( 'Knowledge Base', 'knowledgebase' ),
- 'name_admin_bar' => __( 'Knowledge Base Article', 'knowledgebase' ),
- 'parent_item_colon' => __( 'Parent Article', 'knowledgebase' ),
- 'all_items' => __( 'All Articles', 'knowledgebase' ),
- 'add_new_item' => __( 'Add New Article', 'knowledgebase' ),
- 'add_new' => __( 'Add New Article', 'knowledgebase' ),
- 'new_item' => __( 'New Article', 'knowledgebase' ),
- 'edit_item' => __( 'Edit Article', 'knowledgebase' ),
- 'update_item' => __( 'Update Article', 'knowledgebase' ),
- 'view_item' => __( 'View Article', 'knowledgebase' ),
- 'search_items' => __( 'Search Article', 'knowledgebase' ),
- 'not_found' => __( 'Not found', 'knowledgebase' ),
- 'not_found_in_trash' => __( 'Not found in Trash', 'knowledgebase' ),
+ 'name' => _x( 'Knowledge Base', 'Post Type General Name', 'knowledgebase' ),
+ 'singular_name' => _x( 'Knowledge Base', 'Post Type Singular Name', 'knowledgebase' ),
+ 'menu_name' => __( 'Knowledge Base', 'knowledgebase' ),
+ 'name_admin_bar' => __( 'KB Article', 'knowledgebase' ),
+ 'parent_item_colon' => __( 'Parent Article:', 'knowledgebase' ),
+ 'all_items' => __( 'All Articles', 'knowledgebase' ),
+ 'add_new_item' => __( 'Add New Article', 'knowledgebase' ),
+ 'add_new' => __( 'Add New Article', 'knowledgebase' ),
+ 'new_item' => __( 'New Article', 'knowledgebase' ),
+ 'edit_item' => __( 'Edit Article', 'knowledgebase' ),
+ 'update_item' => __( 'Update Article', 'knowledgebase' ),
+ 'view_item' => __( 'View Article', 'knowledgebase' ),
+ 'search_items' => __( 'Search Articles', 'knowledgebase' ),
+ 'not_found' => __( 'Not found', 'knowledgebase' ),
+ 'not_found_in_trash' => __( 'Not found in Trash', 'knowledgebase' ),
+ 'featured_image' => __( 'Article Featured Image', 'knowledgebase' ),
+ 'set_featured_image' => __( 'Set Article featured image', 'knowledgebase' ),
+ 'remove_featured_image' => __( 'Remove Article featured image', 'knowledgebase' ),
+ 'use_featured_image' => __( 'Use as Article featured image', 'knowledgebase' ),
+ 'insert_into_item' => __( 'Insert into Article', 'knowledgebase' ),
+ 'uploaded_to_this_item' => __( 'Uploaded to this Article', 'knowledgebase' ),
+ 'items_list' => __( 'Articles list', 'knowledgebase' ),
+ 'items_list_navigation' => __( 'Articles list navigation', 'knowledgebase' ),
+ 'filter_items_list' => __( 'Filter Articles list', 'knowledgebase' ),
);
/**
@@ -99,103 +108,140 @@ public static function register_post_type() {
}
/**
- * Register Knowledgebase Custom Taxonomies.
+ * Get base arguments for taxonomies.
*
- * @since 2.3.0
+ * @since 2.5.0
+ *
+ * @param string $slug Taxonomy slug.
+ * @param bool $is_hierarchical Whether the taxonomy is hierarchical.
+ *
+ * @return array Base arguments for the taxonomy.
*/
- public static function register_taxonomies() {
- $catslug = \wzkb_get_option( 'category_slug', 'section' );
- $tagslug = \wzkb_get_option( 'tag_slug', 'kb-tags' );
-
- $args = array(
- 'hierarchical' => true,
+ private static function get_taxonomy_base_args( string $slug, bool $is_hierarchical = true ): array {
+ return array(
+ 'hierarchical' => $is_hierarchical,
'show_admin_column' => true,
- 'show_tagcloud' => false,
'show_in_rest' => true,
+ 'show_tagcloud' => ! $is_hierarchical,
'rewrite' => array(
- 'slug' => $catslug,
+ 'slug' => $slug,
'with_front' => true,
- 'hierarchical' => true,
+ 'hierarchical' => $is_hierarchical,
),
);
+ }
- // Now register categories for the Knowledge Base.
- $catlabels = array(
- 'name' => _x( 'Sections', 'Taxonomy General Name', 'knowledgebase' ),
- 'singular_name' => _x( 'Section', 'Taxonomy Singular Name', 'knowledgebase' ),
- 'menu_name' => __( 'Sections', 'knowledgebase' ),
- 'all_items' => __( 'All Sections', 'knowledgebase' ),
- 'parent_item' => __( 'Parent Section', 'knowledgebase' ),
- 'parent_item_colon' => __( 'Parent Section:', 'knowledgebase' ),
- 'new_item_name' => __( 'New Section Name', 'knowledgebase' ),
- 'add_new_item' => __( 'Add New Section', 'knowledgebase' ),
- 'edit_item' => __( 'Edit Section', 'knowledgebase' ),
- 'update_item' => __( 'Update Section', 'knowledgebase' ),
- 'view_item' => __( 'View Section', 'knowledgebase' ),
- 'separate_items_with_commas' => __( 'Separate sections with commas', 'knowledgebase' ),
- 'add_or_remove_items' => __( 'Add or remove sections', 'knowledgebase' ),
+ /**
+ * Get full taxonomy labels.
+ *
+ * @since 2.5.0
+ *
+ * @param string $singular Singular name.
+ * @param string $plural Plural name.
+ *
+ * @return array Taxonomy labels.
+ */
+ private static function get_taxonomy_labels( string $singular, string $plural ): array {
+ $labels = array(
+ /* translators: 1: Plural taxonomy name. */
+ 'name' => sprintf( _x( 'Knowledge Base %1$s', 'Taxonomy General Name', 'knowledgebase' ), $plural ),
+ /* translators: 1: Singular taxonomy name. */
+ 'singular_name' => sprintf( _x( 'Knowledge Base %1$s', 'Taxonomy Singular Name', 'knowledgebase' ), $singular ),
+ /* translators: 1: Plural taxonomy name. */
+ 'menu_name' => $plural,
+ /* translators: 1: Plural taxonomy name. */
+ 'all_items' => sprintf( __( 'All %1$s', 'knowledgebase' ), $plural ),
+ /* translators: 1: Singular taxonomy name. */
+ 'parent_item' => sprintf( __( 'Parent %1$s', 'knowledgebase' ), $singular ),
+ /* translators: 1: Singular taxonomy name. */
+ 'parent_item_colon' => sprintf( __( 'Parent %1$s:', 'knowledgebase' ), $singular ),
+ /* translators: 1: Singular taxonomy name. */
+ 'new_item_name' => sprintf( __( 'New %1$s Name', 'knowledgebase' ), $singular ),
+ /* translators: 1: Singular taxonomy name. */
+ 'add_new_item' => sprintf( __( 'Add New %1$s', 'knowledgebase' ), $singular ),
+ /* translators: 1: Singular taxonomy name. */
+ 'edit_item' => sprintf( __( 'Edit %1$s', 'knowledgebase' ), $singular ),
+ /* translators: 1: Singular taxonomy name. */
+ 'update_item' => sprintf( __( 'Update %1$s', 'knowledgebase' ), $singular ),
+ /* translators: 1: Singular taxonomy name. */
+ 'view_item' => sprintf( __( 'View %1$s', 'knowledgebase' ), $singular ),
+ /* translators: 1: Plural taxonomy name. */
+ 'separate_items_with_commas' => sprintf( __( 'Separate %1$s with commas', 'knowledgebase' ), $plural ),
+ /* translators: 1: Plural taxonomy name. */
+ 'add_or_remove_items' => sprintf( __( 'Add or remove %1$s', 'knowledgebase' ), $plural ),
'choose_from_most_used' => __( 'Choose from the most used', 'knowledgebase' ),
- 'popular_items' => __( 'Popular Sections', 'knowledgebase' ),
- 'search_items' => __( 'Search Sections', 'knowledgebase' ),
+ /* translators: 1: Plural taxonomy name. */
+ 'popular_items' => sprintf( __( 'Popular %1$s', 'knowledgebase' ), $plural ),
+ /* translators: 1: Plural taxonomy name. */
+ 'search_items' => sprintf( __( 'Search %1$s', 'knowledgebase' ), $plural ),
'not_found' => __( 'Not Found', 'knowledgebase' ),
- 'no_terms' => __( 'No sections', 'knowledgebase' ),
- 'items_list' => __( 'Sections list', 'knowledgebase' ),
- 'items_list_navigation' => __( 'Sections list navigation', 'knowledgebase' ),
+ /* translators: 1: Plural taxonomy name. */
+ 'no_terms' => sprintf( __( 'No %1$s found', 'knowledgebase' ), $plural ),
+ /* translators: 1: Plural taxonomy name. */
+ 'items_list' => sprintf( __( '%1$s list', 'knowledgebase' ), $plural ),
+ /* translators: 1: Plural taxonomy name. */
+ 'items_list_navigation' => sprintf( __( '%1$s list navigation', 'knowledgebase' ), $plural ),
+ /* translators: 1: Plural taxonomy name. */
+ 'back_to_items' => sprintf( __( 'Back to %1$s', 'knowledgebase' ), $plural ),
);
+ return $labels;
+ }
+
+ /**
+ * Register Knowledgebase Custom Taxonomies.
+ *
+ * @since 2.3.0
+ */
+ public static function register_taxonomies() {
+ // Get taxonomy slugs from options.
+ $catslug = \wzkb_get_option( 'category_slug', 'kb/section' );
+ $tagslug = \wzkb_get_option( 'tag_slug', 'kb/tags' );
+ $productslug = \wzkb_get_option( 'product_slug', 'kb/products' );
+
+ // Register products taxonomy first.
+ $product_args = self::get_taxonomy_base_args( $productslug, false );
+ $product_args['labels'] = self::get_taxonomy_labels( 'Product', 'Products' );
+
/**
- * Filter the labels of the custom categories.
+ * Filter the arguments of the products taxonomy.
+ *
+ * @since 3.0.0
+ *
+ * @param array $product_args Product arguments
+ */
+ $product_args = apply_filters( 'wzkb_product_args', $product_args );
+
+ register_taxonomy( 'wzkb_product', array( 'wz_knowledgebase' ), $product_args );
+
+ // Register categories (sections) taxonomy.
+ $cat_args = self::get_taxonomy_base_args( $catslug, true );
+ $cat_args['labels'] = self::get_taxonomy_labels( 'Section', 'Sections' );
+
+ /**
+ * Filter the arguments of the custom categories.
*
* @since 1.2.0
*
- * @param array $catlabels Category labels
+ * @param array $cat_args Category arguments
*/
- $args['labels'] = apply_filters( 'wzkb_cat_labels', $catlabels );
-
- register_taxonomy(
- 'wzkb_category',
- array( 'wz_knowledgebase' ),
- /**
- * Filter the arguments of the custom categories.
- *
- * @since 1.2.0
- *
- * @param array $catlabels Category labels
- */
- apply_filters( 'wzkb_cat_args', $args )
- );
+ $cat_args = apply_filters( 'wzkb_cat_args', $cat_args );
- // Now register tags for the Knowledge Base.
- $taglabels = array(
- 'name' => _x( 'Tags', 'Taxonomy General Name', 'knowledgebase' ),
- 'singular_name' => _x( 'Tag', 'Taxonomy Singular Name', 'knowledgebase' ),
- 'menu_name' => __( 'Tags', 'knowledgebase' ),
- );
+ register_taxonomy( 'wzkb_category', array( 'wz_knowledgebase' ), $cat_args );
+
+ // Register tags taxonomy.
+ $tag_args = self::get_taxonomy_base_args( $tagslug, false );
+ $tag_args['labels'] = self::get_taxonomy_labels( 'Tag', 'Tags' );
/**
- * Filter the labels of the custom tags.
+ * Filter the arguments of the custom tags.
*
* @since 1.2.0
*
- * @param array $taglabels Tags labels
+ * @param array $tag_args Tag arguments
*/
- $args['labels'] = apply_filters( 'wzkb_tag_labels', $taglabels );
-
- $args['hierarchical'] = false;
- $args['show_tagcloud'] = true;
- $args['rewrite']['slug'] = $tagslug;
-
- register_taxonomy(
- 'wzkb_tag',
- array( 'wz_knowledgebase' ),
- /**
- * Filter the arguments of the custom tags.
- *
- * @since 1.2.0
- *
- * @param array $args Tag arguments
- */
- apply_filters( 'wzkb_tag_args', $args )
- );
+ $tag_args = apply_filters( 'wzkb_tag_args', $tag_args );
+
+ register_taxonomy( 'wzkb_tag', array( 'wz_knowledgebase' ), $tag_args );
}
}
diff --git a/includes/class-main.php b/includes/class-main.php
index c534f45..9f7f674 100644
--- a/includes/class-main.php
+++ b/includes/class-main.php
@@ -7,7 +7,8 @@
namespace WebberZone\Knowledge_Base;
-use WebberZone\Knowledge_Base\Admin\Activator;
+use WebberZone\Knowledge_Base\Admin\Admin;
+use WebberZone\Knowledge_Base\Pro\Pro;
if ( ! defined( 'WPINC' ) ) {
exit;
@@ -31,99 +32,108 @@ final class Main {
*
* @since 2.3.0
*
- * @var object Admin.
+ * @var Admin|null Admin instance.
*/
- public $admin;
+ public ?Admin $admin = null;
+
+ /**
+ * Pro features class.
+ *
+ * @since 3.0.0
+ *
+ * @var Pro|null Pro instance.
+ */
+ public ?Pro $pro = null;
/**
* Shortcodes.
*
* @since 2.3.0
*
- * @var object Shortcodes.
+ * @var Frontend\Shortcodes Shortcodes handler.
*/
- public $shortcodes;
+ public Frontend\Shortcodes $shortcodes;
/**
* Styles.
*
* @since 2.3.0
*
- * @var object Styles.
+ * @var Frontend\Styles_Handler Styles handler.
*/
- public $styles;
+ public Frontend\Styles_Handler $styles;
/**
* Language Handler.
*
* @since 2.3.0
*
- * @var object Language Handler.
+ * @var Frontend\Language_Handler Language handler.
*/
- public $language;
+ public Frontend\Language_Handler $language;
/**
* Display.
*
* @since 2.3.0
*
- * @var object Display.
+ * @var Frontend\Display Display handler.
*/
- public $display;
+ public Frontend\Display $display;
/**
* Template Handler.
*
* @since 4.0.0
*
- * @var object Template Handler.
+ * @var Frontend\Template_Handler Template handler.
*/
- public $template_handler;
+ public Frontend\Template_Handler $template_handler;
/**
* CPT.
*
* @since 2.3.0
*
- * @var object CPT.
+ * @var CPT CPT handler.
*/
- public $cpt;
+ public CPT $cpt;
/**
* Feed.
*
* @since 2.3.0
*
- * @var object Feed.
+ * @var Frontend\Feed Feed handler.
*/
- public $feed;
+ public Frontend\Feed $feed;
/**
* Search.
*
* @since 2.3.0
*
- * @var object Search.
+ * @var Frontend\Search Search handler.
*/
- public $search;
+ public Frontend\Search $search;
/**
* Blocks.
*
* @since 2.3.0
*
- * @var object Blocks.
+ * @var Blocks\Blocks Blocks handler.
*/
- public $blocks;
+ public Blocks\Blocks $blocks;
/**
* Related articles.
*
* @since 2.3.0
*
- * @var object Related articles.
+ * @var Frontend\Related Related articles handler.
*/
- public $related_articles;
+ public Frontend\Related $related_articles;
/**
* Gets the instance of the class.
@@ -169,8 +179,13 @@ private function init() {
$this->hooks();
+ // Conditionally load Pro if available.
+ if ( class_exists( 'WebberZone\\Knowledge_Base\\Pro\\Pro' ) ) {
+ $this->pro = new Pro();
+ }
+
if ( is_admin() ) {
- $this->admin = new Admin\Admin();
+ $this->admin = new Admin();
}
}
diff --git a/includes/class-options-api.php b/includes/class-options-api.php
new file mode 100644
index 0000000..94177df
--- /dev/null
+++ b/includes/class-options-api.php
@@ -0,0 +1,312 @@
+cap->assign_terms ) ) {
+ wp_die();
+ }
+ }
+
+ $s = isset( $_REQUEST['q'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['q'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+
+ $comma = _x( ',', 'tag delimiter' );
+ if ( ',' !== $comma ) {
+ $s = str_replace( $comma, ',', $s );
+ }
+ if ( false !== strpos( $s, ',' ) ) {
+ $s = explode( ',', $s );
+ $s = $s[ count( $s ) - 1 ];
+ }
+ $s = trim( $s );
+
+ /** This filter has been defined in /wp-admin/includes/ajax-actions.php */
+ $term_search_min_chars = (int) apply_filters( 'term_search_min_chars', 2, $tax, $s );
+
+ /*
+ * Require $term_search_min_chars chars for matching (default: 2)
+ * ensure it's a non-negative, non-zero integer.
+ */
+ if ( ( 0 === $term_search_min_chars ) || ( strlen( $s ) < $term_search_min_chars ) ) {
+ wp_die();
+ }
+
+ $results = get_terms(
+ array(
+ 'taxonomy' => $taxonomy,
+ 'name__like' => $s,
+ 'fields' => 'names',
+ 'hide_empty' => false,
+ )
+ );
+
+ echo wp_json_encode( $results );
+ wp_die();
+ }
+}
+
+// Initialize hooks.
+Options_API::init();
diff --git a/includes/frontend/class-display.php b/includes/frontend/class-display.php
index 08926ab..573c76b 100644
--- a/includes/frontend/class-display.php
+++ b/includes/frontend/class-display.php
@@ -38,6 +38,7 @@ public function __construct() {
* Optional. Array of parameters.
*
* @type int $category Create a knowledge base for this category.
+ * @type int $product Create a knowledge base for this product.
* @type bool $is_shortcode Is this created using the shortcode?
* @type bool $is_block Is this created using the block?
* @type string $extra_class Space separated list of classes for the wrapping `div`.
@@ -52,6 +53,7 @@ public function __construct() {
public static function get_knowledge_base( $args = array() ) {
$defaults = array(
'category' => 0,
+ 'product' => 0,
'is_shortcode' => false,
'is_block' => false,
'extra_class' => '',
@@ -70,42 +72,61 @@ public static function get_knowledge_base( $args = array() ) {
$args['columns'] = ( ! empty( absint( $args['columns'] ) ) ) ? absint( $args['columns'] ) : \wzkb_get_option( 'columns' );
// Set default classes.
- $classes = array();
- $classes[] = $args['extra_class'];
- $classes[] = $args['is_shortcode'] ? 'wzkb_shortcode' : '';
- $classes[] = $args['is_block'] ? 'wzkb_block' : '';
- $div_classes = implode( ' ', $classes );
-
- /**
- * Filter the classes added to the div wrapper of the Knowledge Base.
- *
- * @since 2.0.0
- *
- * @param string $div_classes String with the classes of the div wrapper.
- */
- $div_classes = apply_filters( 'wzkb_div_class', $div_classes );
+ $div_classes = self::build_wrapper_classes( $args );
$output = '
';
- // Are we trying to display a category?
- $category = intval( $args['category'] );
+ $product = intval( $args['product'] );
+ $category = intval( $args['category'] );
+ $is_multi_product = \wzkb_get_option( 'multi_product' );
- // If $category = -1, then get the current term object and set the category to the term ID.
- if ( -1 === $category ) {
- $term = get_queried_object();
- if ( isset( $term->term_id ) ) {
- $category = $term->term_id;
+ // Harmonize: If product = -1, auto-detect current product term (like category does).
+ if ( -1 === $product ) {
+ $queried_object = get_queried_object();
+ if ( isset( $queried_object->term_id ) && isset( $queried_object->taxonomy ) && 'wzkb_product' === $queried_object->taxonomy ) {
+ $product = intval( $queried_object->term_id );
}
}
- $level = $category > 0 ? 1 : 0;
- $term_id = $category > 0 ? $category : 0;
- $nested_wrapper = isset( $args['nested_wrapper'] ) ? $args['nested_wrapper'] : true;
-
- $output .= self::get_knowledge_base_loop( $term_id, $level, $nested_wrapper, $args );
+ if ( $is_multi_product ) {
+ if ( $product > 0 ) {
+ // Product-specific view in multi-product mode.
+ $output .= self::render_product_sections( $product, $args );
+ } elseif ( 0 === $product && 0 === $category ) {
+ // Products archive view in multi-product mode.
+ $products = self::fetch_terms( 'wzkb_product' );
+ if ( ! empty( $products ) && ! is_wp_error( $products ) ) {
+ foreach ( $products as $product_term ) {
+ $output .= '
';
+ // Display product title as clickable if clickable_section is enabled.
+ $output .= '
';
+ if ( $args['clickable_section'] ) {
+ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_term_link is properly escaped below.
+ $output .= '' . esc_html( $product_term->name ) . ' ';
+ } else {
+ $output .= esc_html( $product_term->name );
+ }
+ $output .= ' ';
+ if ( $product_term->description ) {
+ $output .= '
' . esc_html( $product_term->description ) . '
';
+ }
+ $output .= self::render_product_sections( $product_term->term_id, $args );
+ $output .= '
';
+ }
+ } else {
+ $output .= '
' . esc_html__( 'No products found.', 'knowledgebase' ) . '
';
+ }
+ } else {
+ // Fallback to category view even in multi-product mode.
+ $output .= self::render_category_view( $category, $args );
+ }
+ } else {
+ // Single product mode (default): Simple category-based structure.
+ $output .= self::render_category_view( $category, $args );
+ }
- $output .= '
'; // End wzkb_section.
- $output .= '
';
+ $output .= '
';
+ $output .= '
';
/**
* Filter the formatted output.
@@ -118,6 +139,80 @@ public static function get_knowledge_base( $args = array() ) {
return apply_filters( 'wzkb_knowledge', $output, $args );
}
+ /**
+ * Generic method to fetch terms with optional meta query filters.
+ *
+ * @param string $taxonomy Taxonomy to query.
+ * @param array $query_args Base arguments for get_terms().
+ * @param array $meta_query Optional meta query array.
+ * @return array|\WP_Error Array of terms or WP_Error on failure.
+ */
+ public static function fetch_terms( $taxonomy, $query_args = array(), $meta_query = array() ) {
+ $default_args = array(
+ 'taxonomy' => $taxonomy,
+ 'hide_empty' => false,
+ 'orderby' => 'slug',
+ 'order' => 'ASC',
+ );
+ $args = wp_parse_args( $query_args, $default_args );
+
+ if ( ! empty( $meta_query ) ) {
+ $args['meta_query'] = $meta_query; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
+ }
+
+ return get_terms( $args );
+ }
+
+ /**
+ * Render all top-level sections for a given product term ID.
+ *
+ * @param int $product_id Product term ID.
+ * @param array $args Arguments for display.
+ * @return string Rendered HTML for sections.
+ */
+ public static function render_product_sections( $product_id, $args ) {
+ $product = get_term( $product_id, 'wzkb_product' );
+ if ( is_wp_error( $product ) || ! $product ) {
+ return '' . esc_html__( 'Invalid product ID.', 'knowledgebase' ) . '
';
+ }
+
+ $sections = self::fetch_terms(
+ 'wzkb_category',
+ array(
+ 'parent' => 0,
+ 'hide_empty' => $args['show_empty_sections'] ? 0 : 1,
+ ),
+ array(
+ array(
+ 'key' => 'product_id',
+ 'value' => $product_id,
+ 'compare' => '=',
+ ),
+ )
+ );
+
+ $output = '';
+ if ( ! empty( $sections ) && ! is_wp_error( $sections ) ) {
+ // Add section wrapper if category_level is 1.
+ $category_level = (int) \wzkb_get_option( 'category_level' );
+ if ( 1 === $category_level ) {
+ $output .= '';
+ }
+
+ foreach ( $sections as $section ) {
+ $output .= self::get_knowledge_base_loop( $section->term_id, 1, true, $args );
+ }
+
+ if ( 1 === $category_level ) {
+ $output .= '
';
+ }
+ } else {
+ $output .= '' . esc_html__( 'No sections found for this product.', 'knowledgebase' ) . '
';
+ }
+
+ return $output;
+ }
+
/**
* Creates the knowledge base loop.
*
@@ -130,44 +225,64 @@ public static function get_knowledge_base( $args = array() ) {
* @return string Formatted output.
*/
public static function get_knowledge_base_loop( $term_id, $level, $nested = true, $args = array() ) {
- $divclasses = array( 'wzkb_section', 'wzkb-section-level-' . $level );
- $category_level = (int) \wzkb_get_option( 'category_level' );
+ // Special handling for root level (term_id = 0) in single product mode.
+ if ( 0 === $term_id && 0 === $level ) {
+ $output = '';
- if ( ( $category_level - 1 ) === $level ) {
- $divclasses[] = 'section group';
- } elseif ( $category_level === $level ) {
- $divclasses[] = 'col span_1_of_' . $args['columns'];
- }
+ // Get top-level sections.
+ $sections = self::fetch_terms(
+ 'wzkb_category',
+ array(
+ 'parent' => 0,
+ 'hide_empty' => $args['show_empty_sections'] ? 0 : 1,
+ )
+ );
- /**
- * Filter to add more classes if needed.
- *
- * @since 1.1.0
- *
- * @param array $divclasses Current array of classes.
- * @param int $level Level of the loop.
- * @param int $term_id Term ID.
- */
- $divclasses = apply_filters( 'wzkb_loop_div_class', $divclasses, $level, $term_id );
+ if ( ! empty( $sections ) && ! is_wp_error( $sections ) ) {
+ // Add section wrapper if category_level is 2.
+ $category_level = (int) \wzkb_get_option( 'category_level' );
+ if ( 2 === $category_level ) {
+ $output .= '';
+ }
- $output = '
';
+ foreach ( $sections as $section ) {
+ $output .= self::get_knowledge_base_loop( $section->term_id, 1, true, $args );
+ }
- $term = get_term( $term_id, 'wzkb_category' );
+ if ( 2 === $category_level ) {
+ $output .= '
';
+ }
+ }
+
+ return $output;
+ }
- if ( ! empty( $term ) && ! is_wp_error( $term ) ) {
- $output .= self::get_article_header( $term, $level, $args );
- $output .= self::get_posts_by_term( $term, $level, $args );
- } elseif ( $level > 0 ) {
- /* translators: Section ID. */
+ $term = get_term( $term_id, 'wzkb_category' );
+ if ( is_wp_error( $term ) || ! $term ) {
+ /* translators: %s: Term ID */
return sprintf( __( '%s is not enter a valid section ID', 'knowledgebase' ), $term_id );
}
- $output .= '
';
+ $category_level = (int) \wzkb_get_option( 'category_level' );
+ $divclasses = array( 'wzkb-section', 'wzkb-section-level-' . $level );
+
+ if ( 1 === $category_level && 1 === $level ) {
+ $divclasses[] = 'col span_1_of_' . $args['columns'];
+ } elseif ( 2 === $category_level && 2 === $level ) {
+ $divclasses[] = 'col span_1_of_' . $args['columns'];
+ }
+
+ $output = '
';
+
+ $output .= self::get_article_header( $term, $level, $args );
+ $output .= self::get_posts_by_term( $term, $level, $args );
+
+ $output .= '
';
// Get Knowledge Base Sections.
- $sections = get_terms(
+ $sections = self::fetch_terms(
+ 'wzkb_category',
array(
- 'taxonomy' => 'wzkb_category',
'orderby' => 'slug',
'hide_empty' => $args['show_empty_sections'] ? 0 : 1,
'parent' => $term_id,
@@ -175,8 +290,8 @@ public static function get_knowledge_base_loop( $term_id, $level, $nested = true
);
if ( ! $nested ) {
- $output .= '
'; // End wzkb_section_wrapper.
- $output .= '
'; // End wzkb_section.
+ $output .= '
'; // End wzkb-section-wrapper.
+ $output .= '
'; // End wzkb-section.
}
if ( ! empty( $sections ) && ! is_wp_error( $sections ) ) {
@@ -188,8 +303,8 @@ public static function get_knowledge_base_loop( $term_id, $level, $nested = true
}
if ( $nested ) {
- $output .= ''; // End wzkb_section_wrapper.
- $output .= ''; // End wzkb_section.
+ $output .= ''; // End wzkb-section-wrapper.
+ $output .= ''; // End wzkb-section.
}
return $output;
@@ -279,7 +394,7 @@ public static function get_posts_by_term( $term, $level, $args = array() ) {
* @return string Formatted header output.
*/
public static function get_article_header( $term, $level, $args = array() ) {
- $output = '';
+ $output = '';
if ( $args['clickable_section'] ) {
$output .= '' . $term->name . ' ';
@@ -288,7 +403,7 @@ public static function get_article_header( $term, $level, $args = array() ) {
}
if ( $level >= (int) \wzkb_get_option( 'category_level' ) && $args['show_article_count'] ) {
- $output .= ' ' . $term->count . '
';
+ $output .= '' . $term->count . '
';
}
$output .= ' ';
@@ -381,9 +496,9 @@ public static function get_categories_list( $term_id, $level = 0, $args = array(
$args = wp_parse_args( $args, $defaults );
// Get Knowledge Base Sections.
- $sections = get_terms(
+ $sections = self::fetch_terms(
+ 'wzkb_category',
array(
- 'taxonomy' => 'wzkb_category',
'orderby' => 'slug',
'hide_empty' => \wzkb_get_option( 'show_empty_sections' ) ? 0 : 1,
'parent' => $term_id,
@@ -420,4 +535,49 @@ public static function get_categories_list( $term_id, $level = 0, $args = array(
return $output;
}
+
+ /**
+ * Render the category-based view of the knowledge base.
+ *
+ * @param int $category Category ID to display, -1 for current, 0 for all.
+ * @param array $args Display arguments.
+ * @return string HTML output.
+ */
+ private static function render_category_view( $category, $args ) {
+ if ( -1 === $category ) {
+ $term = get_queried_object();
+ if ( isset( $term->term_id ) ) {
+ $category = $term->term_id;
+ }
+ }
+
+ $level = $category > 0 ? 1 : 0;
+ $term_id = $category > 0 ? $category : 0;
+ $nested_wrapper = isset( $args['nested_wrapper'] ) ? $args['nested_wrapper'] : true;
+
+ return self::get_knowledge_base_loop( $term_id, $level, $nested_wrapper, $args );
+ }
+
+ /**
+ * Build CSS classes for a wrapper based on arguments.
+ *
+ * @since 3.0.0
+ *
+ * @param array $args Arguments containing class-related flags.
+ * @return string Space-separated string of CSS classes.
+ */
+ private static function build_wrapper_classes( $args ) {
+ $classes = array();
+ $classes[] = $args['extra_class'];
+ $classes[] = $args['is_shortcode'] ? 'wzkb-shortcode' : '';
+ $classes[] = $args['is_block'] ? 'wzkb-block' : '';
+ /**
+ * Filter the classes added to the div wrapper of the Knowledge Base.
+ *
+ * @since 2.0.0
+ *
+ * @param string $div_classes String with the classes of the div wrapper.
+ */
+ return apply_filters( 'wzkb_div_class', implode( ' ', $classes ) );
+ }
}
diff --git a/includes/frontend/class-shortcodes.php b/includes/frontend/class-shortcodes.php
index 03c63c1..83e4b00 100644
--- a/includes/frontend/class-shortcodes.php
+++ b/includes/frontend/class-shortcodes.php
@@ -48,6 +48,7 @@ public static function knowledgebase( $atts, $content = null ) {
$atts = shortcode_atts(
array(
'category' => false,
+ 'product' => false,
'is_shortcode' => 1,
),
$atts,
diff --git a/includes/frontend/class-template-handler.php b/includes/frontend/class-template-handler.php
index 256bcf7..6b03342 100644
--- a/includes/frontend/class-template-handler.php
+++ b/includes/frontend/class-template-handler.php
@@ -64,6 +64,8 @@ public function archive_template( $template ) {
$template_name = is_search() ? 'wzkb-search.php' : 'archive-wz_knowledgebase.php';
} elseif ( is_tax( 'wzkb_category' ) && ! is_search() ) {
$template_name = 'taxonomy-wzkb_category.php';
+ } elseif ( is_tax( 'wzkb_product' ) && ! is_search() ) {
+ $template_name = 'taxonomy-wzkb_product.php';
}
if ( $template_name ) {
@@ -112,6 +114,9 @@ public function add_custom_archive_template( $templates ) {
if ( is_tax( 'wzkb_category' ) ) {
return $this->add_custom_template( $templates, 'archive', 'wzkb_category', 'taxonomy-wzkb_category' );
}
+ if ( is_tax( 'wzkb_product' ) ) {
+ return $this->add_custom_template( $templates, 'archive', 'wzkb_product', 'taxonomy-wzkb_product' );
+ }
if ( is_singular( 'wz_knowledgebase' ) ) {
return $this->add_custom_template( $templates, 'single', 'wz_knowledgebase', 'single-wz_knowledgebase' );
}
@@ -190,6 +195,8 @@ public function manage_block_templates( $query_result, $query, $template_type )
$template_name = is_search() ? 'wzkb-search' : 'archive-wz_knowledgebase';
} elseif ( is_tax( 'wzkb_category' ) && ! is_search() ) {
$template_name = 'taxonomy-wzkb_category';
+ } elseif ( is_tax( 'wzkb_product' ) && ! is_search() ) {
+ $template_name = 'taxonomy-wzkb_product';
}
if ( $template_name ) {
@@ -200,24 +207,34 @@ public function manage_block_templates( $query_result, $query, $template_type )
$template_file_path = __DIR__ . '/templates/' . $template_name . '.html';
}
- $template_contents = self::get_template_content( $template_file_path );
- $template_contents = self::replace_placeholders_with_shortcodes( $template_contents );
-
- $new_block = new \WP_Block_Template();
- $new_block->type = 'wp_template';
- $new_block->theme = $theme->stylesheet;
- $new_block->slug = $template_name;
- $new_block->id = 'wzkb//' . $template_name;
- $new_block->title = 'Knowledge Base Template - ' . $template_name;
- $new_block->description = '';
- $new_block->source = $block_source;
- $new_block->status = 'publish';
- $new_block->has_theme_file = true;
- $new_block->is_custom = true;
- $new_block->content = $template_contents;
- $new_block->post_types = array( 'wz_knowledgebase' );
-
- $query_result[] = $new_block;
+ if ( file_exists( $template_file_path ) ) {
+ $template_contents = self::get_template_content( $template_file_path );
+ $template_contents = self::replace_placeholders_with_shortcodes( $template_contents );
+
+ $new_block = new \WP_Block_Template();
+ $new_block->type = 'wp_template';
+ $new_block->theme = $theme->stylesheet;
+ $new_block->slug = $template_name;
+ $new_block->id = 'wzkb//' . $template_name;
+ $new_block->title = 'Knowledge Base Template - ' . $template_name;
+ $new_block->description = '';
+ $new_block->source = $block_source;
+ $new_block->status = 'publish';
+ $new_block->has_theme_file = true;
+ $new_block->is_custom = true;
+ $new_block->content = $template_contents;
+ $new_block->post_types = array( 'wz_knowledgebase' );
+
+ // Add taxonomy support for block template.
+ if ( 'taxonomy-wzkb_category' === $template_name ) {
+ $new_block->description = 'Knowledge Base Section (taxonomy) block template.';
+ }
+ if ( 'taxonomy-wzkb_product' === $template_name ) {
+ $new_block->description = 'Knowledge Base Product (taxonomy) block template.';
+ }
+
+ $query_result[] = $new_block;
+ }
}
return $query_result;
diff --git a/includes/frontend/css/wzkb-styles-rtl.css b/includes/frontend/css/wzkb-styles-rtl.css
index 66db175..7493f07 100644
--- a/includes/frontend/css/wzkb-styles-rtl.css
+++ b/includes/frontend/css/wzkb-styles-rtl.css
@@ -3,7 +3,7 @@
margin: 10px 0;
}
-.wzkb:after, .wzkb_master_section:after {
+.wzkb:after, .wzkb-master-section:after {
content: "";
display: table;
clear: both;
@@ -15,7 +15,7 @@
border: 0;
}
-.wzkb h3.wzkb_section_name {
+.wzkb h3.wzkb-section-name {
font-size: 20px;
}
@@ -48,162 +48,106 @@
}
.wzkb-articles-list li:before {
- content: "\f123";
+ content: "\f139";
display: inline-block;
width: 20px;
height: 20px;
- font-size: 20px;
- line-height: 1;
font-family: dashicons;
text-decoration: inherit;
font-weight: 400;
font-style: normal;
vertical-align: top;
text-align: center;
- -webkit-transition: color .1s ease-in 0;
- transition: color .1s ease-in 0;
+ transition: color .1s ease-in;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
- margin-left: 10px;
- word-wrap: break-word;
- color: #666;
+ margin-right: 10px;
}
-.wzkb_section_count {
- display: block;
- float: left;
- margin: 0;
- padding: 2px;
- border: 0;
- background: orange;
- border-radius: 45%;
- text-align: center;
- font-size: 70%;
- min-width: 36px;
+.wzkb-section-wrapper {
+ margin-left: 15px;
}
-/* Set up the responsive grid based on http://www.responsivegridsystem.com/ /*
-/* SECTIONS */
-.section {
+.wzkb-section {
+ margin: 0 0 10px;
+ width: 100%;
+ float: left;
clear: both;
- padding: 0;
- margin: 0;
}
-/* COLUMN SETUP */
-.col {
- display: block;
+.wzkb-section-count {
+ color: #666;
+ font-size: 0.8em;
float: right;
- margin: 1% 1.6% 1% 0;
-}
-
-.col:first-child {
- margin-right: 0;
+ padding: 5px;
}
-/* GROUPING */
-.group:before, .group:after {
- content: "";
- display: table;
-}
-
-.group:after {
- clear: both;
-}
-
-.group {
- zoom: 1;
-}
-
-/* GRID OF TWO */
-.wzkb_section.span_2_of_2 {
- width: 100%;
-}
-
-.wzkb_section.span_1_of_2 {
- width: 49.2%;
-}
-
-.wzkb_section.span_1_of_2:nth-child(2n+1) {
- clear: right;
- margin-right: 0;
+.wzkb-breadcrumbs {
+ font-size: 0.8em;
+ color: #666;
+ margin-bottom: 15px;
}
-/* GRID OF THREE */
-.wzkb_section.span_3_of_3 {
- width: 100%;
+.wzkb-breadcrumbs a {
+ color: #666;
+ text-decoration: none;
+ border: none !important;
}
-.wzkb_section.span_2_of_3 {
- width: 66.13%;
+.wzkb-breadcrumbs a:hover {
+ color: #333;
}
-.wzkb_section.span_1_of_3 {
+/* Grid */
+.span_1_of_3 {
width: 32.26%;
}
-.wzkb_section.span_1_of_3:nth-child(3n+1) {
- clear: right;
- margin-right: 0;
+.span_2_of_3 {
+ width: 66.13%;
}
-/* GRID OF FOUR */
-.wzkb_section.span_4_of_4 {
+.span_3_of_3 {
width: 100%;
}
-.wzkb_section.span_3_of_4 {
- width: 74.6%;
-}
-
-.wzkb_section.span_2_of_4 {
- width: 49.2%;
-}
-
-.wzkb_section.span_1_of_4 {
- width: 23.8%;
-}
-
-.wzkb_section.span_1_of_4:nth-child(4n+1) {
- clear: right;
- margin-right: 0;
+.col {
+ display: block;
+ float: left;
+ margin: 1% 1.6% 1% 0;
}
-/* GRID OF FIVE */
-.wzkb_section.span_5_of_5 {
- width: 100%;
+.col:first-child {
+ margin-left: 0;
}
-.wzkb_section.span_4_of_5 {
- width: 79.68%;
-}
+/* Grid Responsive */
+@media only screen and (max-width: 480px) {
+ .col {
+ margin: 1% 0 1% 0;
+ }
-.wzkb_section.span_3_of_5 {
- width: 59.36%;
+ .span_3_of_3, .span_2_of_3, .span_1_of_3 {
+ width: 100%;
+ }
}
-.wzkb_section.span_2_of_5 {
- width: 39.04%;
+/* Block styles */
+.wzkb-block {
+ margin-bottom: 2em;
}
-.wzkb_section.span_1_of_5 {
- width: 18.72%;
+.wzkb-block .wzkb-section-name {
+ margin-top: 0;
}
-.wzkb_section.span_1_of_5:nth-child(5n+1) {
- clear: right;
- margin-right: 0;
+/* Shortcode styles */
+.wzkb-shortcode {
+ margin-bottom: 2em;
}
-/* GO FULL WIDTH AT LESS THAN 480 PIXELS */
-@media only screen and (max-width: 480px) {
- .col {
- margin: 1% 0;
- }
-
- .span_2_of_2, .span_1_of_2, .span_1_of_3, .span_2_of_3, .span_3_of_3, .span_1_of_4, .span_2_of_4, .span_3_of_4, .span_4_of_4, .span_1_of_5, .span_2_of_5, .span_3_of_5, .span_4_of_5, .span_5_of_5 {
- width: 100%;
- }
+.wzkb-shortcode .wzkb-section-name {
+ margin-top: 0;
}
/* Archive styles */
@@ -214,8 +158,8 @@
}
#wzkb-sidebar-primary {
- float: left;
- margin-right: -100%;
+ float: right;
+ margin-left: -100%;
max-width: 413px;
position: relative;
}
@@ -225,7 +169,7 @@
#wzkb-sidebar-primary, #wzkb-content-primary {
float: none !important;
width: 98% !important;
- margin-right: 0;
+ margin-left: 0;
}
}
@@ -235,27 +179,27 @@
}
#wzkb-sidebar-primary .widget ul {
- margin-right: 10px;
+ margin-left: 10px;
}
#wzkb-sidebar-primary .widget ul li {
- margin-right: 10px;
+ margin-left: 10px;
list-style-type: disc;
}
.nav-previous {
- float: right;
+ float: left;
}
.nav-next {
- float: left;
+ float: right;
}
.wzkb-search-form {
border: #ddd 1px solid;
- padding-right: 10px;
+ padding-left: 10px;
position: relative;
- padding-left: 12px;
+ padding-right: 12px;
margin: 20px 0;
font-size: 1.3em;
width: 95%;
@@ -270,9 +214,9 @@ input[type="search"].wzkb-search-field {
color: #555;
font-family: 'Open Sans', sans-serif;
font-size: 1em;
- padding: 10px 45px 10px 6px;
+ padding: 10px 6px 10px 45px;
width: 100%;
- background: url(../images/search-icon.png) no-repeat 100% 5px;
+ background: url(../images/search-icon.png) no-repeat 0 5px;
outline: none;
}
@@ -312,13 +256,13 @@ nav.pagination {
}
.wzkb-alert-dismissible {
- padding-left: 4rem;
+ padding-right: 4rem;
}
.wzkb-alert-dismissible .close {
position: absolute;
top: 0;
- left: 0;
+ right: 0;
padding: 0.75rem 1.25rem;
color: inherit;
}
diff --git a/includes/frontend/css/wzkb-styles-rtl.min.css b/includes/frontend/css/wzkb-styles-rtl.min.css
index f4a4f1c..c7b26e5 100644
--- a/includes/frontend/css/wzkb-styles-rtl.min.css
+++ b/includes/frontend/css/wzkb-styles-rtl.min.css
@@ -1 +1 @@
-.wzkb{width:100%;margin:10px 0;}.wzkb:after,.wzkb_master_section:after{content:"";display:table;clear:both;}.wzkb h3 a,.wzkb h4 a,.wzkb h3 a:visited,.wzkb h4 a:visited{color:#333;text-decoration:none;border:0;}.wzkb h3.wzkb_section_name{font-size:20px;}.wzkb-section-name-level-1{border-bottom:1px black dotted;border-top:1px black dotted;padding:5px 1px;background-color:ghostwhite;margin-top:30px;margin-bottom:0;}.wzkb h3.wzkb-section-name-level-0,.wzkb h3.wzkb-section-name-level-1{font-size:28px;}.wzkb h3.wzkb-section-name-level-2{border-bottom:1px solid #666;font-size:24px;}.wzkb-articles-list{margin:0!important;padding:0!important;}.wzkb-articles-list li{margin:5px 0;list-style-type:none!important;}.wzkb-articles-list li:before{content:"\f123";display:inline-block;width:20px;height:20px;font-size:20px;line-height:1;font-family:dashicons;text-decoration:inherit;font-weight:400;font-style:normal;vertical-align:top;text-align:center;-webkit-transition:color .1s ease-in 0;transition:color .1s ease-in 0;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;margin-left:10px;word-wrap:break-word;color:#666;}.wzkb_section_count{display:block;float:left;margin:0;padding:2px;border:0;background:orange;border-radius:45%;text-align:center;font-size:70%;min-width:36px;}.section{clear:both;padding:0;margin:0;}.col{display:block;float:right;margin:1% 1.6% 1% 0;}.col:first-child{margin-right:0;}.group:before,.group:after{content:"";display:table;}.group:after{clear:both;}.group{zoom:1;}.wzkb_section.span_2_of_2{width:100%;}.wzkb_section.span_1_of_2{width:49.2%;}.wzkb_section.span_1_of_2:nth-child(2n+1){clear:right;margin-right:0;}.wzkb_section.span_3_of_3{width:100%;}.wzkb_section.span_2_of_3{width:66.13%;}.wzkb_section.span_1_of_3{width:32.26%;}.wzkb_section.span_1_of_3:nth-child(3n+1){clear:right;margin-right:0;}.wzkb_section.span_4_of_4{width:100%;}.wzkb_section.span_3_of_4{width:74.6%;}.wzkb_section.span_2_of_4{width:49.2%;}.wzkb_section.span_1_of_4{width:23.8%;}.wzkb_section.span_1_of_4:nth-child(4n+1){clear:right;margin-right:0;}.wzkb_section.span_5_of_5{width:100%;}.wzkb_section.span_4_of_5{width:79.68%;}.wzkb_section.span_3_of_5{width:59.36%;}.wzkb_section.span_2_of_5{width:39.04%;}.wzkb_section.span_1_of_5{width:18.72%;}.wzkb_section.span_1_of_5:nth-child(5n+1){clear:right;margin-right:0;}@media only screen and (max-width:480px){.col{margin:1% 0}.span_2_of_2,.span_1_of_2,.span_1_of_3,.span_2_of_3,.span_3_of_3,.span_1_of_4,.span_2_of_4,.span_3_of_4,.span_4_of_4,.span_1_of_5,.span_2_of_5,.span_3_of_5,.span_4_of_5,.span_5_of_5{width:100%}}#wzkb-content-primary{margin:0 auto;max-width:1366px;display:block;}#wzkb-sidebar-primary{float:left;margin-right:-100%;max-width:413px;position:relative;}@media only screen and (max-width:480px){#wzkb-sidebar-primary,#wzkb-content-primary{float:none!important;width:98%!important;margin-right:0}}#wzkb-sidebar-primary .widget{padding:20px;margin-top:10px;}#wzkb-sidebar-primary .widget ul{margin-right:10px;}#wzkb-sidebar-primary .widget ul li{margin-right:10px;list-style-type:disc;}.nav-previous{float:right;}.nav-next{float:left;}.wzkb-search-form{border:#ddd 1px solid;padding-right:10px;position:relative;padding-left:12px;margin:20px 0;font-size:1.3em;width:95%;}.wzkb-search-form label{width:100%;}input[type="search"].wzkb-search-field{border:none;color:#555;font-family:'Open Sans',sans-serif;font-size:1em;padding:10px 45px 10px 6px;width:100%;background:url(../images/search-icon.png) no-repeat 100% 5px;outline:none;}input[type="submit"].wzkb-search-submit{display:none;}.wzkb-search-form .screen-reader-text{position:absolute;clip:rect(0 0 0 0);}nav.pagination{width:100%;margin:15px 0;padding:15px 0;border-top:1px dotted #ccc;border-bottom:1px dotted #ccc;text-align:center;}.wzkb-alert{position:relative;padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem;}.wzkb-alert-heading{color:inherit;}.wzkb-alert-link{font-weight:700;}.wzkb-alert-dismissible{padding-left:4rem;}.wzkb-alert-dismissible .close{position:absolute;top:0;left:0;padding:.75rem 1.25rem;color:inherit;}.wzkb-alert-primary{color:#004085;background-color:#cce5ff;border-color:#b8daff;}.wzkb-alert-primary hr{border-top-color:#9fcdff;}.wzkb-alert-primary .wzkb-alert-link{color:#002752;}.wzkb-alert-secondary{color:#383d41;background-color:#e2e3e5;border-color:#d6d8db;}.wzkb-alert-secondary hr{border-top-color:#c8cbcf;}.wzkb-alert-secondary .wzkb-alert-link{color:#202326;}.wzkb-alert-success{color:#155724;background-color:#d4edda;border-color:#c3e6cb;}.wzkb-alert-success hr{border-top-color:#b1dfbb;}.wzkb-alert-success .wzkb-alert-link{color:#0b2e13;}.wzkb-alert-info{color:#0c5460;background-color:#d1ecf1;border-color:#bee5eb;}.wzkb-alert-info hr{border-top-color:#abdde5;}.wzkb-alert-info .wzkb-alert-link{color:#062c33;}.wzkb-alert-warning{color:#856404;background-color:#fff3cd;border-color:#ffeeba;}.wzkb-alert-warning hr{border-top-color:#ffe8a1;}.wzkb-alert-warning .wzkb-alert-link{color:#533f03;}.wzkb-alert-danger{color:#721c24;background-color:#f8d7da;border-color:#f5c6cb;}.wzkb-alert-danger hr{border-top-color:#f1b0b7;}.wzkb-alert-danger .wzkb-alert-link{color:#491217;}.wzkb-alert-light{color:#818182;background-color:#fefefe;border-color:#fdfdfe;}.wzkb-alert-light hr{border-top-color:#ececf6;}.wzkb-alert-light .wzkb-alert-link{color:#686868;}.wzkb-alert-dark{color:#1b1e21;background-color:#d6d8d9;border-color:#c6c8ca;}.wzkb-alert-dark hr{border-top-color:#b9bbbe;}.wzkb-alert-dark .wzkb-alert-link{color:#040505;}.wzkb-related-articles{clear:both;}.wzkb-related-articles ul{list-style-type:none;margin:0;display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));grid-column-gap:25px;column-gap:25px;}.wzkb-related-articles ul li{width:auto;}.wzkb-related-articles img,.wzkb-related-articles a{display:block;}
\ No newline at end of file
+.wzkb{width:100%;margin:10px 0;}.wzkb:after,.wzkb-master-section:after{content:"";display:table;clear:both;}.wzkb h3 a,.wzkb h4 a,.wzkb h3 a:visited,.wzkb h4 a:visited{color:#333;text-decoration:none;border:0;}.wzkb h3.wzkb-section-name{font-size:20px;}.wzkb-section-name-level-1{border-bottom:1px black dotted;border-top:1px black dotted;padding:5px 1px;background-color:ghostwhite;margin-top:30px;margin-bottom:0;}.wzkb h3.wzkb-section-name-level-0,.wzkb h3.wzkb-section-name-level-1{font-size:28px;}.wzkb h3.wzkb-section-name-level-2{border-bottom:1px solid #666;font-size:24px;}.wzkb-articles-list{margin:0!important;padding:0!important;}.wzkb-articles-list li{margin:5px 0;list-style-type:none!important;}.wzkb-articles-list li:before{content:"\f139";display:inline-block;width:20px;height:20px;font-family:dashicons;text-decoration:inherit;font-weight:400;font-style:normal;vertical-align:top;text-align:center;transition:color .1s ease-in;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;margin-right:10px;}.wzkb-section-wrapper{margin-left:15px;}.wzkb-section{margin:0 0 10px;width:100%;float:left;clear:both;}.wzkb-section-count{color:#666;font-size:.8em;float:right;padding:5px;}.wzkb-breadcrumbs{font-size:.8em;color:#666;margin-bottom:15px;}.wzkb-breadcrumbs a{color:#666;text-decoration:none;border:none!important;}.wzkb-breadcrumbs a:hover{color:#333;}.span_1_of_3{width:32.26%;}.span_2_of_3{width:66.13%;}.span_3_of_3{width:100%;}.col{display:block;float:left;margin:1% 1.6% 1% 0;}.col:first-child{margin-left:0;}@media only screen and (max-width:480px){.col{margin:1% 0 1% 0}.span_3_of_3,.span_2_of_3,.span_1_of_3{width:100%}}.wzkb-block{margin-bottom:2em;}.wzkb-block .wzkb-section-name{margin-top:0;}.wzkb-shortcode{margin-bottom:2em;}.wzkb-shortcode .wzkb-section-name{margin-top:0;}#wzkb-content-primary{margin:0 auto;max-width:1366px;display:block;}#wzkb-sidebar-primary{float:right;margin-left:-100%;max-width:413px;position:relative;}@media only screen and (max-width:480px){#wzkb-sidebar-primary,#wzkb-content-primary{float:none!important;width:98%!important;margin-left:0}}#wzkb-sidebar-primary .widget{padding:20px;margin-top:10px;}#wzkb-sidebar-primary .widget ul{margin-left:10px;}#wzkb-sidebar-primary .widget ul li{margin-left:10px;list-style-type:disc;}.nav-previous{float:left;}.nav-next{float:right;}.wzkb-search-form{border:#ddd 1px solid;padding-left:10px;position:relative;padding-right:12px;margin:20px 0;font-size:1.3em;width:95%;}.wzkb-search-form label{width:100%;}input[type="search"].wzkb-search-field{border:none;color:#555;font-family:'Open Sans',sans-serif;font-size:1em;padding:10px 6px 10px 45px;width:100%;background:url(../images/search-icon.png) no-repeat 0 5px;outline:none;}input[type="submit"].wzkb-search-submit{display:none;}.wzkb-search-form .screen-reader-text{position:absolute;clip:rect(0 0 0 0);}nav.pagination{width:100%;margin:15px 0;padding:15px 0;border-top:1px dotted #ccc;border-bottom:1px dotted #ccc;text-align:center;}.wzkb-alert{position:relative;padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem;}.wzkb-alert-heading{color:inherit;}.wzkb-alert-link{font-weight:700;}.wzkb-alert-dismissible{padding-right:4rem;}.wzkb-alert-dismissible .close{position:absolute;top:0;right:0;padding:.75rem 1.25rem;color:inherit;}.wzkb-alert-primary{color:#004085;background-color:#cce5ff;border-color:#b8daff;}.wzkb-alert-primary hr{border-top-color:#9fcdff;}.wzkb-alert-primary .wzkb-alert-link{color:#002752;}.wzkb-alert-secondary{color:#383d41;background-color:#e2e3e5;border-color:#d6d8db;}.wzkb-alert-secondary hr{border-top-color:#c8cbcf;}.wzkb-alert-secondary .wzkb-alert-link{color:#202326;}.wzkb-alert-success{color:#155724;background-color:#d4edda;border-color:#c3e6cb;}.wzkb-alert-success hr{border-top-color:#b1dfbb;}.wzkb-alert-success .wzkb-alert-link{color:#0b2e13;}.wzkb-alert-info{color:#0c5460;background-color:#d1ecf1;border-color:#bee5eb;}.wzkb-alert-info hr{border-top-color:#abdde5;}.wzkb-alert-info .wzkb-alert-link{color:#062c33;}.wzkb-alert-warning{color:#856404;background-color:#fff3cd;border-color:#ffeeba;}.wzkb-alert-warning hr{border-top-color:#ffe8a1;}.wzkb-alert-warning .wzkb-alert-link{color:#533f03;}.wzkb-alert-danger{color:#721c24;background-color:#f8d7da;border-color:#f5c6cb;}.wzkb-alert-danger hr{border-top-color:#f1b0b7;}.wzkb-alert-danger .wzkb-alert-link{color:#491217;}.wzkb-alert-light{color:#818182;background-color:#fefefe;border-color:#fdfdfe;}.wzkb-alert-light hr{border-top-color:#ececf6;}.wzkb-alert-light .wzkb-alert-link{color:#686868;}.wzkb-alert-dark{color:#1b1e21;background-color:#d6d8d9;border-color:#c6c8ca;}.wzkb-alert-dark hr{border-top-color:#b9bbbe;}.wzkb-alert-dark .wzkb-alert-link{color:#040505;}.wzkb-related-articles{clear:both;}.wzkb-related-articles ul{list-style-type:none;margin:0;display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));grid-column-gap:25px;column-gap:25px;}.wzkb-related-articles ul li{width:auto;}.wzkb-related-articles img,.wzkb-related-articles a{display:block;}
\ No newline at end of file
diff --git a/includes/frontend/css/wzkb-styles.css b/includes/frontend/css/wzkb-styles.css
index fd35664..3657e26 100644
--- a/includes/frontend/css/wzkb-styles.css
+++ b/includes/frontend/css/wzkb-styles.css
@@ -3,7 +3,7 @@
margin: 10px 0;
}
-.wzkb:after, .wzkb_master_section:after {
+.wzkb:after, .wzkb-master-section:after {
content: "";
display: table;
clear: both;
@@ -15,7 +15,7 @@
border: 0;
}
-.wzkb h3.wzkb_section_name {
+.wzkb h3.wzkb-section-name {
font-size: 20px;
}
@@ -69,7 +69,7 @@
color: #666;
}
-.wzkb_section_count {
+.wzkb-section-count {
display: block;
float: right;
margin: 0;
@@ -116,81 +116,81 @@
}
/* GRID OF TWO */
-.wzkb_section.span_2_of_2 {
+.wzkb-section.span_2_of_2 {
width: 100%;
}
-.wzkb_section.span_1_of_2 {
+.wzkb-section.span_1_of_2 {
width: 49.2%;
}
-.wzkb_section.span_1_of_2:nth-child(2n+1) {
+.wzkb-section.span_1_of_2:nth-child(2n+1) {
clear: left;
margin-left: 0;
}
/* GRID OF THREE */
-.wzkb_section.span_3_of_3 {
+.wzkb-section.span_3_of_3 {
width: 100%;
}
-.wzkb_section.span_2_of_3 {
+.wzkb-section.span_2_of_3 {
width: 66.13%;
}
-.wzkb_section.span_1_of_3 {
+.wzkb-section.span_1_of_3 {
width: 32.26%;
}
-.wzkb_section.span_1_of_3:nth-child(3n+1) {
+.wzkb-section.span_1_of_3:nth-child(3n+1) {
clear: left;
margin-left: 0;
}
/* GRID OF FOUR */
-.wzkb_section.span_4_of_4 {
+.wzkb-section.span_4_of_4 {
width: 100%;
}
-.wzkb_section.span_3_of_4 {
+.wzkb-section.span_3_of_4 {
width: 74.6%;
}
-.wzkb_section.span_2_of_4 {
+.wzkb-section.span_2_of_4 {
width: 49.2%;
}
-.wzkb_section.span_1_of_4 {
+.wzkb-section.span_1_of_4 {
width: 23.8%;
}
-.wzkb_section.span_1_of_4:nth-child(4n+1) {
+.wzkb-section.span_1_of_4:nth-child(4n+1) {
clear: left;
margin-left: 0;
}
/* GRID OF FIVE */
-.wzkb_section.span_5_of_5 {
+.wzkb-section.span_5_of_5 {
width: 100%;
}
-.wzkb_section.span_4_of_5 {
+.wzkb-section.span_4_of_5 {
width: 79.68%;
}
-.wzkb_section.span_3_of_5 {
+.wzkb-section.span_3_of_5 {
width: 59.36%;
}
-.wzkb_section.span_2_of_5 {
+.wzkb-section.span_2_of_5 {
width: 39.04%;
}
-.wzkb_section.span_1_of_5 {
+.wzkb-section.span_1_of_5 {
width: 18.72%;
}
-.wzkb_section.span_1_of_5:nth-child(5n+1) {
+.wzkb-section.span_1_of_5:nth-child(5n+1) {
clear: left;
margin-left: 0;
}
@@ -474,6 +474,7 @@ nav.pagination {
.wzkb_breadcrumb-item {
display: flex;
align-items: center;
+ flex-direction: row;
}
.wzkb_breadcrumb-item:not(:last-child)::after {
diff --git a/includes/frontend/css/wzkb-styles.min.css b/includes/frontend/css/wzkb-styles.min.css
index 132b5e8..7ecda73 100644
--- a/includes/frontend/css/wzkb-styles.min.css
+++ b/includes/frontend/css/wzkb-styles.min.css
@@ -1 +1 @@
-.wzkb{width:100%;margin:10px 0;}.wzkb:after,.wzkb_master_section:after{content:"";display:table;clear:both;}.wzkb h3 a,.wzkb h4 a,.wzkb h3 a:visited,.wzkb h4 a:visited{color:#333;text-decoration:none;border:0;}.wzkb h3.wzkb_section_name{font-size:20px;}.wzkb-section-name-level-1{border-bottom:1px black dotted;border-top:1px black dotted;padding:5px 1px;background-color:ghostwhite;margin-top:30px;margin-bottom:0;}.wzkb h3.wzkb-section-name-level-0,.wzkb h3.wzkb-section-name-level-1{font-size:28px;}.wzkb h3.wzkb-section-name-level-2{border-bottom:1px solid #666;font-size:24px;}.wzkb-articles-list{margin:0!important;padding:0!important;}.wzkb-articles-list li{margin:5px 0;list-style-type:none!important;}.wzkb-articles-list li:before{content:"\f123";display:inline-block;width:20px;height:20px;font-size:20px;line-height:1;font-family:dashicons;text-decoration:inherit;font-weight:400;font-style:normal;vertical-align:top;text-align:center;-webkit-transition:color .1s ease-in 0;transition:color .1s ease-in 0;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;margin-right:10px;word-wrap:break-word;color:#666;}.wzkb_section_count{display:block;float:right;margin:0;padding:2px;border:0;background:orange;border-radius:45%;text-align:center;font-size:70%;min-width:36px;}.section{clear:both;padding:0;margin:0;}.col{display:block;float:left;margin:1% 0 1% 1.6%;}.col:first-child{margin-left:0;}.group:before,.group:after{content:"";display:table;}.group:after{clear:both;}.group{zoom:1;}.wzkb_section.span_2_of_2{width:100%;}.wzkb_section.span_1_of_2{width:49.2%;}.wzkb_section.span_1_of_2:nth-child(2n+1){clear:left;margin-left:0;}.wzkb_section.span_3_of_3{width:100%;}.wzkb_section.span_2_of_3{width:66.13%;}.wzkb_section.span_1_of_3{width:32.26%;}.wzkb_section.span_1_of_3:nth-child(3n+1){clear:left;margin-left:0;}.wzkb_section.span_4_of_4{width:100%;}.wzkb_section.span_3_of_4{width:74.6%;}.wzkb_section.span_2_of_4{width:49.2%;}.wzkb_section.span_1_of_4{width:23.8%;}.wzkb_section.span_1_of_4:nth-child(4n+1){clear:left;margin-left:0;}.wzkb_section.span_5_of_5{width:100%;}.wzkb_section.span_4_of_5{width:79.68%;}.wzkb_section.span_3_of_5{width:59.36%;}.wzkb_section.span_2_of_5{width:39.04%;}.wzkb_section.span_1_of_5{width:18.72%;}.wzkb_section.span_1_of_5:nth-child(5n+1){clear:left;margin-left:0;}@media only screen and (max-width:480px){.col{margin:1% 0}.span_2_of_2,.span_1_of_2,.span_1_of_3,.span_2_of_3,.span_3_of_3,.span_1_of_4,.span_2_of_4,.span_3_of_4,.span_4_of_4,.span_1_of_5,.span_2_of_5,.span_3_of_5,.span_4_of_5,.span_5_of_5{width:100%}}#wzkb-content-primary{margin:0 auto;max-width:1366px;display:block;}#wzkb-sidebar-primary{float:right;margin-left:-100%;max-width:413px;position:relative;}@media only screen and (max-width:480px){#wzkb-sidebar-primary,#wzkb-content-primary{float:none!important;width:98%!important;margin-left:0}}#wzkb-sidebar-primary .widget{padding:20px;margin-top:10px;}#wzkb-sidebar-primary .widget ul{margin-left:10px;}#wzkb-sidebar-primary .widget ul li{margin-left:10px;list-style-type:disc;}.nav-previous{float:left;}.nav-next{float:right;}.wzkb-search-form{border:#ddd 1px solid;padding-left:10px;position:relative;padding-right:12px;margin:20px 0;font-size:1.3em;width:95%;}.wzkb-search-form label{width:100%;}input[type="search"].wzkb-search-field{border:none;color:#555;font-family:'Open Sans',sans-serif;font-size:1em;padding:10px 6px 10px 45px;width:100%;background:url(../images/search-icon.png) no-repeat 0 5px;outline:none;}input[type="submit"].wzkb-search-submit{display:none;}.wzkb-search-form .screen-reader-text{position:absolute;clip:rect(0 0 0 0);}nav.pagination{width:100%;margin:15px 0;padding:15px 0;border-top:1px dotted #ccc;border-bottom:1px dotted #ccc;text-align:center;}.wzkb-alert{position:relative;padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem;}.wzkb-alert-heading{color:inherit;}.wzkb-alert-link{font-weight:700;}.wzkb-alert-dismissible{padding-right:4rem;}.wzkb-alert-dismissible .close{position:absolute;top:0;right:0;padding:.75rem 1.25rem;color:inherit;}.wzkb-alert-primary{color:#004085;background-color:#cce5ff;border-color:#b8daff;}.wzkb-alert-primary hr{border-top-color:#9fcdff;}.wzkb-alert-primary .wzkb-alert-link{color:#002752;}.wzkb-alert-secondary{color:#383d41;background-color:#e2e3e5;border-color:#d6d8db;}.wzkb-alert-secondary hr{border-top-color:#c8cbcf;}.wzkb-alert-secondary .wzkb-alert-link{color:#202326;}.wzkb-alert-success{color:#155724;background-color:#d4edda;border-color:#c3e6cb;}.wzkb-alert-success hr{border-top-color:#b1dfbb;}.wzkb-alert-success .wzkb-alert-link{color:#0b2e13;}.wzkb-alert-info{color:#0c5460;background-color:#d1ecf1;border-color:#bee5eb;}.wzkb-alert-info hr{border-top-color:#abdde5;}.wzkb-alert-info .wzkb-alert-link{color:#062c33;}.wzkb-alert-warning{color:#856404;background-color:#fff3cd;border-color:#ffeeba;}.wzkb-alert-warning hr{border-top-color:#ffe8a1;}.wzkb-alert-warning .wzkb-alert-link{color:#533f03;}.wzkb-alert-danger{color:#721c24;background-color:#f8d7da;border-color:#f5c6cb;}.wzkb-alert-danger hr{border-top-color:#f1b0b7;}.wzkb-alert-danger .wzkb-alert-link{color:#491217;}.wzkb-alert-light{color:#818182;background-color:#fefefe;border-color:#fdfdfe;}.wzkb-alert-light hr{border-top-color:#ececf6;}.wzkb-alert-light .wzkb-alert-link{color:#686868;}.wzkb-alert-dark{color:#1b1e21;background-color:#d6d8d9;border-color:#c6c8ca;}.wzkb-alert-dark hr{border-top-color:#b9bbbe;}.wzkb-alert-dark .wzkb-alert-link{color:#040505;}.wzkb-related-articles{clear:both;}.wzkb-related-articles ul{list-style-type:none;margin:0;display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));grid-column-gap:25px;column-gap:25px;}.wzkb-related-articles ul li{width:auto;}.wzkb-related-articles img,.wzkb-related-articles a{display:block;}.wzkb_breadcrumb{margin-bottom:1em;}.wzkb_breadcrumb-list{list-style:none;padding:0;margin:0;display:flex;flex-wrap:wrap;align-items:center;}.wzkb_breadcrumb-item{display:flex;align-items:center;}.wzkb_breadcrumb-item:not(:last-child)::after{content:"\00BB";content:attr(data-separator);margin:0 .5em;color:var(--wzkb-breadcrumb-separator-color,#666);}
\ No newline at end of file
+.wzkb{width:100%;margin:10px 0;}.wzkb:after,.wzkb-master-section:after{content:"";display:table;clear:both;}.wzkb h3 a,.wzkb h4 a,.wzkb h3 a:visited,.wzkb h4 a:visited{color:#333;text-decoration:none;border:0;}.wzkb h3.wzkb-section-name{font-size:20px;}.wzkb-section-name-level-1{border-bottom:1px black dotted;border-top:1px black dotted;padding:5px 1px;background-color:ghostwhite;margin-top:30px;margin-bottom:0;}.wzkb h3.wzkb-section-name-level-0,.wzkb h3.wzkb-section-name-level-1{font-size:28px;}.wzkb h3.wzkb-section-name-level-2{border-bottom:1px solid #666;font-size:24px;}.wzkb-articles-list{margin:0!important;padding:0!important;}.wzkb-articles-list li{margin:5px 0;list-style-type:none!important;}.wzkb-articles-list li:before{content:"\f123";display:inline-block;width:20px;height:20px;font-size:20px;line-height:1;font-family:dashicons;text-decoration:inherit;font-weight:400;font-style:normal;vertical-align:top;text-align:center;-webkit-transition:color .1s ease-in 0;transition:color .1s ease-in 0;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;margin-right:10px;word-wrap:break-word;color:#666;}.wzkb-section-count{display:block;float:right;margin:0;padding:2px;border:0;background:orange;border-radius:45%;text-align:center;font-size:70%;min-width:36px;}.section{clear:both;padding:0;margin:0;}.col{display:block;float:left;margin:1% 0 1% 1.6%;}.col:first-child{margin-left:0;}.group:before,.group:after{content:"";display:table;}.group:after{clear:both;}.group{zoom:1;}.wzkb-section.span_2_of_2{width:100%;}.wzkb-section.span_1_of_2{width:49.2%;}.wzkb-section.span_1_of_2:nth-child(2n+1){clear:left;margin-left:0;}.wzkb-section.span_3_of_3{width:100%;}.wzkb-section.span_2_of_3{width:66.13%;}.wzkb-section.span_1_of_3{width:32.26%;}.wzkb-section.span_1_of_3:nth-child(3n+1){clear:left;margin-left:0;}.wzkb-section.span_4_of_4{width:100%;}.wzkb-section.span_3_of_4{width:74.6%;}.wzkb-section.span_2_of_4{width:49.2%;}.wzkb-section.span_1_of_4{width:23.8%;}.wzkb-section.span_1_of_4:nth-child(4n+1){clear:left;margin-left:0;}.wzkb-section.span_5_of_5{width:100%;}.wzkb-section.span_4_of_5{width:79.68%;}.wzkb-section.span_3_of_5{width:59.36%;}.wzkb-section.span_2_of_5{width:39.04%;}.wzkb-section.span_1_of_5{width:18.72%;}.wzkb-section.span_1_of_5:nth-child(5n+1){clear:left;margin-left:0;}@media only screen and (max-width:480px){.col{margin:1% 0}.span_2_of_2,.span_1_of_2,.span_1_of_3,.span_2_of_3,.span_3_of_3,.span_1_of_4,.span_2_of_4,.span_3_of_4,.span_4_of_4,.span_1_of_5,.span_2_of_5,.span_3_of_5,.span_4_of_5,.span_5_of_5{width:100%}}#wzkb-content-primary{margin:0 auto;max-width:1366px;display:block;}#wzkb-sidebar-primary{float:right;margin-left:-100%;max-width:413px;position:relative;}@media only screen and (max-width:480px){#wzkb-sidebar-primary,#wzkb-content-primary{float:none!important;width:98%!important;margin-left:0}}#wzkb-sidebar-primary .widget{padding:20px;margin-top:10px;}#wzkb-sidebar-primary .widget ul{margin-left:10px;}#wzkb-sidebar-primary .widget ul li{margin-left:10px;list-style-type:disc;}.nav-previous{float:left;}.nav-next{float:right;}.wzkb-search-form{border:#ddd 1px solid;padding-left:10px;position:relative;padding-right:12px;margin:20px 0;font-size:1.3em;width:95%;}.wzkb-search-form label{width:100%;}input[type="search"].wzkb-search-field{border:none;color:#555;font-family:'Open Sans',sans-serif;font-size:1em;padding:10px 6px 10px 45px;width:100%;background:url(../images/search-icon.png) no-repeat 0 5px;outline:none;}input[type="submit"].wzkb-search-submit{display:none;}.wzkb-search-form .screen-reader-text{position:absolute;clip:rect(0 0 0 0);}nav.pagination{width:100%;margin:15px 0;padding:15px 0;border-top:1px dotted #ccc;border-bottom:1px dotted #ccc;text-align:center;}.wzkb-alert{position:relative;padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem;}.wzkb-alert-heading{color:inherit;}.wzkb-alert-link{font-weight:700;}.wzkb-alert-dismissible{padding-right:4rem;}.wzkb-alert-dismissible .close{position:absolute;top:0;right:0;padding:.75rem 1.25rem;color:inherit;}.wzkb-alert-primary{color:#004085;background-color:#cce5ff;border-color:#b8daff;}.wzkb-alert-primary hr{border-top-color:#9fcdff;}.wzkb-alert-primary .wzkb-alert-link{color:#002752;}.wzkb-alert-secondary{color:#383d41;background-color:#e2e3e5;border-color:#d6d8db;}.wzkb-alert-secondary hr{border-top-color:#c8cbcf;}.wzkb-alert-secondary .wzkb-alert-link{color:#202326;}.wzkb-alert-success{color:#155724;background-color:#d4edda;border-color:#c3e6cb;}.wzkb-alert-success hr{border-top-color:#b1dfbb;}.wzkb-alert-success .wzkb-alert-link{color:#0b2e13;}.wzkb-alert-info{color:#0c5460;background-color:#d1ecf1;border-color:#bee5eb;}.wzkb-alert-info hr{border-top-color:#abdde5;}.wzkb-alert-info .wzkb-alert-link{color:#062c33;}.wzkb-alert-warning{color:#856404;background-color:#fff3cd;border-color:#ffeeba;}.wzkb-alert-warning hr{border-top-color:#ffe8a1;}.wzkb-alert-warning .wzkb-alert-link{color:#533f03;}.wzkb-alert-danger{color:#721c24;background-color:#f8d7da;border-color:#f5c6cb;}.wzkb-alert-danger hr{border-top-color:#f1b0b7;}.wzkb-alert-danger .wzkb-alert-link{color:#491217;}.wzkb-alert-light{color:#818182;background-color:#fefefe;border-color:#fdfdfe;}.wzkb-alert-light hr{border-top-color:#ececf6;}.wzkb-alert-light .wzkb-alert-link{color:#686868;}.wzkb-alert-dark{color:#1b1e21;background-color:#d6d8d9;border-color:#c6c8ca;}.wzkb-alert-dark hr{border-top-color:#b9bbbe;}.wzkb-alert-dark .wzkb-alert-link{color:#040505;}.wzkb-related-articles{clear:both;}.wzkb-related-articles ul{list-style-type:none;margin:0;display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));grid-column-gap:25px;column-gap:25px;}.wzkb-related-articles ul li{width:auto;}.wzkb-related-articles img,.wzkb-related-articles a{display:block;}.wzkb_breadcrumb{margin-bottom:1em;}.wzkb_breadcrumb-list{list-style:none;padding:0;margin:0;display:flex;flex-wrap:wrap;align-items:center;}.wzkb_breadcrumb-item{display:flex;align-items:center;flex-direction:row;}.wzkb_breadcrumb-item:not(:last-child)::after{content:"\00BB";content:attr(data-separator);margin:0 .5em;color:var(--wzkb-breadcrumb-separator-color,#666);}
\ No newline at end of file
diff --git a/includes/frontend/templates/taxonomy-wzkb_category.php b/includes/frontend/templates/taxonomy-wzkb_category.php
index 779b2c9..f4f719a 100644
--- a/includes/frontend/templates/taxonomy-wzkb_category.php
+++ b/includes/frontend/templates/taxonomy-wzkb_category.php
@@ -39,7 +39,12 @@
term_id}']" );
+ // Display knowledge base content for this category.
+ $args = array(
+ 'category' => $this_tax->term_id,
+ 'extra_class' => 'wzkb-category-archive',
+ );
+ echo wzkb_knowledge( $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
// If no content, include the "No posts found" template.
else :
@@ -59,5 +64,3 @@
+
+
+
+
+
+
+ {{kbsearch}}
+
+
+ {{kbbreadcrumb}}
+
+
+ {{knowledgebase product="-1"}}
+
+
+
+
+
+
diff --git a/includes/frontend/templates/taxonomy-wzkb_product.php b/includes/frontend/templates/taxonomy-wzkb_product.php
new file mode 100644
index 0000000..8e233ea
--- /dev/null
+++ b/includes/frontend/templates/taxonomy-wzkb_product.php
@@ -0,0 +1,60 @@
+get_queried_object();
+
+get_header();
+
+if ( wzkb_get_option( 'include_styles' ) ) {
+ wp_enqueue_style( 'wz-knowledgebase-styles' );
+}
+?>
+
+
+
+
+
+
+
+
+ $this_tax->term_id,
+ 'extra_class' => 'wzkb-product-archive',
+ );
+ echo wzkb_knowledge( $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
+ ?>
+
+
+
+
+
+
+
+
+
+
+ $settings ) {
- foreach ( $settings as $option ) {
- // When checkbox is set to true, set this to 1.
- if ( 'checkbox' === $option['type'] && ! empty( $option['options'] ) ) {
- $options[ $option['id'] ] = 1;
- } else {
- $options[ $option['id'] ] = 0;
- }
- // If an option is set.
- if ( in_array( $option['type'], array( 'textarea', 'css', 'html', 'text', 'url', 'csv', 'color', 'numbercsv', 'postids', 'posttypes', 'number', 'wysiwyg', 'file', 'password' ), true ) && isset( $option['options'] ) ) {
- $options[ $option['id'] ] = $option['options'];
- }
- if ( in_array( $option['type'], array( 'multicheck', 'radio', 'select', 'radiodesc', 'thumbsizes' ), true ) && isset( $option['default'] ) ) {
- $options[ $option['id'] ] = $option['default'];
- }
- }
- }
-
- /**
- * Filters the default settings array.
- *
- * @since 1.2.0
- *
- * @param array $options Default settings.
- */
- return apply_filters( 'wzkb_settings_defaults', $options );
+ return WebberZone\Knowledge_Base\Options_API::get_settings_defaults();
}
-
/**
* Get the default option for a specific key
*
@@ -213,29 +94,19 @@ function wzkb_settings_defaults() {
* @return mixed
*/
function wzkb_get_default_option( $key = '' ) {
-
- $default_settings = wzkb_settings_defaults();
-
- if ( array_key_exists( $key, $default_settings ) ) {
- return $default_settings[ $key ];
- } else {
- return false;
- }
+ return WebberZone\Knowledge_Base\Options_API::get_default_option( $key );
}
-
/**
* Reset settings.
*
- * @since 1.2.0
- *
- * @return void
+ * @since 1.0.0
+ * @return bool Success status.
*/
function wzkb_settings_reset() {
- delete_option( 'wzkb_settings' );
+ return WebberZone\Knowledge_Base\Options_API::reset_settings();
}
-
if ( ! function_exists( 'wz_tag_search' ) ) :
/**
* Function to add an action to search for tags using Ajax.
diff --git a/knowledgebase.php b/knowledgebase.php
index 6ea55d6..3064d43 100644
--- a/knowledgebase.php
+++ b/knowledgebase.php
@@ -13,7 +13,7 @@
* Plugin Name: WebberZone Knowledge Base
* Plugin URI: https://github.com/WebberZone/knowledgebase
* Description: Create a multi-product knowledge base on your WordPress site.
- * Version: 2.3.1-beta1
+ * Version: 3.0.0-beta1
* Author: WebberZone
* Author URI: https://webberzone.com
* License: GPL-2.0+
@@ -37,7 +37,7 @@
*
* @var string $wzkb_version Plugin version
*/
- define( 'WZKB_VERSION', '2.3.0' );
+ define( 'WZKB_VERSION', '3.0.0-beta1' );
}
if ( ! defined( 'WZKB_PLUGIN_DIR' ) ) {
@@ -78,16 +78,28 @@
if ( ! function_exists( __NAMESPACE__ . '\load' ) ) {
/**
- * The main function responsible for returning the one true WebberZone Snippetz instance to functions everywhere.
+ * The main function responsible for returning the one true WebberZone Knowledge Base instance to functions everywhere.
*
* @since 2.3.0
*/
function load() {
- Main::get_instance();
+ wzkb();
}
add_action( 'plugins_loaded', __NAMESPACE__ . '\load' );
}
+if ( ! function_exists( 'wzkb' ) ) {
+ /**
+ * Get the main WebberZone Knowledge Base instance.
+ *
+ * @since 3.0.0
+ * @return Main Main instance.
+ */
+ function wzkb() {
+ return Main::get_instance();
+ }
+}
+
// Register the activation hook.
register_activation_hook( __FILE__, __NAMESPACE__ . '\Admin\Activator::activate' );
@@ -110,4 +122,4 @@ function load() {
* @var array WZKB Settings
*/
global $wzkb_settings;
-$wzkb_settings = wzkb_get_settings();
+$wzkb_settings = Options_API::get_settings();
diff --git a/package.json b/package.json
index c8f635d..d2f019e 100644
--- a/package.json
+++ b/package.json
@@ -35,12 +35,12 @@
"readme.txt"
],
"devDependencies": {
- "@wordpress/prettier-config": "^4.14.0",
- "@wordpress/scripts": "^27"
+ "@wordpress/prettier-config": "^4.22.0",
+ "@wordpress/scripts": "^30"
},
"dependencies": {
- "@wordpress/icons": "^10.14.0",
+ "@wordpress/icons": "^10.22.0",
"clsx": "^2.1.1",
- "uuid": "^11.0.3"
+ "uuid": "^11.1.0"
}
}
\ No newline at end of file
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
index ca585b3..8911e82 100644
--- a/phpstan-baseline.neon
+++ b/phpstan-baseline.neon
@@ -1,31 +1,6 @@
parameters:
ignoreErrors:
- -
- message: "#^Constant WZKB_PLUGIN_URL not found\\.$#"
- count: 3
- path: includes/admin/settings/sidebar.php
-
-
message: "#^Constant WZKB_PLUGIN_DIR not found\\.$#"
count: 1
- path: includes/autoloader.php
-
- -
- message: "#^Constant WZKB_PLUGIN_DIR not found\\.$#"
- count: 1
- path: includes/frontend/class-language-handler.php
-
- -
- message: "#^Method WebberZone\\\\Knowledge_Base\\\\Widgets\\\\Articles_Widget\\:\\:form\\(\\) should return string but return statement is missing\\.$#"
- count: 1
- path: includes/widgets/class-articles-widget.php
-
- -
- message: "#^Method WebberZone\\\\Knowledge_Base\\\\Widgets\\\\Breadcrumb_Widget\\:\\:form\\(\\) should return string but return statement is missing\\.$#"
- count: 1
- path: includes/widgets/class-breadcrumb-widget.php
-
- -
- message: "#^Method WebberZone\\\\Knowledge_Base\\\\Widgets\\\\Sections_Widget\\:\\:form\\(\\) should return string but return statement is missing\\.$#"
- count: 1
- path: includes/widgets/class-sections-widget.php
\ No newline at end of file
+ path: includes/frontend/class-language-handler.php
\ No newline at end of file
diff --git a/readme.txt b/readme.txt
index 950890c..ed08ed2 100644
--- a/readme.txt
+++ b/readme.txt
@@ -5,7 +5,7 @@ Tags: knowledge base, knowledgebase, FAQ, frequently asked questions, support, d
Requires at least: 6.3
Tested up to: 6.7
Requires PHP: 7.4
-Stable tag: 2.3.1
+Stable tag: 2.3.0
License: GPLv2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
@@ -113,15 +113,32 @@ You can report security bugs through the Patchstack Vulnerability Disclosure Pro
== Upgrade Notice ==
-= 2.3.1 =
+= 2.3.0 =
Completely rewritten. Several new features and enhancements.
== Changelog ==
-= 2.3.1
+= 3.0.0 =
+
+* Features:
+ * Introduced a new hierarchical Products taxonomy (`wzkb_product`) enabling multi-product support for articles and sections.
+ * Migration wizard to map existing sections and articles to products, with dry-run and batch processing.
+ * Product-based frontend templates that preserve section hierarchy.
+ * Admin UI enhancements for managing products, sections, and migration.
-* Bug fix:
- * Default thumbnail didn't work properly in the related posts.
+* Modifications:
+ * Standardized CSS class names to use consistent hyphenation (e.g., `wzkb_section` is now `wzkb-section`). If you have custom CSS targeting the old class names, you'll need to update your stylesheets.
+
+* Breaking Changes:
+ * CSS classes have been renamed for consistency:
+ * `wzkb_section` → `wzkb-section`
+ * `wzkb_section_wrapper` → `wzkb-section-wrapper`
+ * `wzkb_section_name` → `wzkb-section-name`
+ * `wzkb_section_count` → `wzkb-section-count`
+ * `wzkb_shortcode` → `wzkb-shortcode`
+ * `wzkb_block` → `wzkb-block`
+ * And other similar class name changes
+ * If you have custom CSS targeting these classes, you'll need to update your selectors
= 2.3.0 =
diff --git a/uninstall.php b/uninstall.php
index 2aeeaa9..69b0f0f 100644
--- a/uninstall.php
+++ b/uninstall.php
@@ -61,6 +61,7 @@ function wzkb_delete_data() {
wzkb_delete_taxonomy( 'wzkb_category' );
wzkb_delete_taxonomy( 'wzkb_tag' );
+ wzkb_delete_taxonomy( 'wzkb_product' );
}
// Delete the cache.
From d4b6d62dd1f3b28f4adde5b4f043ce8d2dc7fec6 Mon Sep 17 00:00:00 2001
From: Ajay D'Souza
Date: Fri, 25 Apr 2025 13:13:50 +0100
Subject: [PATCH 006/119] Use Hook_Registry
---
includes/admin/class-activator.php | 3 +-
includes/admin/class-admin-columns.php | 20 +-
includes/admin/class-admin.php | 11 +-
includes/admin/class-product-migrator.php | 12 +-
includes/admin/class-settings.php | 14 +-
includes/blocks/class-blocks.php | 4 +-
includes/class-cpt.php | 6 +-
includes/class-main.php | 5 +-
includes/class-options-api.php | 4 +-
includes/frontend/class-feed.php | 4 +-
includes/frontend/class-language-handler.php | 4 +-
includes/frontend/class-styles-handler.php | 4 +-
includes/frontend/class-template-handler.php | 14 +-
includes/util/class-cache.php | 4 +-
includes/util/class-hook-registry.php | 252 +++++++++++++++++++
15 files changed, 318 insertions(+), 43 deletions(-)
create mode 100644 includes/util/class-hook-registry.php
diff --git a/includes/admin/class-activator.php b/includes/admin/class-activator.php
index c749fbc..806061d 100644
--- a/includes/admin/class-activator.php
+++ b/includes/admin/class-activator.php
@@ -12,6 +12,7 @@
namespace WebberZone\Knowledge_Base\Admin;
use WebberZone\Knowledge_Base\CPT;
+use WebberZone\Knowledge_Base\Util\Hook_Registry;
/**
* Class Activator
@@ -26,7 +27,7 @@ class Activator {
* @since 3.3.0
*/
public function __construct() {
- add_action( 'wp_initialize_site', array( $this, 'activate_new_site' ) );
+ Hook_Registry::add_action( 'wp_initialize_site', array( $this, 'activate_new_site' ) );
}
/**
diff --git a/includes/admin/class-admin-columns.php b/includes/admin/class-admin-columns.php
index 42028c0..f986661 100644
--- a/includes/admin/class-admin-columns.php
+++ b/includes/admin/class-admin-columns.php
@@ -7,6 +7,8 @@
namespace WebberZone\Knowledge_Base\Admin;
+use WebberZone\Knowledge_Base\Util\Hook_Registry;
+
// If this file is called directly, abort.
if ( ! defined( 'WPINC' ) ) {
die;
@@ -25,20 +27,20 @@ class Admin_Columns {
* @since 2.3.0
*/
public function __construct() {
- add_filter( 'manage_edit-wzkb_category_columns', array( $this, 'tax_columns' ) );
- add_filter( 'manage_edit-wzkb_category_sortable_columns', array( $this, 'tax_sortable_columns' ) );
- add_filter( 'manage_edit-wzkb_tag_columns', array( $this, 'tax_columns' ) );
- add_filter( 'manage_edit-wzkb_tag_sortable_columns', array( $this, 'tax_sortable_columns' ) );
+ Hook_Registry::add_filter( 'manage_edit-wzkb_category_columns', array( $this, 'tax_columns' ) );
+ Hook_Registry::add_filter( 'manage_edit-wzkb_category_sortable_columns', array( $this, 'tax_sortable_columns' ) );
+ Hook_Registry::add_filter( 'manage_edit-wzkb_tag_columns', array( $this, 'tax_columns' ) );
+ Hook_Registry::add_filter( 'manage_edit-wzkb_tag_sortable_columns', array( $this, 'tax_sortable_columns' ) );
- add_filter( 'manage_wzkb_category_custom_column', array( $this, 'tax_id' ), 10, 3 );
- add_filter( 'manage_wzkb_tag_custom_column', array( $this, 'tax_id' ), 10, 3 );
+ Hook_Registry::add_filter( 'manage_wzkb_category_custom_column', array( $this, 'tax_id' ), 10, 3 );
+ Hook_Registry::add_filter( 'manage_wzkb_tag_custom_column', array( $this, 'tax_id' ), 10, 3 );
// Register Product filter for Articles admin screen.
- add_action( 'restrict_manage_posts', array( $this, 'add_product_filter_dropdown' ) );
- add_action( 'pre_get_posts', array( $this, 'filter_articles_by_product' ) );
+ Hook_Registry::add_action( 'restrict_manage_posts', array( $this, 'add_product_filter_dropdown' ) );
+ Hook_Registry::add_action( 'pre_get_posts', array( $this, 'filter_articles_by_product' ) );
// Add sorting.
- add_filter( 'terms_clauses', array( $this, 'sort_terms_by_product' ), 10, 2 );
+ Hook_Registry::add_filter( 'terms_clauses', array( $this, 'sort_terms_by_product' ), 10, 2 );
}
/**
diff --git a/includes/admin/class-admin.php b/includes/admin/class-admin.php
index 29ef4c6..984ab55 100644
--- a/includes/admin/class-admin.php
+++ b/includes/admin/class-admin.php
@@ -10,6 +10,7 @@
namespace WebberZone\Knowledge_Base\Admin;
use WebberZone\Knowledge_Base\Util\Cache;
+use WebberZone\Knowledge_Base\Util\Hook_Registry;
// If this file is called directly, abort.
if ( ! defined( 'WPINC' ) ) {
@@ -90,11 +91,11 @@ public function __construct() {
* @since 2.3.0
*/
public function hooks() {
- add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ) );
- add_action( 'admin_notices', array( $this, 'admin_notices' ) );
- add_filter( 'dashboard_glance_items', array( $this, 'dashboard_glance_items' ), 10, 1 );
- add_filter( 'admin_head', array( $this, 'admin_head' ) );
- add_action( 'admin_footer', array( $this, 'maybe_add_button_to_post_list' ) );
+ Hook_Registry::add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ) );
+ Hook_Registry::add_action( 'admin_notices', array( $this, 'admin_notices' ) );
+ Hook_Registry::add_filter( 'dashboard_glance_items', array( $this, 'dashboard_glance_items' ), 10, 1 );
+ Hook_Registry::add_filter( 'admin_head', array( $this, 'admin_head' ) );
+ Hook_Registry::add_action( 'admin_footer', array( $this, 'maybe_add_button_to_post_list' ) );
}
/**
diff --git a/includes/admin/class-product-migrator.php b/includes/admin/class-product-migrator.php
index 2c867bd..6390c1e 100644
--- a/includes/admin/class-product-migrator.php
+++ b/includes/admin/class-product-migrator.php
@@ -7,6 +7,8 @@
namespace WebberZone\Knowledge_Base\Admin;
+use WebberZone\Knowledge_Base\Util\Hook_Registry;
+
if ( ! defined( 'WPINC' ) ) {
die;
}
@@ -49,10 +51,10 @@ class Product_Migrator {
* Constructor: Hook admin notices and menu.
*/
public function __construct() {
- add_action( 'admin_notices', array( $this, 'maybe_show_enable_notice' ) );
- add_action( 'admin_menu', array( $this, 'register_migration_wizard_page' ) );
- add_action( 'wp_ajax_wzkb_dismiss_product_notice', array( $this, 'dismiss_product_notice' ) );
- add_action( 'wp_ajax_wzkb_product_migration_batch', array( $this, 'handle_migration_batch' ) );
+ Hook_Registry::add_action( 'admin_notices', array( $this, 'maybe_show_enable_notice' ) );
+ Hook_Registry::add_action( 'admin_menu', array( $this, 'register_migration_wizard_page' ) );
+ Hook_Registry::add_action( 'wp_ajax_wzkb_dismiss_product_notice', array( $this, 'dismiss_product_notice' ) );
+ Hook_Registry::add_action( 'wp_ajax_wzkb_product_migration_batch', array( $this, 'handle_migration_batch' ) );
}
/**
@@ -111,7 +113,7 @@ public function register_migration_wizard_page() {
'wzkb-product-migration',
array( $this, 'render_migration_wizard' ),
);
- add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
+ Hook_Registry::add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
}
/**
diff --git a/includes/admin/class-settings.php b/includes/admin/class-settings.php
index 2274110..1a6c9ac 100644
--- a/includes/admin/class-settings.php
+++ b/includes/admin/class-settings.php
@@ -9,6 +9,8 @@
namespace WebberZone\Knowledge_Base\Admin;
+use WebberZone\Knowledge_Base\Util\Hook_Registry;
+
// If this file is called directly, abort.
if ( ! defined( 'WPINC' ) ) {
die;
@@ -140,13 +142,13 @@ public function __construct() {
self::$prefix = 'wzkb';
$this->menu_slug = 'wzkb-settings';
- add_action( 'admin_menu', array( $this, 'initialise_settings' ) );
- add_action( 'admin_head', array( $this, 'admin_head' ), 11 );
- add_filter( 'plugin_row_meta', array( $this, 'plugin_row_meta' ), 11, 2 );
- add_filter( 'plugin_action_links_' . plugin_basename( WZKB_PLUGIN_FILE ), array( $this, 'plugin_actions_links' ) );
- add_filter( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ), 99 );
+ Hook_Registry::add_action( 'admin_menu', array( $this, 'initialise_settings' ) );
+ Hook_Registry::add_action( 'admin_head', array( $this, 'admin_head' ), 11 );
+ Hook_Registry::add_filter( 'plugin_row_meta', array( $this, 'plugin_row_meta' ), 11, 2 );
+ Hook_Registry::add_filter( 'plugin_action_links_' . plugin_basename( WZKB_PLUGIN_FILE ), array( $this, 'plugin_actions_links' ) );
+ Hook_Registry::add_filter( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ), 99 );
- add_filter( self::$prefix . '_settings_sanitize', array( $this, 'change_settings_on_save' ), 99 );
+ Hook_Registry::add_filter( self::$prefix . '_settings_sanitize', array( $this, 'change_settings_on_save' ), 99 );
}
/**
diff --git a/includes/blocks/class-blocks.php b/includes/blocks/class-blocks.php
index 71c6cd5..9650518 100644
--- a/includes/blocks/class-blocks.php
+++ b/includes/blocks/class-blocks.php
@@ -8,8 +8,8 @@
namespace WebberZone\Knowledge_Base\Blocks;
+use WebberZone\Knowledge_Base\Util\Hook_Registry;
use WebberZone\Knowledge_Base\Frontend\Display;
-use WebberZone\Knowledge_Base\Frontend\Breadcrumbs;
// If this file is called directly, abort.
if ( ! defined( 'WPINC' ) ) {
@@ -29,7 +29,7 @@ class Blocks {
* @since 2.3.0
*/
public function __construct() {
- add_action( 'init', array( $this, 'register_blocks' ) );
+ Hook_Registry::add_action( 'init', array( $this, 'register_blocks' ) );
}
/**
diff --git a/includes/class-cpt.php b/includes/class-cpt.php
index f4c3937..5adf72b 100644
--- a/includes/class-cpt.php
+++ b/includes/class-cpt.php
@@ -7,6 +7,8 @@
namespace WebberZone\Knowledge_Base;
+use WebberZone\Knowledge_Base\Util\Hook_Registry;
+
// If this file is called directly, abort.
if ( ! defined( 'WPINC' ) ) {
die;
@@ -25,8 +27,8 @@ class CPT {
* @since 2.3.0
*/
public function __construct() {
- add_action( 'init', array( $this, 'register_post_type' ) );
- add_action( 'init', array( $this, 'register_taxonomies' ) );
+ Hook_Registry::add_action( 'init', array( $this, 'register_post_type' ) );
+ Hook_Registry::add_action( 'init', array( $this, 'register_taxonomies' ) );
}
/**
diff --git a/includes/class-main.php b/includes/class-main.php
index 9f7f674..4b2370f 100644
--- a/includes/class-main.php
+++ b/includes/class-main.php
@@ -9,6 +9,7 @@
use WebberZone\Knowledge_Base\Admin\Admin;
use WebberZone\Knowledge_Base\Pro\Pro;
+use WebberZone\Knowledge_Base\Util\Hook_Registry;
if ( ! defined( 'WPINC' ) ) {
exit;
@@ -195,8 +196,8 @@ private function init() {
* @since 2.3.0
*/
public function hooks() {
- add_action( 'init', array( $this, 'initiate_plugin' ) );
- add_action( 'widgets_init', array( $this, 'register_widgets' ) );
+ Hook_Registry::add_action( 'init', array( $this, 'initiate_plugin' ) );
+ Hook_Registry::add_action( 'widgets_init', array( $this, 'register_widgets' ) );
}
/**
diff --git a/includes/class-options-api.php b/includes/class-options-api.php
index 94177df..5210487 100644
--- a/includes/class-options-api.php
+++ b/includes/class-options-api.php
@@ -9,6 +9,8 @@
namespace WebberZone\Knowledge_Base;
+use WebberZone\Knowledge_Base\Util\Hook_Registry;
+
if ( ! defined( 'WPINC' ) ) {
die;
}
@@ -50,7 +52,7 @@ class Options_API {
* @since 3.0.0
*/
public static function init() {
- add_action( 'wp_ajax_' . self::FILTER_PREFIX . '_tags_search', array( __CLASS__, 'tags_search' ) );
+ Hook_Registry::add_action( 'wp_ajax_' . self::FILTER_PREFIX . '_tags_search', array( __CLASS__, 'tags_search' ) );
}
/**
diff --git a/includes/frontend/class-feed.php b/includes/frontend/class-feed.php
index 744004d..0d0ec7e 100644
--- a/includes/frontend/class-feed.php
+++ b/includes/frontend/class-feed.php
@@ -7,6 +7,8 @@
namespace WebberZone\Knowledge_Base\Frontend;
+use WebberZone\Knowledge_Base\Util\Hook_Registry;
+
// If this file is called directly, abort.
if ( ! defined( 'WPINC' ) ) {
die;
@@ -21,7 +23,7 @@ class Feed {
* Constructor.
*/
public function __construct() {
- add_filter( 'request', array( $this, 'in_feed' ), 11 );
+ Hook_Registry::add_filter( 'request', array( $this, 'in_feed' ), 11 );
}
/**
diff --git a/includes/frontend/class-language-handler.php b/includes/frontend/class-language-handler.php
index 5a5c597..c585a25 100644
--- a/includes/frontend/class-language-handler.php
+++ b/includes/frontend/class-language-handler.php
@@ -7,6 +7,8 @@
namespace WebberZone\Knowledge_Base\Frontend;
+use WebberZone\Knowledge_Base\Util\Hook_Registry;
+
// If this file is called directly, abort.
if ( ! defined( 'WPINC' ) ) {
die;
@@ -25,7 +27,7 @@ class Language_Handler {
* @since 2.3.0
*/
public function __construct() {
- add_action( 'init', array( $this, 'load_plugin_textdomain' ) );
+ Hook_Registry::add_action( 'init', array( $this, 'load_plugin_textdomain' ) );
}
/**
diff --git a/includes/frontend/class-styles-handler.php b/includes/frontend/class-styles-handler.php
index 2fc7a7f..4eccfe4 100644
--- a/includes/frontend/class-styles-handler.php
+++ b/includes/frontend/class-styles-handler.php
@@ -7,6 +7,8 @@
namespace WebberZone\Knowledge_Base\Frontend;
+use WebberZone\Knowledge_Base\Util\Hook_Registry;
+
if ( ! defined( 'WPINC' ) ) {
die;
}
@@ -24,7 +26,7 @@ class Styles_Handler {
* @since 2.3.0
*/
public function __construct() {
- add_action( 'wp_enqueue_scripts', array( $this, 'register_styles' ) );
+ Hook_Registry::add_action( 'wp_enqueue_scripts', array( $this, 'register_styles' ) );
}
/**
diff --git a/includes/frontend/class-template-handler.php b/includes/frontend/class-template-handler.php
index 6b03342..9be5035 100644
--- a/includes/frontend/class-template-handler.php
+++ b/includes/frontend/class-template-handler.php
@@ -7,6 +7,8 @@
namespace WebberZone\Knowledge_Base\Frontend;
+use WebberZone\Knowledge_Base\Util\Hook_Registry;
+
/**
* Class Template_Handler
*
@@ -17,9 +19,9 @@ class Template_Handler {
* Constructor for the Template_Handler class.
*/
public function __construct() {
- add_filter( 'template_include', array( $this, 'archive_template' ) );
+ Hook_Registry::add_filter( 'template_include', array( $this, 'archive_template' ) );
- add_filter( 'get_block_templates', array( $this, 'manage_block_templates' ), 10, 3 );
+ Hook_Registry::add_filter( 'get_block_templates', array( $this, 'manage_block_templates' ), 10, 3 );
$template_types = array(
'archive' => 'add_custom_archive_template',
@@ -30,12 +32,12 @@ public function __construct() {
);
foreach ( $template_types as $type => $callback ) {
- add_filter( "{$type}_template_hierarchy", array( $this, $callback ) );
+ Hook_Registry::add_filter( "{$type}_template_hierarchy", array( $this, $callback ) );
}
- add_filter( 'pre_get_posts', array( $this, 'posts_per_search_page' ) );
- add_filter( 'document_title_parts', array( $this, 'update_title' ), 99999 );
- add_action( 'widgets_init', array( $this, 'register_sidebars' ), 11 );
+ Hook_Registry::add_filter( 'pre_get_posts', array( $this, 'posts_per_search_page' ) );
+ Hook_Registry::add_filter( 'document_title_parts', array( $this, 'update_title' ), 99999 );
+ Hook_Registry::add_action( 'widgets_init', array( $this, 'register_sidebars' ), 11 );
}
/**
diff --git a/includes/util/class-cache.php b/includes/util/class-cache.php
index 136725e..36f67eb 100644
--- a/includes/util/class-cache.php
+++ b/includes/util/class-cache.php
@@ -9,6 +9,8 @@
namespace WebberZone\Knowledge_Base\Util;
+use WebberZone\Knowledge_Base\Util\Hook_Registry;
+
if ( ! defined( 'WPINC' ) ) {
die;
}
@@ -26,7 +28,7 @@ class Cache {
* @since 2.3.0
*/
public function __construct() {
- add_action( 'wp_ajax_wzkb_clear_cache', array( $this, 'ajax_clearcache' ) );
+ Hook_Registry::add_action( 'wp_ajax_wzkb_clear_cache', array( $this, 'ajax_clearcache' ) );
}
/**
diff --git a/includes/util/class-hook-registry.php b/includes/util/class-hook-registry.php
new file mode 100644
index 0000000..af9fac8
--- /dev/null
+++ b/includes/util/class-hook-registry.php
@@ -0,0 +1,252 @@
+ $hook_type,
+ 'name' => $hook_name,
+ 'callback' => $callback,
+ 'priority' => $priority,
+ 'args' => $args,
+ 'closure_id' => $closure_id,
+ );
+
+ // Register with WordPress.
+ if ( 'action' === $hook_type ) {
+ add_action( $hook_name, $callback, $priority, $args );
+ } else {
+ add_filter( $hook_name, $callback, $priority, $args );
+ }
+
+ return true;
+ }
+
+ /**
+ * Register an action.
+ *
+ * @param string $hook_name The hook name.
+ * @param callable $callback The callback function.
+ * @param int $priority Priority of the hook.
+ * @param int $args Number of arguments.
+ *
+ * @return bool True if registered, false if duplicate or invalid hook.
+ */
+ public static function add_action( $hook_name, $callback, $priority = 10, $args = 1 ) {
+ return self::register( 'action', $hook_name, $callback, $priority, $args );
+ }
+
+ /**
+ * Register a filter.
+ *
+ * @param string $hook_name The hook name.
+ * @param callable $callback The callback function.
+ * @param int $priority Priority of the hook.
+ * @param int $args Number of arguments.
+ *
+ * @return bool True if registered, false if duplicate or invalid hook.
+ */
+ public static function add_filter( $hook_name, $callback, $priority = 10, $args = 1 ) {
+ return self::register( 'filter', $hook_name, $callback, $priority, $args );
+ }
+
+ /**
+ * Remove a hook (action or filter).
+ *
+ * Note: Closures are removable only if registered via this class.
+ *
+ * @param string $hook_type Either 'action' or 'filter'.
+ * @param string $hook_name The hook name.
+ * @param callable $callback The callback function.
+ * @param int $priority Priority of the hook.
+ *
+ * @return bool True if removed, false if not found or removal failed.
+ */
+ public static function remove( $hook_type, $hook_name, $callback, $priority = 10 ) {
+ $closure_id = '';
+ if ( $callback instanceof \Closure ) {
+ // Find the closure_id for this callback.
+ foreach ( self::$hooks as $hook ) {
+ if ( $hook['name'] === $hook_name &&
+ $hook['priority'] === $priority &&
+ $hook['type'] === $hook_type &&
+ $hook['callback'] === $callback ) {
+ $closure_id = $hook['closure_id'];
+ break;
+ }
+ }
+ }
+
+ $key = self::create_hook_key( $hook_name, $callback, $priority, $closure_id );
+
+ if ( ! isset( self::$hooks[ $key ] ) || self::$hooks[ $key ]['type'] !== $hook_type ) {
+ return false;
+ }
+
+ $removed = false;
+ if ( 'action' === $hook_type ) {
+ $removed = remove_action( $hook_name, $callback, $priority );
+ } else {
+ $removed = remove_filter( $hook_name, $callback, $priority );
+ }
+
+ if ( $removed ) {
+ unset( self::$hooks[ $key ] );
+ }
+
+ return $removed;
+ }
+
+ /**
+ * Remove an action.
+ *
+ * @param string $hook_name The hook name.
+ * @param callable $callback The callback function.
+ * @param int $priority Priority of the hook.
+ *
+ * @return bool True if removed, false if not found or removal failed.
+ */
+ public static function remove_action( $hook_name, $callback, $priority = 10 ) {
+ return self::remove( 'action', $hook_name, $callback, $priority );
+ }
+
+ /**
+ * Remove a filter.
+ *
+ * @param string $hook_name The hook name.
+ * @param callable $callback The callback function.
+ * @param int $priority Priority of the hook.
+ *
+ * @return bool True if removed, false if not found or removal failed.
+ */
+ public static function remove_filter( $hook_name, $callback, $priority = 10 ) {
+ return self::remove( 'filter', $hook_name, $callback, $priority );
+ }
+
+ /**
+ * Get all registered hooks.
+ *
+ * @return array Array of registered hooks.
+ */
+ public static function get_hooks() {
+ return self::$hooks;
+ }
+
+ /**
+ * Remove all registered hooks.
+ *
+ * @return void
+ */
+ public static function remove_all_hooks() {
+ foreach ( self::$hooks as $key => $hook ) {
+ if ( 'action' === $hook['type'] ) {
+ remove_action( $hook['name'], $hook['callback'], $hook['priority'] );
+ } else {
+ remove_filter( $hook['name'], $hook['callback'], $hook['priority'] );
+ }
+ unset( self::$hooks[ $key ] );
+ }
+ }
+
+ /**
+ * Create a unique key for a hook registration.
+ *
+ * @param string $hook_name The hook name.
+ * @param callable $callback The callback function.
+ * @param int $priority Priority of the hook.
+ * @param string $closure_id Unique ID for closures.
+ *
+ * @return string Unique key.
+ */
+ private static function create_hook_key( $hook_name, $callback, $priority, $closure_id = '' ) {
+ return md5( $hook_name . self::callback_to_string( $callback ) . $priority . $closure_id );
+ }
+
+ /**
+ * Convert a callback to a string representation.
+ *
+ * @param callable $callback The callback to convert.
+ *
+ * @return string String representation of the callback.
+ */
+ private static function callback_to_string( $callback ) {
+ if ( is_string( $callback ) ) {
+ return $callback;
+ }
+
+ if ( is_array( $callback ) ) {
+ if ( is_object( $callback[0] ) ) {
+ return get_class( $callback[0] ) . '::' . $callback[1];
+ }
+ return $callback[0] . '::' . $callback[1];
+ }
+
+ if ( $callback instanceof \Closure ) {
+ return 'closure';
+ }
+
+ return 'unknown';
+ }
+}
From 96d4822be71eda3a969751c66f6652b7e9fe7291 Mon Sep 17 00:00:00 2001
From: Ajay D'Souza
Date: Sun, 27 Apr 2025 14:31:18 +0100
Subject: [PATCH 007/119] Setup Wizard
---
includes/admin/class-admin.php | 43 +-
includes/admin/class-settings.php | 4 +-
includes/admin/class-setup-wizard.php | 724 ++++++++++++++++++
includes/admin/css/wizard.css | 167 ++++
includes/admin/js/wizard.js | 28 +
.../admin/settings/class-settings-api.php | 14 +-
.../settings/class-settings-sanitize.php | 2 +-
.../settings/js/settings-admin-scripts.js | 11 +-
.../settings/js/settings-admin-scripts.min.js | 2 +-
includes/class-options-api.php | 34 +-
includes/options-api.php | 14 +
11 files changed, 1019 insertions(+), 24 deletions(-)
create mode 100644 includes/admin/class-setup-wizard.php
create mode 100644 includes/admin/css/wizard.css
create mode 100644 includes/admin/js/wizard.js
diff --git a/includes/admin/class-admin.php b/includes/admin/class-admin.php
index 984ab55..a22c875 100644
--- a/includes/admin/class-admin.php
+++ b/includes/admin/class-admin.php
@@ -11,6 +11,7 @@
use WebberZone\Knowledge_Base\Util\Cache;
use WebberZone\Knowledge_Base\Util\Hook_Registry;
+use WebberZone\Knowledge_Base\Admin\Setup_Wizard;
// If this file is called directly, abort.
if ( ! defined( 'WPINC' ) ) {
@@ -42,6 +43,15 @@ class Admin {
*/
public $activator;
+ /**
+ * Setup wizard.
+ *
+ * @since 3.0.0
+ *
+ * @var Setup_Wizard|null Setup wizard instance.
+ */
+ public ?Setup_Wizard $setup_wizard = null;
+
/**
* Cache.
*
@@ -83,6 +93,7 @@ public function __construct() {
$this->cache = new Cache();
$this->admin_columns = new Admin_Columns();
$this->product_migrator = new Product_Migrator();
+ $this->setup_wizard = new Setup_Wizard();
}
/**
@@ -147,16 +158,16 @@ public function admin_enqueue_scripts() {
* @since 2.3.0
*/
public function admin_notices() {
- $kb_slug = \wzkb_get_option( 'kb_slug', 'not-set-random-string' );
- $product_slug = \wzkb_get_option( 'product_slug', 'not-set-random-string' );
- $cat_slug = \wzkb_get_option( 'category_slug', 'not-set-random-string' );
- $tag_slug = \wzkb_get_option( 'tag_slug', 'not-set-random-string' );
-
// Only add the notice if the user is an admin.
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
+ $kb_slug = \wzkb_get_option( 'kb_slug', 'not-set-random-string' );
+ $product_slug = \wzkb_get_option( 'product_slug', 'not-set-random-string' );
+ $cat_slug = \wzkb_get_option( 'category_slug', 'not-set-random-string' );
+ $tag_slug = \wzkb_get_option( 'tag_slug', 'not-set-random-string' );
+
// Only add the notice if the settings cannot be found.
if ( 'not-set-random-string' === $kb_slug || 'not-set-random-string' === $product_slug || 'not-set-random-string' === $cat_slug || 'not-set-random-string' === $tag_slug ) {
?>
@@ -165,14 +176,32 @@ public function admin_notices() {
admin page to update and save the options.', 'knowledgebase' ),
- esc_url( admin_url( 'edit.php?post_type=wz_knowledgebase&page=wzkb-settings' ) )
+ esc_html__( 'Knowledge Base settings for the slug have not been registered. Please visit the %s to update and save the options.', 'knowledgebase' ),
+ '' . esc_html__( 'admin page', 'knowledgebase' ) . ' '
);
?>
id && 'wzkb_product' === $current_screen->taxonomy ) { // Check for Products taxonomy admin screen.
+ $multi_product = (int) wzkb_get_option( 'multi_product', 0 );
+ if ( ! $multi_product ) { // Yoda condition: Only show if not enabled.
+ // translators: %s: Link to plugin settings page.
+ $settings_link = sprintf( '%s ', esc_url( admin_url( 'edit.php?post_type=wz_knowledgebase&page=wzkb-settings' ) ), esc_html__( 'plugin settings', 'knowledgebase' ) );
+ $message = sprintf(
+ esc_html__( 'The Products taxonomy is only available in multi-product mode. Please enable multi-product mode in the %s.', 'knowledgebase' ),
+ $settings_link
+ );
+ printf(
+ '',
+ wp_kses_post( $message )
+ );
+ }
+ }
}
/**
diff --git a/includes/admin/class-settings.php b/includes/admin/class-settings.php
index 1a6c9ac..1ee0384 100644
--- a/includes/admin/class-settings.php
+++ b/includes/admin/class-settings.php
@@ -385,7 +385,7 @@ public static function settings_general() {
'cache' => array(
'id' => 'cache',
'name' => esc_html__( 'Enable cache', 'knowledgebase' ),
- 'desc' => esc_html__( 'Cache the output of the WP_Query lookups to speed up retrieval of the knowledgebase. Recommended for large knowledge bases', 'knowledgebase' ),
+ 'desc' => esc_html__( 'Cache the output of the queries to speed up retrieval of the knowledgebase. Recommended for large knowledge bases', 'knowledgebase' ),
'type' => 'checkbox',
'options' => false,
),
@@ -478,7 +478,7 @@ public static function settings_output() {
'name' => esc_html__( 'Show article count', 'knowledgebase' ),
'desc' => esc_html__( 'If selected, the number of articles will be displayed in an orange circle next to the header. You can override the color by styling wzkb_section_count', 'knowledgebase' ),
'type' => 'checkbox',
- 'options' => false,
+ 'options' => true,
),
'show_excerpt' => array(
'id' => 'show_excerpt',
diff --git a/includes/admin/class-setup-wizard.php b/includes/admin/class-setup-wizard.php
new file mode 100644
index 0000000..c269cfb
--- /dev/null
+++ b/includes/admin/class-setup-wizard.php
@@ -0,0 +1,724 @@
+steps = array(
+ 'welcome' => array(
+ 'name' => esc_html__( 'Welcome', 'knowledgebase' ),
+ 'view' => array( $this, 'welcome_step' ),
+ 'handler' => array( $this, 'welcome_save' ),
+ ),
+ 'multi_product' => array(
+ 'name' => esc_html__( 'Multi-Product Mode', 'knowledgebase' ),
+ 'view' => array( $this, 'mode_step' ),
+ 'handler' => array( $this, 'mode_save' ),
+ ),
+ 'permalinks' => array(
+ 'name' => esc_html__( 'Permalinks', 'knowledgebase' ),
+ 'view' => array( $this, 'structure_step' ),
+ 'handler' => array( $this, 'structure_save' ),
+ ),
+ 'display' => array(
+ 'name' => esc_html__( 'Display', 'knowledgebase' ),
+ 'view' => array( $this, 'display_step' ),
+ 'handler' => array( $this, 'display_save' ),
+ ),
+ 'complete' => array(
+ 'name' => esc_html__( 'Ready!', 'knowledgebase' ),
+ 'view' => array( $this, 'complete_step' ),
+ 'handler' => '',
+ ),
+ );
+ }
+
+ /**
+ * Add admin menus/screens.
+ *
+ * @since 3.0.0
+ *
+ * @return void
+ */
+ public function admin_menus() {
+ add_submenu_page(
+ 'edit.php?post_type=wz_knowledgebase',
+ esc_html__( 'Knowledge Base Setup', 'knowledgebase' ),
+ esc_html__( 'Setup Wizard', 'knowledgebase' ),
+ 'manage_options',
+ 'wzkb-setup',
+ array( $this, 'render_wizard' )
+ );
+ }
+
+ /**
+ * Handle wizard logic (redirects, saves).
+ *
+ * @since 3.0.0
+ *
+ * @return void
+ */
+ public function setup_wizard() {
+ if ( empty( $_GET['page'] ) || 'wzkb-setup' !== $_GET['page'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ return;
+ }
+
+ if ( ! current_user_can( 'manage_options' ) ) {
+ wp_die( esc_html__( 'Sorry, you are not allowed to access this page.', 'knowledgebase' ) );
+ }
+
+ if ( empty( $this->steps ) ) {
+ $this->init_wizard_steps();
+ }
+
+ // Remove admin_footer button from post list if present (via Hook_Registry).
+ $main = \WebberZone\Knowledge_Base\wzkb();
+
+ // Ensure the admin property exists.
+ if ( isset( $main->admin ) ) {
+ $admin_instance = $main->admin;
+ Hook_Registry::remove_action( 'admin_footer', array( $admin_instance, 'maybe_add_button_to_post_list' ) );
+ }
+
+ // Initialize current step.
+ $this->current_step = isset( $_GET['step'] ) ? sanitize_key( $_GET['step'] ) : get_option( 'wzkb_setup_current_step', current( array_keys( $this->steps ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+
+ if ( ! isset( $this->steps[ $this->current_step ] ) ) {
+ $this->current_step = current( array_keys( $this->steps ) );
+ }
+
+ if ( isset( $_GET['wzkb_nonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['wzkb_nonce'] ) ), 'wzkb-setup' ) ) {
+ update_option( 'wzkb_setup_current_step', $this->current_step, false );
+ }
+
+ if ( ! empty( $_POST['wzkb_save_step'] ) && isset( $this->steps[ $this->current_step ]['handler'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
+ check_admin_referer( 'wzkb-setup' );
+ call_user_func( $this->steps[ $this->current_step ]['handler'] );
+ update_option( 'wzkb_setup_current_step', $this->get_next_step_link(), false );
+ }
+ }
+
+ /**
+ * Render the wizard page.
+ *
+ * @since 3.0.0
+ *
+ * @return void
+ */
+ public function render_wizard() {
+ if ( empty( $this->steps ) ) {
+ $this->init_wizard_steps();
+ }
+ ?>
+
+
+
+ setup_wizard_steps(); ?>
+ setup_wizard_content(); ?>
+
+ steps;
+ ?>
+
+ $step ) {
+ $is_completed = array_search( $this->current_step, array_keys( $this->steps ), true ) > array_search( $step_key, array_keys( $this->steps ), true );
+ $aria_current = $step_key === $this->current_step ? ' aria-current="step"' : '';
+ $class = $step_key === $this->current_step ? 'active' : ( $is_completed ? 'done' : '' );
+ ?>
+ >
+
+
+
+
+
+
+ steps[ $this->current_step ]['view'] ) ) {
+ call_user_func( $this->steps[ $this->current_step ]['view'] );
+ }
+ ?>
+
+
+
+
+
+
+
+
+
+ get_next_step_link() ) );
+ exit;
+ }
+
+ /**
+ * Mode step.
+ *
+ * @since 3.0.0
+ *
+ * @return void
+ */
+ public function mode_step() {
+ // Load existing multi-product setting.
+ $settings = wzkb_get_settings();
+ $multi = $settings['multi_product'] ?? 0;
+ $category_level = $settings['category_level'] ?? 2;
+ ?>
+
+
+
+ get_next_step_link() ) );
+ exit;
+ }
+
+ /**
+ * Permalinks step.
+ *
+ * @since 3.0.0
+ *
+ * @return void
+ */
+ public function structure_step() {
+ // Load all settings at once.
+ $settings = wzkb_get_settings();
+ $kb_slug = $settings['kb_slug'] ?? 'knowledgebase';
+ $product_slug = $settings['product_slug'] ?? 'kb/product';
+ $category_slug = $settings['category_slug'] ?? 'kb/section';
+ $tag_slug = $settings['tag_slug'] ?? 'kb/tags';
+ ?>
+
+
+ support/knowledgebase'
+ );
+ ?>
+
+
+ */
+ esc_html__( 'All the below slugs must be unique and not nested within each other. e.g. %1$s (Knowledge Base) and %2$s (Product) are NOT valid as the product slug is nested below the knowledgebase slug. This also applies to Sections and Tags.', 'knowledgebase' ),
+ 'kb',
+ 'kb/product'
+ );
+ ?>
+
+
+ isset( $_POST['kb_slug'] ) ? sanitize_text_field( wp_unslash( $_POST['kb_slug'] ) ) : 'knowledgebase',
+ 'product_slug' => isset( $_POST['product_slug'] ) ? sanitize_text_field( wp_unslash( $_POST['product_slug'] ) ) : 'kb/product',
+ 'category_slug' => isset( $_POST['category_slug'] ) ? sanitize_text_field( wp_unslash( $_POST['category_slug'] ) ) : 'kb/section',
+ 'tag_slug' => isset( $_POST['tag_slug'] ) ? sanitize_text_field( wp_unslash( $_POST['tag_slug'] ) ) : 'kb/tags',
+ );
+ $updated = wzkb_update_settings( $partial );
+ if ( $updated ) {
+ add_settings_error( 'wzkb_setup', 'permalinks_saved', esc_html__( 'Permalink settings saved successfully.', 'knowledgebase' ), 'success' );
+ flush_rewrite_rules();
+ } else {
+ add_settings_error( 'wzkb_setup', 'permalinks_save_failed', esc_html__( 'Failed to save permalink settings.', 'knowledgebase' ), 'error' );
+ }
+ wp_safe_redirect( esc_url_raw( $this->get_next_step_link() ) );
+ exit;
+ }
+
+ /**
+ * Display step.
+ *
+ * @since 3.0.0
+ *
+ * @return void
+ */
+ public function display_step() {
+ // Load all settings at once.
+ $settings = \wzkb_get_settings();
+ $kb_title = $settings['kb_title'] ?? '';
+ $show_article_count = $settings['show_article_count'] ?? 0;
+ $show_excerpt = $settings['show_excerpt'] ?? 0;
+ $clickable_section = $settings['clickable_section'] ?? 0;
+ $limit = $settings['limit'] ?? 5;
+ $show_related = $settings['show_related_articles'] ?? 0;
+ $cache = $settings['cache'] ?? 0;
+ $include_styles = $settings['include_styles'] ?? 0;
+ $columns = $settings['columns'] ?? 1;
+ ?>
+
+
+ isset( $_POST['kb_title'] ) ? sanitize_text_field( wp_unslash( $_POST['kb_title'] ) ) : '',
+ 'show_article_count' => isset( $_POST['show_article_count'] ) ? 1 : 0,
+ 'show_excerpt' => isset( $_POST['show_excerpt'] ) ? 1 : 0,
+ 'clickable_section' => isset( $_POST['clickable_section'] ) ? 1 : 0,
+ 'limit' => isset( $_POST['limit'] ) ? absint( wp_unslash( $_POST['limit'] ) ) : 5,
+ 'show_related_articles' => isset( $_POST['show_related_articles'] ) ? 1 : 0,
+ 'cache' => isset( $_POST['cache'] ) ? 1 : 0,
+ 'include_styles' => isset( $_POST['include_styles'] ) ? 1 : 0,
+ 'columns' => isset( $_POST['columns'] ) ? absint( wp_unslash( $_POST['columns'] ) ) : 1,
+ );
+ $updated = wzkb_update_settings( $partial );
+ if ( $updated ) {
+ add_settings_error( 'wzkb_setup', 'display_saved', esc_html__( 'Display settings saved successfully.', 'knowledgebase' ), 'success' );
+ } else {
+ add_settings_error( 'wzkb_setup', 'display_save_failed', esc_html__( 'Failed to save display settings.', 'knowledgebase' ), 'error' );
+ }
+ wp_safe_redirect( esc_url_raw( $this->get_next_step_link() ) );
+ exit;
+ }
+
+ /**
+ * Complete step.
+ *
+ * @since 3.0.0
+ *
+ * @return void
+ */
+ public function complete_step() {
+ update_option( 'wzkb_setup_completed', true );
+ update_option( 'wzkb_setup_current_step', '' );
+ $multi_product = \wzkb_get_option( 'multi_product' );
+ ?>
+
+
+
+
+
+
+
+
+ current_step;
+ }
+
+ $keys = array_keys( $this->steps );
+ if ( end( $keys ) === $step ) {
+ return admin_url( 'edit.php?post_type=wz_knowledgebase' );
+ }
+
+ $step_index = array_search( $step, $keys, true );
+ if ( false === $step_index ) {
+ return '';
+ }
+
+ return add_query_arg( 'step', $keys[ $step_index + 1 ], remove_query_arg( 'activate_error' ) );
+ }
+
+ /**
+ * Enqueue scripts and styles.
+ *
+ * @since 3.0.0
+ *
+ * @return void
+ */
+ public function enqueue_scripts() {
+ if ( empty( $_GET['page'] ) || 'wzkb-setup' !== $_GET['page'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ return;
+ }
+
+ wp_register_style(
+ 'wzkb-wizard',
+ plugins_url( 'css/wizard.css', __FILE__ ),
+ array( 'dashicons' ),
+ WZKB_VERSION
+ );
+ wp_enqueue_style( 'wzkb-wizard' );
+
+ wp_register_script(
+ 'wzkb-wizard',
+ plugins_url( 'js/wizard.js', __FILE__ ),
+ array( 'jquery' ),
+ WZKB_VERSION,
+ true
+ );
+ wp_localize_script(
+ 'wzkb-wizard',
+ 'wzkbWizard',
+ array(
+ 'skip_setup' => esc_html__( 'Are you sure you want to skip the setup wizard? You can return to it later. This will only skip the setup wizard if you have not completed it yet.', 'knowledgebase' ),
+ 'dashboard_url' => admin_url( 'edit.php?post_type=wz_knowledgebase' ),
+ )
+ );
+ wp_enqueue_script( 'wzkb-wizard' );
+ }
+}
diff --git a/includes/admin/css/wizard.css b/includes/admin/css/wizard.css
new file mode 100644
index 0000000..1acd633
--- /dev/null
+++ b/includes/admin/css/wizard.css
@@ -0,0 +1,167 @@
+/* Setup wizard styles */
+.wzkb-setup {
+ padding: 20px;
+ background: #fff;
+ border: 1px solid #c3c4c7;
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04);
+}
+
+/* Ensure content respects admin menu */
+#wpcontent .wzkb-setup {
+ margin-left: 0;
+ max-width: 960px;
+}
+
+.wzkb-setup h1 {
+ margin: 0 0 20px;
+ padding: 0;
+ color: #1d2327;
+ font-size: 2em;
+ line-height: 1.1em;
+}
+
+.wzkb-setup .wzkb-setup-steps {
+ padding: 0;
+ margin: 0 0 20px;
+ list-style: none;
+ display: flex;
+ flex-wrap: wrap;
+ color: #1d2327;
+}
+
+.wzkb-setup .wzkb-setup-steps li {
+ flex: 1;
+ text-align: center;
+ padding: 10px;
+ border-bottom: 4px solid #ccc;
+ line-height: 1.4em;
+}
+
+.wzkb-setup .wzkb-setup-steps li.active {
+ border-color: #2271b1;
+ color: #2271b1;
+}
+
+.wzkb-setup .wzkb-setup-steps li.done {
+ border-color: #2271b1;
+ color: #2271b1;
+}
+
+.wzkb-setup .wzkb-setup-content {
+ padding: 20px;
+ background: #fff;
+ border: 1px solid #c3c4c7;
+}
+
+.wzkb-setup .wzkb-setup-content p {
+ margin: 20px 0;
+ font-size: 1.1em;
+ line-height: 1.75em;
+ color: #0a0a0a;
+}
+
+.wzkb-setup .wzkb-setup-content table.form-table {
+ width: 100%;
+ border-top: 1px solid #f0f0f1;
+ margin-top: 0;
+}
+
+.wzkb-setup .wzkb-setup-content table th {
+ width: 35%;
+ padding: 20px;
+}
+
+.wzkb-setup .wzkb-setup-content table td {
+ padding: 20px;
+ vertical-align: top;
+}
+
+.wzkb-setup .wzkb-setup-content table td select,
+.wzkb-setup .wzkb-setup-content table td input[type="text"],
+.wzkb-setup .wzkb-setup-content table td input[type="number"],
+.wzkb-setup .wzkb-setup-content table td input[type="url"],
+.wzkb-setup .wzkb-setup-content table td input[type="email"],
+.wzkb-setup .wzkb-setup-content table td input:not([type="checkbox"]):not([type="radio"]) {
+ width: 100%;
+}
+
+.wzkb-setup .wzkb-setup-content table td .description {
+ font-size: 0.9em;
+ margin: 4px 0 0;
+ color: #646970;
+}
+
+.wzkb-url-prefix-row {
+ display: flex;
+ align-items: center;
+ gap: 0.5em;
+}
+
+.wzkb-url-prefix-text {
+ white-space: nowrap;
+ color: #000;
+ background-color: #f0f0f0;
+ font-size: 1em;
+ padding: 0.25em 0.5em;
+}
+
+.wzkb-url-slug-input {
+ flex: 1 1 auto;
+ min-width: 120px;
+ width: auto;
+}
+
+.wzkb-url-desc {
+ margin-left: 0;
+ padding-left: 0;
+}
+
+.wzkb-setup .wzkb-setup-actions {
+ margin: 20px 0 0;
+ padding-top: 10px;
+ border-top: 1px solid #f0f0f1;
+ text-align: right;
+}
+
+.wzkb-return-to-dashboard {
+ font-size: 1em;
+ color: #50575e;
+ margin: 1em 0;
+ display: block;
+ text-align: center;
+}
+
+.wzkb-setup-next-actions-horizontal {
+ display: flex;
+ flex-direction: row;
+ gap: 20px;
+ justify-content: flex-start;
+ align-items: center;
+ list-style: none;
+ padding: 0;
+ margin: 20px 0 0 0;
+}
+
+.wzkb-setup-next-actions-horizontal .setup-product {
+ margin: 0;
+ padding: 0;
+}
+
+.wzkb-setup-next-actions-horizontal .button {
+ min-width: 180px;
+ font-size: 1.1em;
+ text-align: center;
+}
+
+.wzkb-setup-next-actions-horizontal .button.button-large {
+ font-size: 1.2em;
+}
+
+.wzkb-setup-warning {
+ background-color: #fff599;
+ padding: 10px;
+ border: 1px solid #f7dc6f;
+ border-radius: 5px;
+ margin-bottom: 20px;
+ color: #0a0a0a;
+}
\ No newline at end of file
diff --git a/includes/admin/js/wizard.js b/includes/admin/js/wizard.js
new file mode 100644
index 0000000..af50d23
--- /dev/null
+++ b/includes/admin/js/wizard.js
@@ -0,0 +1,28 @@
+jQuery(function ($) {
+ 'use strict';
+
+ // Enable continue button when required fields are filled.
+ $('.wzkb-setup-content').on('change', 'input, select', function () {
+ var form = $(this).closest('form');
+ var continue_btn = form.find('.button-next');
+
+ var required_inputs = form.find('input[required], select[required]');
+ var is_complete = true;
+
+ required_inputs.each(function () {
+ if (!$(this).val()) {
+ is_complete = false;
+ return false;
+ }
+ });
+
+ continue_btn.prop('disabled', !is_complete);
+ });
+
+ // Confirm skip setup for "Not right now" button only.
+ $('#wzkb-not-now').on('click', function () {
+ if (confirm(wzkbWizard.skip_setup)) {
+ window.location.href = wzkbWizard.dashboard_url;
+ }
+ });
+});
\ No newline at end of file
diff --git a/includes/admin/settings/class-settings-api.php b/includes/admin/settings/class-settings-api.php
index f28d615..198dc52 100644
--- a/includes/admin/settings/class-settings-api.php
+++ b/includes/admin/settings/class-settings-api.php
@@ -18,7 +18,7 @@
/**
* Settings API wrapper class
*
- * @version 2.7.0
+ * @version 2.7.1
*/
class Settings_API {
@@ -27,7 +27,7 @@ class Settings_API {
*
* @var string
*/
- public const VERSION = '2.7.0';
+ public const VERSION = '2.7.1';
/**
* Settings Key.
@@ -518,6 +518,14 @@ public function admin_enqueue_scripts( $hook ) {
self::VERSION,
true
);
+ wp_localize_script(
+ "wz-{$this->prefix}-admin",
+ 'WZSettingsAdmin',
+ array(
+ 'prefix' => $this->prefix,
+ 'settings_key' => $this->settings_key,
+ )
+ );
if ( $hook === $this->settings_page ) {
$this->enqueue_scripts_styles();
@@ -965,7 +973,7 @@ public function show_form() {
ob_start();
?>
-