--- a/bfg-tools-extension-zipper/bfg-tools-extension-zipper.php
+++ b/bfg-tools-extension-zipper/bfg-tools-extension-zipper.php
@@ -1,363 +1,363 @@
-<?php
-/**
- * Plugin Name: BFG Tools – Extension Zipper
- * Description: Zip any installed extension (plugins on your site) into a downloadable .zip. Appears under BFG Tools → Extension Zipper. Zips are saved under /wp-content/uploads/extension-zips.
- * Version: 1.0.7
- * Author: The Bald Fat Guy
- * License: GPLv2 or later
- * License URI: https://www.gnu.org/licenses/gpl-2.0.html
- * Text Domain: bfg-tools-extension-zipper
- */
-
-if ( ! defined( 'ABSPATH' ) ) exit;
-
-/** ------------------------------------------------------------------------
- * Canonical constants (reviewer request: determine locations via helpers)
- * --------------------------------------------------------------------- */
-define( 'BFGTOEXZ_PLUGIN_FILE', __FILE__ );
-define( 'BFGTOEXZ_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
-define( 'BFGTOEXZ_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
-define( 'BFGTOEXZ_VERSION', '1.0.6' );
-
-/** A unique, prefixed hub slug for this plugin’s top-level menu */
-define( 'BFGTOEXZ_HUB_SLUG', 'bfgtoexz-tools' );
-
-/** ------------------------------------------------------------------------
- * Top-level “BFG Tools” hub (guarded + prefixed)
- * --------------------------------------------------------------------- */
-if ( ! function_exists( 'bfgtoexz_tools_register_menu' ) ) {
- function bfgtoexz_tools_register_menu() {
- $slug = BFGTOEXZ_HUB_SLUG; // unique/prefixed
-
- // Avoid duplicates if already created by this plugin
- if ( isset( $GLOBALS['admin_page_hooks'][ $slug ] ) ) { return; }
-
- add_menu_page(
- __( 'BFG Tools', 'bfg-tools-extension-zipper' ),
- __( 'BFG Tools', 'bfg-tools-extension-zipper' ),
- 'manage_options',
- $slug,
- 'bfgtoexz_tools_render_hub',
- 'dashicons-archive',
- 65
- );
- }
-
- function bfgtoexz_tools_render_hub() {
- if ( ! current_user_can( 'manage_options' ) ) {
- wp_die( esc_html__( 'You do not have permission.', 'bfg-tools-extension-zipper' ) );
- }
- if ( ! function_exists( 'is_plugin_active' ) ) {
- require_once ABSPATH . 'wp-admin/includes/plugin.php';
- }
-
- $cards = array();
-
- // General card
- $cards[] = array(
- 'title' => __( 'General', 'bfg-tools-extension-zipper' ),
- 'desc' => __( 'About BFG Tools and quick links.', 'bfg-tools-extension-zipper' ),
- 'url' => admin_url( 'admin.php?page=' . BFGTOEXZ_HUB_SLUG ),
- 'external' => false,
- );
-
- // Extension Zipper (this plugin)
- if ( function_exists( 'is_plugin_active' ) && is_plugin_active( plugin_basename( BFGTOEXZ_PLUGIN_FILE ) ) ) {
- $cards[] = array(
- 'title' => __( 'Extension Zipper', 'bfg-tools-extension-zipper' ),
- 'desc' => __( 'Create downloadable ZIPs of installed extensions.', 'bfg-tools-extension-zipper' ),
- 'url' => admin_url( 'admin.php?page=bfgtoexz-extension-zipper' ),
- 'external' => false,
- );
- }
-
- // Theme Zipper (if installed)
- if ( function_exists( 'is_plugin_active' ) && is_plugin_active( 'bfg-theme-zipper/bfg-theme-zipper.php' ) ) {
- $cards[] = array(
- 'title' => __( 'Theme Zipper', 'bfg-tools-extension-zipper' ),
- 'desc' => __( 'Package installed themes into ZIPs.', 'bfg-tools-extension-zipper' ),
- 'url' => admin_url( 'admin.php?page=bfg-theme-zipper' ),
- 'external' => false,
- );
- }
-
- $site_url = apply_filters( 'bfgtoexz_tools_website_url', 'https://thebaldfatguy.com' );
- $cards[] = array(
- 'title' => __( 'Visit BFG Website', 'bfg-tools-extension-zipper' ),
- 'desc' => __( 'More tools and updates.', 'bfg-tools-extension-zipper' ),
- 'url' => esc_url( $site_url ),
- 'external' => true,
- );
- ?>
- <div class="wrap">
- <h1 style="display:flex;align-items:center;gap:12px;margin-bottom:16px;">
- <span class="dashicons dashicons-archive"></span>
- <?php echo esc_html__( 'The Bald Fat Guy Tools', 'bfg-tools-extension-zipper' ); ?>
- </h1>
-
- <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:16px;">
- <?php foreach ( $cards as $c ) : ?>
- <div style="border:1px solid #dcdcde;border-radius:10px;padding:16px;background:#fff;">
- <h2 style="margin:0 0 8px;font-size:1.1rem;"><?php echo esc_html( $c['title'] ); ?></h2>
- <p style="margin:0 0 12px;color:#555;"><?php echo esc_html( $c['desc'] ); ?></p>
- <?php if ( ! empty( $c['external'] ) ) : ?>
- <a class="button button-secondary" href="<?php echo esc_url( $c['url'] ); ?>" target="_blank" rel="noopener"><?php esc_html_e( 'Open', 'bfg-tools-extension-zipper' ); ?></a>
- <?php else : ?>
- <a class="button" href="<?php echo esc_url( $c['url'] ); ?>"><?php esc_html_e( 'Open', 'bfg-tools-extension-zipper' ); ?></a>
- <?php endif; ?>
- </div>
- <?php endforeach; ?>
- </div>
-
- <p style="margin-top:24px;color:#666;"><?php echo esc_html__( 'Written by The Bald Fat Guy', 'bfg-tools-extension-zipper' ); ?></p>
- </div>
- <?php
- }
- add_action( 'admin_menu', 'bfgtoexz_tools_register_menu' );
-}
-
-/** ------------------------------------------------------------------------
- * Extension Zipper (prefixed, i18n fixed, safer paths)
- * --------------------------------------------------------------------- */
-if ( ! function_exists( 'get_plugins' ) ) {
- require_once ABSPATH . 'wp-admin/includes/plugin.php';
-}
-
-class BFGTOEXZ_Extension_Zipper {
- const PAGE = 'bfgtoexz-extension-zipper';
- const NONCE = 'bfgtoexz_nonce';
-
- public function __construct() {
- add_action( 'admin_menu', array( $this, 'menu' ) );
- add_action( 'admin_post_bfgtoexz_zip', array( $this, 'zip' ) );
- add_filter( 'plugin_action_links_' . plugin_basename( BFGTOEXZ_PLUGIN_FILE ), array( $this, 'links' ) );
- add_action( 'admin_notices', array( $this, 'notice' ) );
- }
-
- public function links( $links ) {
- $links[] = '<a href="' . esc_url( admin_url( 'admin.php?page=' . self::PAGE ) ) . '">' . esc_html__( 'Open Extension Zipper', 'bfg-tools-extension-zipper' ) . '</a>';
- return $links;
- }
-
- public function menu() {
- add_submenu_page(
- BFGTOEXZ_HUB_SLUG,
- __( 'Extension Zipper', 'bfg-tools-extension-zipper' ),
- __( 'Extension Zipper', 'bfg-tools-extension-zipper' ),
- 'manage_options',
- self::PAGE,
- array( $this, 'page' )
- );
- }
-
- private function can_zip() { return class_exists( 'ZipArchive' ); }
-
- /** Group plugins by top-level folder and keep first file path for proper plugin_dir_path() */
- private function list_plugins_grouped() {
- $all = get_plugins();
- $by = array();
- foreach ( $all as $file => $data ) {
- // $file example: 'akismet/akismet.php'
- $parts = explode( '/', $file );
- $dir = $parts[0];
- if ( ! $dir ) { continue; }
-
- if ( ! isset( $by[ $dir ] ) ) {
- $by[ $dir ] = array(
- 'dir' => $dir,
- 'name' => ! empty( $data['Name'] ) ? $data['Name'] : $dir,
- 'version'=> ! empty( $data['Version'] ) ? $data['Version'] : '',
- 'first' => $file, // keep first file we see; good enough to resolve path
- );
- }
- }
- ksort( $by );
- return $by;
- }
-
- public function notice() {
- if ( ! current_user_can( 'manage_options' ) ) { return; }
- $key = 'bfgtoexz_msg_' . get_current_user_id();
- $msg = get_transient( $key );
- if ( $msg ) {
- delete_transient( $key );
- echo '<div class="notice notice-success is-dismissible"><p>' . esc_html( $msg ) . '</p></div>';
- }
- }
-
- public function page() {
- if ( ! current_user_can( 'manage_options' ) ) {
- wp_die( esc_html__( 'No permission', 'bfg-tools-extension-zipper' ) );
- }
-
- $plugins = $this->list_plugins_grouped();
- $uploads = wp_upload_dir();
- $target_dir_fs = trailingslashit( $uploads['basedir'] ) . 'extension-zips';
- $target_dir_url = trailingslashit( $uploads['baseurl'] ) . 'extension-zips';
- if ( ! file_exists( $target_dir_fs ) ) { wp_mkdir_p( $target_dir_fs ); }
- $can = $this->can_zip(); ?>
-
- <div class="wrap">
- <div style="margin:8px 0 16px;">
- <a class="button" href="<?php echo esc_url( admin_url( 'admin.php?page=' . BFGTOEXZ_HUB_SLUG ) ); ?>">← <?php echo esc_html__( 'Back to BFG Tools', 'bfg-tools-extension-zipper' ); ?></a>
- </div>
-
- <h1><?php echo esc_html__( 'BFG Extension Zipper', 'bfg-tools-extension-zipper' ); ?></h1>
- <p>
- <?php echo esc_html__( 'Creates .zip files and saves them to:', 'bfg-tools-extension-zipper' ); ?>
- <code><?php echo esc_html( $target_dir_fs ); ?></code><br/>
- <?php echo esc_html__( 'Zip URL base:', 'bfg-tools-extension-zipper' ); ?>
- <code><?php echo esc_url( $target_dir_url ); ?></code>
- </p>
-
- <?php if ( ! $can ): ?>
- <div class="notice notice-error"><p><?php esc_html_e( 'ZipArchive extension is required.', 'bfg-tools-extension-zipper' ); ?></p></div>
- <?php endif; ?>
-
- <table class="widefat striped">
- <thead>
- <tr>
- <th><?php esc_html_e( 'Extension', 'bfg-tools-extension-zipper' ); ?></th>
- <th><?php esc_html_e( 'Version', 'bfg-tools-extension-zipper' ); ?></th>
- <th><?php esc_html_e( 'Folder', 'bfg-tools-extension-zipper' ); ?></th>
- <th><?php esc_html_e( 'Actions', 'bfg-tools-extension-zipper' ); ?></th>
- </tr>
- </thead>
- <tbody>
- <?php foreach ( $plugins as $dir => $info ) :
- $name = $info['name'];
- $ver = $info['version'];
- $first = $info['first']; // e.g. 'akismet/akismet.php'
- $base = sanitize_title( $name ? $name : $dir );
- $ver_part = $ver ? sanitize_title( $ver ) : ( 'v' . gmdate( 'Ymd-His' ) );
- $zip_name = $base . '-' . $ver_part . '.zip';
-
- $zip_path = trailingslashit( $target_dir_fs ) . $zip_name;
- $zip_url = trailingslashit( $target_dir_url ) . $zip_name; ?>
- <tr>
- <td><?php echo esc_html( $name ); ?></td>
- <td><?php echo esc_html( $ver ? $ver : '—' ); ?></td>
- <td><code><?php echo esc_html( $dir ); ?></code></td>
- <td>
- <?php if ( $can ): ?>
- <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" style="display:inline-block;margin-right:8px;">
- <?php wp_nonce_field( self::NONCE ); ?>
- <input type="hidden" name="action" value="bfgtoexz_zip">
- <input type="hidden" name="plugin_dir" value="<?php echo esc_attr( $dir ); ?>">
- <input type="hidden" name="first_file" value="<?php echo esc_attr( $first ); ?>">
- <input type="hidden" name="zip_name" value="<?php echo esc_attr( $zip_name ); ?>">
- <button class="button button-primary"><?php esc_html_e( 'Create Zip', 'bfg-tools-extension-zipper' ); ?></button>
- </form>
- <?php endif; ?>
-
- <?php if ( file_exists( $zip_path ) ): ?>
- <a class="button" href="<?php echo esc_url( $zip_url ); ?>" target="_blank" rel="noopener">
- <?php esc_html_e( 'Download Latest Zip', 'bfg-tools-extension-zipper' ); ?>
- </a>
- <?php else: ?>
- <span class="description"><?php esc_html_e( 'No zip yet', 'bfg-tools-extension-zipper' ); ?></span>
- <?php endif; ?>
- </td>
- </tr>
- <?php endforeach; ?>
- </tbody>
- </table>
-
- <p class="description"><?php esc_html_e( 'Excludes: .git, .svn, node_modules, .DS_Store, *.log', 'bfg-tools-extension-zipper' ); ?></p>
- </div>
- <?php
- }
-
- public function zip() {
- if ( ! current_user_can( 'manage_options' ) ) {
- wp_die( esc_html__( 'No permission', 'bfg-tools-extension-zipper' ) );
- }
- check_admin_referer( self::NONCE );
-
- $dir = isset( $_POST['plugin_dir'] ) ? sanitize_text_field( wp_unslash( $_POST['plugin_dir'] ) ) : '';
- $first = isset( $_POST['first_file'] ) ? sanitize_text_field( wp_unslash( $_POST['first_file'] ) ) : '';
- $zipn = isset( $_POST['zip_name'] ) ? sanitize_file_name( wp_unslash( $_POST['zip_name'] ) ) : '';
- if ( ! $dir || ! $first || ! $zipn ) {
- wp_die( esc_html__( 'Missing parameters', 'bfg-tools-extension-zipper' ) );
- }
-
- /**
- * Derive the /plugins/ dir from this plugin's path (no WP_PLUGIN_DIR).
- * plugin_dir_path(__FILE__) => .../plugins/this-plugin/
- * dirname() gives .../plugins/
- */
- $plugins_dir = trailingslashit( dirname( BFGTOEXZ_PLUGIN_DIR ) ); // ends with /plugins/
- $abs_first = $plugins_dir . ltrim( $first, '/' ); // plugin-slug/main-file.php
- $src = plugin_dir_path( $abs_first ); // .../plugins/plugin-slug/
-
- if ( ! $src || ! file_exists( $src ) || ! is_dir( $src ) ) {
- wp_die( esc_html__( 'Extension folder not found', 'bfg-tools-extension-zipper' ) );
- }
-
- $uploads = wp_upload_dir();
- $dest = trailingslashit( $uploads['basedir'] ) . 'extension-zips';
- if ( ! file_exists( $dest ) ) { wp_mkdir_p( $dest ); }
- $zip_path = trailingslashit( $dest ) . $zipn;
-
- if ( ! class_exists( 'ZipArchive' ) ) {
- wp_die( esc_html__( 'ZipArchive not available', 'bfg-tools-extension-zipper' ) );
- }
- $zip = new ZipArchive();
- if ( true !== $zip->open( $zip_path, ZipArchive::CREATE | ZipArchive::OVERWRITE ) ) {
- wp_die( esc_html__( 'Unable to create zip', 'bfg-tools-extension-zipper' ) );
- }
-
- $exclude_dirs = array( '.git','.svn','.github','.idea','node_modules','vendor/bin','__MACOSX' );
- $exclude_exts = array( 'log','map' );
-
- $src = wp_normalize_path( $src );
- $len = strlen( $src );
-
-// Derive the actual plugin folder name dynamically.
-// Example: /plugins/woocommerce/ → zip root "woocommerce"
-$src_parts = explode('/', trim(str_replace(trailingslashit($plugins_dir), '', $src), '/'));
-$zip_root = !empty($src_parts[0]) ? $src_parts[0] : basename($src);
-
- $it = new RecursiveIteratorIterator(
- new RecursiveDirectoryIterator( $src, FilesystemIterator::SKIP_DOTS ),
- RecursiveIteratorIterator::SELF_FIRST
- );
-
- foreach ( $it as $fi ) {
- $fp = wp_normalize_path( $fi->getPathname() );
- $rp = ltrim( substr( $fp, $len ), '/' );
- $ext = pathinfo( $rp, PATHINFO_EXTENSION );
-
- // Exclude directories and certain extensions
- foreach ( $exclude_dirs as $d ) {
- $d = trim( $d, '/' );
- if ( strpos( $rp, $d . '/' ) === 0 || $rp === $d ) {
- continue 2;
- }
- }
- if ( $ext && in_array( strtolower( $ext ), $exclude_exts, true ) ) {
- continue;
- }
-
- if ( $fi->isDir() ) {
- $zip->addEmptyDir( $zip_root . '/' . $rp );
- } else {
- $zip->addFile( $fp, $zip_root . '/' . $rp );
- }
- }
- $zip->close();
-
- /* translators: %s: generated ZIP filename. */
- $msg = sprintf( __( 'Zip created: %s', 'bfg-tools-extension-zipper' ), basename( $zip_path ) );
-
- set_transient(
- 'bfgtoexz_msg_' . get_current_user_id(),
- $msg,
- 60
- );
-
- wp_safe_redirect( add_query_arg( array( 'page' => self::PAGE ), admin_url( 'admin.php' ) ) );
- exit;
- }
-}
-new BFGTOEXZ_Extension_Zipper();
+<?php
+/**
+ * Plugin Name: BFG Tools – Extension Zipper
+ * Description: Zip any installed extension (plugins on your site) into a downloadable .zip. Appears under BFG Tools → Extension Zipper. Zips are saved under /wp-content/uploads/extension-zips.
+ * Version: 1.0.8
+ * Author: The Bald Fat Guy
+ * License: GPLv2 or later
+ * License URI: https://www.gnu.org/licenses/gpl-2.0.html
+ * Text Domain: bfg-tools-extension-zipper
+ */
+
+if ( ! defined( 'ABSPATH' ) ) exit;
+
+/** ------------------------------------------------------------------------
+ * Canonical constants (reviewer request: determine locations via helpers)
+ * --------------------------------------------------------------------- */
+define( 'BFGTOEXZ_PLUGIN_FILE', __FILE__ );
+define( 'BFGTOEXZ_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
+define( 'BFGTOEXZ_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
+define( 'BFGTOEXZ_VERSION', '1.0.6' );
+
+/** A unique, prefixed hub slug for this plugin’s top-level menu */
+define( 'BFGTOEXZ_HUB_SLUG', 'bfgtoexz-tools' );
+
+/** ------------------------------------------------------------------------
+ * Top-level “BFG Tools” hub (guarded + prefixed)
+ * --------------------------------------------------------------------- */
+if ( ! function_exists( 'bfgtoexz_tools_register_menu' ) ) {
+ function bfgtoexz_tools_register_menu() {
+ $slug = BFGTOEXZ_HUB_SLUG; // unique/prefixed
+
+ // Avoid duplicates if already created by this plugin
+ if ( isset( $GLOBALS['admin_page_hooks'][ $slug ] ) ) { return; }
+
+ add_menu_page(
+ __( 'BFG Tools', 'bfg-tools-extension-zipper' ),
+ __( 'BFG Tools', 'bfg-tools-extension-zipper' ),
+ 'manage_options',
+ $slug,
+ 'bfgtoexz_tools_render_hub',
+ 'dashicons-archive',
+ 65
+ );
+ }
+
+ function bfgtoexz_tools_render_hub() {
+ if ( ! current_user_can( 'manage_options' ) ) {
+ wp_die( esc_html__( 'You do not have permission.', 'bfg-tools-extension-zipper' ) );
+ }
+ if ( ! function_exists( 'is_plugin_active' ) ) {
+ require_once ABSPATH . 'wp-admin/includes/plugin.php';
+ }
+
+ $cards = array();
+
+ // General card
+ $cards[] = array(
+ 'title' => __( 'General', 'bfg-tools-extension-zipper' ),
+ 'desc' => __( 'About BFG Tools and quick links.', 'bfg-tools-extension-zipper' ),
+ 'url' => admin_url( 'admin.php?page=' . BFGTOEXZ_HUB_SLUG ),
+ 'external' => false,
+ );
+
+ // Extension Zipper (this plugin)
+ if ( function_exists( 'is_plugin_active' ) && is_plugin_active( plugin_basename( BFGTOEXZ_PLUGIN_FILE ) ) ) {
+ $cards[] = array(
+ 'title' => __( 'Extension Zipper', 'bfg-tools-extension-zipper' ),
+ 'desc' => __( 'Create downloadable ZIPs of installed extensions.', 'bfg-tools-extension-zipper' ),
+ 'url' => admin_url( 'admin.php?page=bfgtoexz-extension-zipper' ),
+ 'external' => false,
+ );
+ }
+
+ // Theme Zipper (if installed)
+ if ( function_exists( 'is_plugin_active' ) && is_plugin_active( 'bfg-theme-zipper/bfg-theme-zipper.php' ) ) {
+ $cards[] = array(
+ 'title' => __( 'Theme Zipper', 'bfg-tools-extension-zipper' ),
+ 'desc' => __( 'Package installed themes into ZIPs.', 'bfg-tools-extension-zipper' ),
+ 'url' => admin_url( 'admin.php?page=bfg-theme-zipper' ),
+ 'external' => false,
+ );
+ }
+
+ $site_url = apply_filters( 'bfgtoexz_tools_website_url', 'https://thebaldfatguy.com' );
+ $cards[] = array(
+ 'title' => __( 'Visit BFG Website', 'bfg-tools-extension-zipper' ),
+ 'desc' => __( 'More tools and updates.', 'bfg-tools-extension-zipper' ),
+ 'url' => esc_url( $site_url ),
+ 'external' => true,
+ );
+ ?>
+ <div class="wrap">
+ <h1 style="display:flex;align-items:center;gap:12px;margin-bottom:16px;">
+ <span class="dashicons dashicons-archive"></span>
+ <?php echo esc_html__( 'The Bald Fat Guy Tools', 'bfg-tools-extension-zipper' ); ?>
+ </h1>
+
+ <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:16px;">
+ <?php foreach ( $cards as $c ) : ?>
+ <div style="border:1px solid #dcdcde;border-radius:10px;padding:16px;background:#fff;">
+ <h2 style="margin:0 0 8px;font-size:1.1rem;"><?php echo esc_html( $c['title'] ); ?></h2>
+ <p style="margin:0 0 12px;color:#555;"><?php echo esc_html( $c['desc'] ); ?></p>
+ <?php if ( ! empty( $c['external'] ) ) : ?>
+ <a class="button button-secondary" href="<?php echo esc_url( $c['url'] ); ?>" target="_blank" rel="noopener"><?php esc_html_e( 'Open', 'bfg-tools-extension-zipper' ); ?></a>
+ <?php else : ?>
+ <a class="button" href="<?php echo esc_url( $c['url'] ); ?>"><?php esc_html_e( 'Open', 'bfg-tools-extension-zipper' ); ?></a>
+ <?php endif; ?>
+ </div>
+ <?php endforeach; ?>
+ </div>
+
+ <p style="margin-top:24px;color:#666;"><?php echo esc_html__( 'Written by The Bald Fat Guy', 'bfg-tools-extension-zipper' ); ?></p>
+ </div>
+ <?php
+ }
+ add_action( 'admin_menu', 'bfgtoexz_tools_register_menu' );
+}
+
+/** ------------------------------------------------------------------------
+ * Extension Zipper (prefixed, i18n fixed, safer paths)
+ * --------------------------------------------------------------------- */
+if ( ! function_exists( 'get_plugins' ) ) {
+ require_once ABSPATH . 'wp-admin/includes/plugin.php';
+}
+
+class BFGTOEXZ_Extension_Zipper {
+ const PAGE = 'bfgtoexz-extension-zipper';
+ const NONCE = 'bfgtoexz_nonce';
+
+ public function __construct() {
+ add_action( 'admin_menu', array( $this, 'menu' ) );
+ add_action( 'admin_post_bfgtoexz_zip', array( $this, 'zip' ) );
+ add_filter( 'plugin_action_links_' . plugin_basename( BFGTOEXZ_PLUGIN_FILE ), array( $this, 'links' ) );
+ add_action( 'admin_notices', array( $this, 'notice' ) );
+ }
+
+ public function links( $links ) {
+ $links[] = '<a href="' . esc_url( admin_url( 'admin.php?page=' . self::PAGE ) ) . '">' . esc_html__( 'Open Extension Zipper', 'bfg-tools-extension-zipper' ) . '</a>';
+ return $links;
+ }
+
+ public function menu() {
+ add_submenu_page(
+ BFGTOEXZ_HUB_SLUG,
+ __( 'Extension Zipper', 'bfg-tools-extension-zipper' ),
+ __( 'Extension Zipper', 'bfg-tools-extension-zipper' ),
+ 'manage_options',
+ self::PAGE,
+ array( $this, 'page' )
+ );
+ }
+
+ private function can_zip() { return class_exists( 'ZipArchive' ); }
+
+ /** Group plugins by top-level folder and keep first file path for proper plugin_dir_path() */
+ private function list_plugins_grouped() {
+ $all = get_plugins();
+ $by = array();
+ foreach ( $all as $file => $data ) {
+ // $file example: 'akismet/akismet.php'
+ $parts = explode( '/', $file );
+ $dir = $parts[0];
+ if ( ! $dir ) { continue; }
+
+ if ( ! isset( $by[ $dir ] ) ) {
+ $by[ $dir ] = array(
+ 'dir' => $dir,
+ 'name' => ! empty( $data['Name'] ) ? $data['Name'] : $dir,
+ 'version'=> ! empty( $data['Version'] ) ? $data['Version'] : '',
+ 'first' => $file, // keep first file we see; good enough to resolve path
+ );
+ }
+ }
+ ksort( $by );
+ return $by;
+ }
+
+ public function notice() {
+ if ( ! current_user_can( 'manage_options' ) ) { return; }
+ $key = 'bfgtoexz_msg_' . get_current_user_id();
+ $msg = get_transient( $key );
+ if ( $msg ) {
+ delete_transient( $key );
+ echo '<div class="notice notice-success is-dismissible"><p>' . esc_html( $msg ) . '</p></div>';
+ }
+ }
+
+ public function page() {
+ if ( ! current_user_can( 'manage_options' ) ) {
+ wp_die( esc_html__( 'No permission', 'bfg-tools-extension-zipper' ) );
+ }
+
+ $plugins = $this->list_plugins_grouped();
+ $uploads = wp_upload_dir();
+ $target_dir_fs = trailingslashit( $uploads['basedir'] ) . 'extension-zips';
+ $target_dir_url = trailingslashit( $uploads['baseurl'] ) . 'extension-zips';
+ if ( ! file_exists( $target_dir_fs ) ) { wp_mkdir_p( $target_dir_fs ); }
+ $can = $this->can_zip(); ?>
+
+ <div class="wrap">
+ <div style="margin:8px 0 16px;">
+ <a class="button" href="<?php echo esc_url( admin_url( 'admin.php?page=' . BFGTOEXZ_HUB_SLUG ) ); ?>">← <?php echo esc_html__( 'Back to BFG Tools', 'bfg-tools-extension-zipper' ); ?></a>
+ </div>
+
+ <h1><?php echo esc_html__( 'BFG Extension Zipper', 'bfg-tools-extension-zipper' ); ?></h1>
+ <p>
+ <?php echo esc_html__( 'Creates .zip files and saves them to:', 'bfg-tools-extension-zipper' ); ?>
+ <code><?php echo esc_html( $target_dir_fs ); ?></code><br/>
+ <?php echo esc_html__( 'Zip URL base:', 'bfg-tools-extension-zipper' ); ?>
+ <code><?php echo esc_url( $target_dir_url ); ?></code>
+ </p>
+
+ <?php if ( ! $can ): ?>
+ <div class="notice notice-error"><p><?php esc_html_e( 'ZipArchive extension is required.', 'bfg-tools-extension-zipper' ); ?></p></div>
+ <?php endif; ?>
+
+ <table class="widefat striped">
+ <thead>
+ <tr>
+ <th><?php esc_html_e( 'Extension', 'bfg-tools-extension-zipper' ); ?></th>
+ <th><?php esc_html_e( 'Version', 'bfg-tools-extension-zipper' ); ?></th>
+ <th><?php esc_html_e( 'Folder', 'bfg-tools-extension-zipper' ); ?></th>
+ <th><?php esc_html_e( 'Actions', 'bfg-tools-extension-zipper' ); ?></th>
+ </tr>
+ </thead>
+ <tbody>
+ <?php foreach ( $plugins as $dir => $info ) :
+ $name = $info['name'];
+ $ver = $info['version'];
+ $first = $info['first']; // e.g. 'akismet/akismet.php'
+ $base = sanitize_title( $name ? $name : $dir );
+ $ver_part = $ver ? sanitize_title( $ver ) : ( 'v' . gmdate( 'Ymd-His' ) );
+ $zip_name = $base . '-' . $ver_part . '.zip';
+
+ $zip_path = trailingslashit( $target_dir_fs ) . $zip_name;
+ $zip_url = trailingslashit( $target_dir_url ) . $zip_name; ?>
+ <tr>
+ <td><?php echo esc_html( $name ); ?></td>
+ <td><?php echo esc_html( $ver ? $ver : '—' ); ?></td>
+ <td><code><?php echo esc_html( $dir ); ?></code></td>
+ <td>
+ <?php if ( $can ): ?>
+ <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" style="display:inline-block;margin-right:8px;">
+ <?php wp_nonce_field( self::NONCE ); ?>
+ <input type="hidden" name="action" value="bfgtoexz_zip">
+ <input type="hidden" name="plugin_dir" value="<?php echo esc_attr( $dir ); ?>">
+ <input type="hidden" name="first_file" value="<?php echo esc_attr( $first ); ?>">
+ <input type="hidden" name="zip_name" value="<?php echo esc_attr( $zip_name ); ?>">
+ <button class="button button-primary"><?php esc_html_e( 'Create Zip', 'bfg-tools-extension-zipper' ); ?></button>
+ </form>
+ <?php endif; ?>
+
+ <?php if ( file_exists( $zip_path ) ): ?>
+ <a class="button" href="<?php echo esc_url( $zip_url ); ?>" target="_blank" rel="noopener">
+ <?php esc_html_e( 'Download Latest Zip', 'bfg-tools-extension-zipper' ); ?>
+ </a>
+ <?php else: ?>
+ <span class="description"><?php esc_html_e( 'No zip yet', 'bfg-tools-extension-zipper' ); ?></span>
+ <?php endif; ?>
+ </td>
+ </tr>
+ <?php endforeach; ?>
+ </tbody>
+ </table>
+
+ <p class="description"><?php esc_html_e( 'Excludes: .git, .svn, node_modules, .DS_Store, *.log', 'bfg-tools-extension-zipper' ); ?></p>
+ </div>
+ <?php
+ }
+
+ public function zip() {
+ if ( ! current_user_can( 'manage_options' ) ) {
+ wp_die( esc_html__( 'No permission', 'bfg-tools-extension-zipper' ) );
+ }
+ check_admin_referer( self::NONCE );
+
+ $dir = isset( $_POST['plugin_dir'] ) ? sanitize_text_field( wp_unslash( $_POST['plugin_dir'] ) ) : '';
+ $first = isset( $_POST['first_file'] ) ? sanitize_text_field( wp_unslash( $_POST['first_file'] ) ) : '';
+ $zipn = isset( $_POST['zip_name'] ) ? sanitize_file_name( wp_unslash( $_POST['zip_name'] ) ) : '';
+ if ( ! $dir || ! $first || ! $zipn ) {
+ wp_die( esc_html__( 'Missing parameters', 'bfg-tools-extension-zipper' ) );
+ }
+
+ /**
+ * Derive the /plugins/ dir from this plugin's path (no WP_PLUGIN_DIR).
+ * plugin_dir_path(__FILE__) => .../plugins/this-plugin/
+ * dirname() gives .../plugins/
+ */
+ $plugins_dir = trailingslashit( dirname( BFGTOEXZ_PLUGIN_DIR ) ); // ends with /plugins/
+ $abs_first = $plugins_dir . ltrim( $first, '/' ); // plugin-slug/main-file.php
+ $src = plugin_dir_path( $abs_first ); // .../plugins/plugin-slug/
+
+ if ( ! $src || ! file_exists( $src ) || ! is_dir( $src ) ) {
+ wp_die( esc_html__( 'Extension folder not found', 'bfg-tools-extension-zipper' ) );
+ }
+
+ $uploads = wp_upload_dir();
+ $dest = trailingslashit( $uploads['basedir'] ) . 'extension-zips';
+ if ( ! file_exists( $dest ) ) { wp_mkdir_p( $dest ); }
+ $zip_path = trailingslashit( $dest ) . $zipn;
+
+ if ( ! class_exists( 'ZipArchive' ) ) {
+ wp_die( esc_html__( 'ZipArchive not available', 'bfg-tools-extension-zipper' ) );
+ }
+ $zip = new ZipArchive();
+ if ( true !== $zip->open( $zip_path, ZipArchive::CREATE | ZipArchive::OVERWRITE ) ) {
+ wp_die( esc_html__( 'Unable to create zip', 'bfg-tools-extension-zipper' ) );
+ }
+
+ $exclude_dirs = array( '.git','.svn','.github','.idea','node_modules','vendor/bin','__MACOSX' );
+ $exclude_exts = array( 'log','map' );
+
+ $src = wp_normalize_path( $src );
+ $len = strlen( $src );
+
+// Derive the actual plugin folder name dynamically.
+// Example: /plugins/woocommerce/ → zip root "woocommerce"
+$src_parts = explode('/', trim(str_replace(trailingslashit($plugins_dir), '', $src), '/'));
+$zip_root = !empty($src_parts[0]) ? $src_parts[0] : basename($src);
+
+ $it = new RecursiveIteratorIterator(
+ new RecursiveDirectoryIterator( $src, FilesystemIterator::SKIP_DOTS ),
+ RecursiveIteratorIterator::SELF_FIRST
+ );
+
+ foreach ( $it as $fi ) {
+ $fp = wp_normalize_path( $fi->getPathname() );
+ $rp = ltrim( substr( $fp, $len ), '/' );
+ $ext = pathinfo( $rp, PATHINFO_EXTENSION );
+
+ // Exclude directories and certain extensions
+ foreach ( $exclude_dirs as $d ) {
+ $d = trim( $d, '/' );
+ if ( strpos( $rp, $d . '/' ) === 0 || $rp === $d ) {
+ continue 2;
+ }
+ }
+ if ( $ext && in_array( strtolower( $ext ), $exclude_exts, true ) ) {
+ continue;
+ }
+
+ if ( $fi->isDir() ) {
+ $zip->addEmptyDir( $zip_root . '/' . $rp );
+ } else {
+ $zip->addFile( $fp, $zip_root . '/' . $rp );
+ }
+ }
+ $zip->close();
+
+ /* translators: %s: generated ZIP filename. */
+ $msg = sprintf( __( 'Zip created: %s', 'bfg-tools-extension-zipper' ), basename( $zip_path ) );
+
+ set_transient(
+ 'bfgtoexz_msg_' . get_current_user_id(),
+ $msg,
+ 60
+ );
+
+ wp_safe_redirect( add_query_arg( array( 'page' => self::PAGE ), admin_url( 'admin.php' ) ) );
+ exit;
+ }
+}
+new BFGTOEXZ_Extension_Zipper();