Plugin Directory

Changeset 3726427


Ignore:
Timestamp:
10/03/2026 03:26:42 PM (2 weeks ago)
Author:
soulja
Message:

Release 4.3.32

Location:
omnidirectory/trunk
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • omnidirectory/trunk/omnidirectory.php

    r3717915 r3726427  
    11<?php
    22/**
    3  * Plugin Name: OmniDirectory
     3 * Plugin Name: OmniDirectory – Directory & Listings
    44 * Plugin URI:  https://wordpress.org/plugins/omnidirectory/
    5  * Description: A universal, multi-purpose directory plugin — adaptable for Doctors, Lawyers, Companies, Clergy, or any professional listing. Registers an "omni_member" post type with admin-configurable field labels, a live AJAX search shortcode [omnidirectory_search], language-auto-detecting search, and Polylang/WPML compatibility.
    6  * Version:     4.3.31
     5 * Description: A flexible WordPress directory plugin for staff, member, and business listings, with multilingual, Arabic-aware search and RTL support.
     6 * Version:     4.3.32
    77 * Author:      Kerio
    88 * Text Domain: omnidirectory
    … …  
    256256        const IMAGE_MAX_DOWNLOAD_BYTES = 5242880; // 5 MB.
    257257
    258         const VERSION        = '4.3.31'; // single source of truth for enqueue versioning
     258        const VERSION        = '4.3.32'; // single source of truth for enqueue versioning
    259259
    260260        /**
    … …  
    14881488         */
    14891489        private function get_import_column_aliases() {
    1490                 return array(
    1491                         'name'              => array( 'title', 'full name', 'member name' ),
    1492                         'category'          => array( 'categories', 'directory category' ),
    1493                         'field1'            => array( 'field 1', 'specialty', 'specialty / title', 'job title', 'title / specialty' ),
    1494                         'field2'            => array( 'field 2', 'location', 'location / office', 'address' ),
    1495                         'field3'            => array( 'field 3' ),
    1496                         'field4'            => array( 'field 4' ),
    1497                         'field5'            => array( 'field 5' ),
    1498                         'phone'             => array( 'phone', 'phone number', 'telephone' ),
    1499                         'email'             => array( 'email address' ),
    1500                         'bio'               => array( 'biography', 'description', 'content' ),
    1501                         'gallery_shortcode' => array( 'gallery' ),
    1502                         'featured_image'    => array( 'image', 'photo', 'photo url', 'image url', 'avatar' ),
    1503                         'language'          => array( 'lang', 'locale' ),
     1490                /**
     1491                 * Let a companion plugin add its own CSV header aliases (internal
     1492                 * key => alternate header spellings) to this same column map —
     1493                 * e.g. OmniDirectory Pro's own Dynamic Fields, via headers like
     1494                 * "dynamic_{field_id}". Empty additions by default.
     1495                 *
     1496                 * @param array<string,string[]> $aliases
     1497                 */
     1498                return apply_filters(
     1499                        'omnidirectory_csv_import_aliases',
     1500                        array(
     1501                                'name'              => array( 'title', 'full name', 'member name' ),
     1502                                'category'          => array( 'categories', 'directory category' ),
     1503                                'field1'            => array( 'field 1', 'specialty', 'specialty / title', 'job title', 'title / specialty' ),
     1504                                'field2'            => array( 'field 2', 'location', 'location / office', 'address' ),
     1505                                'field3'            => array( 'field 3' ),
     1506                                'field4'            => array( 'field 4' ),
     1507                                'field5'            => array( 'field 5' ),
     1508                                'phone'             => array( 'phone', 'phone number', 'telephone' ),
     1509                                'email'             => array( 'email address' ),
     1510                                'bio'               => array( 'biography', 'description', 'content' ),
     1511                                'gallery_shortcode' => array( 'gallery' ),
     1512                                'featured_image'    => array( 'image', 'photo', 'photo url', 'image url', 'avatar' ),
     1513                                'language'          => array( 'lang', 'locale' ),
     1514                        )
    15041515                );
    15051516        }
    … …  
    28972908                                        // special here, but this stays correct regardless of
    28982909                                        // the exact id format).
     2910                                        // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- part of the staged import swap process (publishing phase); the result set is bounded by the current import batch size ($batch_size, 50 by default) and further bounded overall by the 1,000-row CSV validation limit enforced before any Job is created. Plugin Check retains this as a generic meta-query performance warning regardless of this bounding.
    28992911                                        'meta_query'     => array(
    29002912                                                array(
    … …  
    29572969                                        // pattern correctly excludes only members whose key
    29582970                                        // genuinely starts with this job's own prefix.
     2971                                        // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- part of the staged import swap process (retiring phase); the result set is bounded by the current import batch size ($batch_size, 50 by default) and further bounded overall by the 1,000-row CSV validation limit enforced before any Job is created. Plugin Check retains this as a generic meta-query performance warning regardless of this bounding.
    29592972                                        'meta_query'     => array(
    29602973                                                'relation' => 'OR',
    … …  
    34513464                                'fields'         => 'ids',
    34523465                                'no_found_rows'  => true,
    3453                                 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- a single, exact-match lookup on a plugin-internal meta key this method itself controls the values of (never arbitrary/unbounded), called at most once per eligible member per batch (see process_import_image_batch_cron()'s own batch_size bound).
     3466                                // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- a single, exact-match lookup on a plugin-internal meta key this method itself controls the values of (never arbitrary/unbounded), called at most once per eligible member per batch (see process_import_image_batch_cron()'s own batch_size bound).
    34543467                                'meta_key'       => self::IMAGE_IMPORTED_FOR_KEY_META,
     3468                                // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- a single, exact-match lookup on a plugin-internal meta key this method itself controls the values of (never arbitrary/unbounded), called at most once per eligible member per batch (see process_import_image_batch_cron()'s own batch_size bound).
    34553469                                'meta_value'     => $member_import_key,
    34563470                        )
    … …  
    35393553                );
    35403554
    3541                 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- an admin-page-load-only display query, bounded to at most this one job's own members (this plugin's own 1000-row Validation ceiling bounds that further), not an arbitrary/unbounded lookup; run only when rendering this one admin screen.
    35423555                $remaining = count(
    35433556                        get_posts(
    … …  
    35483561                                        'fields'         => 'ids',
    35493562                                        'no_found_rows'  => true,
     3563                                        // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- an admin-page-load-only display query, bounded to at most this one job's own members (this plugin's own 1000-row Validation ceiling bounds that further), not an arbitrary/unbounded lookup; run only when rendering this one admin screen.
    35503564                                        'meta_query'     => array_merge(
    35513565                                                array( 'relation' => 'AND' ),
    … …  
    35713585                );
    35723586
    3573                 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- see the identical justification immediately above.
    35743587                $failed = count(
    35753588                        get_posts(
    … …  
    35803593                                        'fields'         => 'ids',
    35813594                                        'no_found_rows'  => true,
     3595                                        // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- see the identical justification immediately above.
    35823596                                        'meta_query'     => array_merge(
    35833597                                                array( 'relation' => 'AND' ),
    … …  
    38543868                        }
    38553869
     3870                        /**
     3871                         * Filter the fully-built $meta_input array for this CSV row,
     3872                         * immediately before it's used to create the member post —
     3873                         * lets a companion plugin (e.g. OmniDirectory Pro's own
     3874                         * dynamic custom fields) add its own additional postmeta
     3875                         * values from this same row, atomically, as part of this
     3876                         * same wp_insert_post() call below, without this plugin
     3877                         * knowing anything about what those fields are.
     3878                         *
     3879                         * $row and $column_map are this row's own raw CSV cell
     3880                         * values and this file's own column-index-to-field-key
     3881                         * mapping (already built earlier in this same method) —
     3882                         * offered here only as read-only context a callback can use
     3883                         * to recognize its own columns by name; this plugin's own
     3884                         * CSV engine does not read, validate, or map anything on a
     3885                         * callback's behalf, and neither $row, $column_map, nor
     3886                         * $args is itself modified by this filter — only the
     3887                         * returned $meta_input is used. Staging, Swap, Recovery,
     3888                         * WP-Cron batching, and the 1,000-row import limit are
     3889                         * entirely unaffected: this is the exact same per-row
     3890                         * $meta_input this method already builds and inserts today,
     3891                         * merely given one more chance to gain additional keys first.
     3892                         * Empty by default (returns $meta_input unchanged) — a
     3893                         * Free-only install's import behavior is identical to before
     3894                         * this filter existed.
     3895                         *
     3896                         * @param array $meta_input  This row's own meta_input array, built above.
     3897                         * @param array $row         This row's own raw cell values, as returned by fgetcsv().
     3898                         * @param array $column_map  Column index => internal field key, built earlier in this same method.
     3899                         * @param array $args        This call's own $args (post_status, import_key, etc.) — see this method's own docblock.
     3900                         */
     3901                        $meta_input = apply_filters( 'omnidirectory_csv_row_meta_input', $meta_input, $row, $column_map, $args );
     3902
    38563903                        $post_id = wp_insert_post(
    38573904                                array(
    … …  
    39253972                                'fields'         => 'ids',
    39263973                                'no_found_rows'  => true,
    3927                                 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- a single, exact-match lookup on a plugin-internal meta key this method itself controls the values of (never arbitrary/unbounded), used once per row per batch (at most 50 times per cron run) specifically to keep staging idempotent — see IMPORT_KEY_META's own docblock.
     3974                                // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- a single, exact-match lookup on a plugin-internal meta key this method itself controls the values of (never arbitrary/unbounded), used once per row per batch (at most 50 times per cron run) specifically to keep staging idempotent — see IMPORT_KEY_META's own docblock.
    39283975                                'meta_key'       => self::IMPORT_KEY_META,
     3976                                // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- a single, exact-match lookup on a plugin-internal meta key this method itself controls the values of (never arbitrary/unbounded), used once per row per batch (at most 50 times per cron run) specifically to keep staging idempotent — see IMPORT_KEY_META's own docblock.
    39293977                                'meta_value'     => $import_key,
    39303978                        )
    … …  
    61606208                $data['queried_post_id'] = $queried_post_id;
    61616209
     6210                /**
     6211                 * Filter to inject extra markup INSIDE each of the 8
     6212                 * render_profile_*() layouts' own profile info area — e.g.
     6213                 * OmniDirectory Pro's own Dynamic Fields — as opposed to
     6214                 * 'omnidirectory_single_profile_extra_fields' below, which fires
     6215                 * only after render_profile_by_layout() as a whole and can't
     6216                 * place anything inside any layout's own markup. Resolved once,
     6217                 * here, rather than inside each of the 8 layouts separately, so
     6218                 * every layout simply prints this same already-built string in
     6219                 * its own profile info area — see each render_profile_*()
     6220                 * layout's own inline comment for exactly where. Empty by
     6221                 * default; a Free-only install's output is unaffected.
     6222                 *
     6223                 * @param string $dynamic_fields_html Extra HTML to print inside the profile info area. Empty by default.
     6224                 * @param int    $post_id             The member whose own data this content should reflect (same post actually being displayed — see $display_post_id's own docblock above for the "?member_lang" fallback this already accounts for).
     6225                 * @param array  $data                This method's own profile data array built just above (name, field1-5, phone, email, bio, etc.) — read-only context for a callback that needs it.
     6226                 */
     6227                $data['dynamic_fields_html'] = apply_filters( 'omnidirectory_profile_data_fields', '', $display_post_id, $data );
     6228
    61626229                // Enqueue the same front-end styles AND script used by the search
    61636230                // cards so the single-post layout looks and behaves consistently
    … …  
    62036270                        add_filter( 'safe_style_css', array( $this, 'allow_display_css_property' ) );
    62046271                        $html .= wp_kses( $actions_html, $this->get_widget_html_allowed_tags() );
     6272                        remove_filter( 'safe_style_css', array( $this, 'allow_display_css_property' ) );
     6273                }
     6274
     6275                /**
     6276                 * Filter to inject extra markup for additional, non-action
     6277                 * content — e.g. a companion plugin's own dynamic custom fields
     6278                 * — at the same shared call site as
     6279                 * 'omnidirectory_single_profile_actions' above (once, after
     6280                 * whichever of the eight render_profile_*() layouts was used),
     6281                 * but kept as its own, separately-named filter rather than
     6282                 * overloading that one: that filter's own name and existing
     6283                 * documented purpose is specifically "actions" (Print/Share-style
     6284                 * buttons), not arbitrary profile content, and this plugin has no
     6285                 * dynamic-fields concept of its own to render here — only
     6286                 * OmniDirectory Pro (or another companion plugin) does. Empty by
     6287                 * default; a Free-only install's output here is unaffected, and
     6288                 * this plugin's own 8 render_profile_*() layouts are not touched
     6289                 * at all to support this — see this method's own docblock above
     6290                 * for why this one shared call site, not each layout
     6291                 * individually, is where both of these filters live.
     6292                 *
     6293                 * @param string $extra_fields_html Extra HTML to render after the profile. Empty by default.
     6294                 * @param int    $post_id           The post whose own data this content should reflect — the ORIGINAL queried post (see $actions_html's own docblock above for why this isn't necessarily $data['post_id']).
     6295                 * @param array  $data              The same profile data array this method's own render_profile_by_layout() call just used (name, field1-5, phone, email, bio, etc.) — offered here only as read-only context for a callback that needs it; this plugin does not itself add anything to it.
     6296                 */
     6297                $extra_fields_html = apply_filters( 'omnidirectory_single_profile_extra_fields', '', $queried_post_id, $data );
     6298
     6299                if ( '' !== $extra_fields_html ) {
     6300                        // Same trust model and same reasoning as $actions_html just
     6301                        // above — see that block's own comment.
     6302                        add_filter( 'safe_style_css', array( $this, 'allow_display_css_property' ) );
     6303                        $html .= wp_kses( $extra_fields_html, $this->get_widget_html_allowed_tags() );
    62056304                        remove_filter( 'safe_style_css', array( $this, 'allow_display_css_property' ) );
    62066305                }
    … …  
    72617360                                <?php endif; ?>
    72627361
    7263                                 <?php if ( $data['field2'] || $data['field3'] || $data['field4'] || $data['field5'] || $data['email'] ) : ?>
     7362                                <?php if ( $data['field2'] || $data['field3'] || $data['field4'] || $data['field5'] || $data['email'] || ! empty( $data['dynamic_fields_html'] ) ) : ?>
    72647363                                        <div class="omni-profile-details">
    72657364                                                <?php if ( $data['field2'] ) : ?>
    … …  
    73087407                                                        </div>
    73097408                                                <?php endif; ?>
     7409                                                <?php if ( ! empty( $data['dynamic_fields_html'] ) ) : ?>
     7410                                                        <?php echo wp_kses( $data['dynamic_fields_html'], $this->get_widget_html_allowed_tags() ); ?>
     7411                                                <?php endif; ?>
    73107412                                        </div>
    73117413                                <?php endif; ?>
    … …  
    74007502                                                </div>
    74017503                                        <?php endif; ?>
     7504                                        <?php if ( ! empty( $data['dynamic_fields_html'] ) ) : ?>
     7505                                                <?php echo wp_kses( $data['dynamic_fields_html'], $this->get_widget_html_allowed_tags() ); ?>
     7506                                        <?php endif; ?>
    74027507                                </aside>
    74037508
    … …  
    74527557                        </div>
    74537558
    7454                         <?php if ( $data['field2'] || $data['field3'] || $data['field4'] || $data['field5'] || $data['phone'] || $data['email'] ) : ?>
     7559                        <?php if ( $data['field2'] || $data['field3'] || $data['field4'] || $data['field5'] || $data['phone'] || $data['email'] || ! empty( $data['dynamic_fields_html'] ) ) : ?>
    74557560                                <div class="omni-profile-hero-cards">
    74567561                                        <?php if ( $data['field2'] ) : ?>
    … …  
    75087613                                                </div>
    75097614                                        <?php endif; ?>
     7615                                        <?php if ( ! empty( $data['dynamic_fields_html'] ) ) : ?>
     7616                                                <?php echo wp_kses( $data['dynamic_fields_html'], $this->get_widget_html_allowed_tags() ); ?>
     7617                                        <?php endif; ?>
    75107618                                </div>
    75117619                        <?php endif; ?>
    … …  
    75577665                $id_gallery     = 'omni-tab-gallery-' . $post_suffix;
    75587666                $id_documents   = 'omni-tab-documents-' . $post_suffix;
    7559                 $has_contact_info = ( $data['field1'] || $data['field2'] || $data['field3'] || $data['field4'] || $data['field5'] || $data['phone'] || $data['email'] );
     7667                $has_contact_info = ( $data['field1'] || $data['field2'] || $data['field3'] || $data['field4'] || $data['field5'] || $data['phone'] || $data['email'] || ! empty( $data['dynamic_fields_html'] ) );
    75607668                ob_start();
    75617669                ?>
    … …  
    76687776                                                                </div>
    76697777                                                        <?php endif; ?>
     7778                                                        <?php if ( ! empty( $data['dynamic_fields_html'] ) ) : ?>
     7779                                                                <?php echo wp_kses( $data['dynamic_fields_html'], $this->get_widget_html_allowed_tags() ); ?>
     7780                                                        <?php endif; ?>
    76707781                                                </div>
    76717782                                        <?php endif; ?>
    7672                                         <?php if ( ! $data['phone'] && ! $data['email'] ) : ?>
     7783                                        <?php if ( ! $data['phone'] && ! $data['email'] && empty( $data['dynamic_fields_html'] ) ) : ?>
    76737784                                                <p class="omni-profile-tab-empty"><?php esc_html_e( 'No contact information provided.', 'omnidirectory' ); ?></p>
    76747785                                        <?php endif; ?>
    … …  
    77257836                                );
    77267837                                ?>
    7727                                 <?php if ( $data['field2'] || $data['field3'] || $data['field4'] || $data['field5'] || $data['phone'] || $data['email'] ) : ?>
     7838                                <?php if ( $data['field2'] || $data['field3'] || $data['field4'] || $data['field5'] || $data['phone'] || $data['email'] || ! empty( $data['dynamic_fields_html'] ) ) : ?>
    77287839                                        <div class="omni-profile-minimalist-rows">
    77297840                                                <?php foreach ( $minimalist_rows as $row ) : ?>
    … …  
    77477858                                                        </div>
    77487859                                                <?php endif; ?>
     7860                                                <?php if ( ! empty( $data['dynamic_fields_html'] ) ) : ?>
     7861                                                        <?php echo wp_kses( $data['dynamic_fields_html'], $this->get_widget_html_allowed_tags() ); ?>
     7862                                                <?php endif; ?>
    77497863                                        </div>
    77507864                                <?php endif; ?>
    … …  
    77947908                                </div>
    77957909                                <div class="omni-profile-splitcover-right">
    7796                                         <?php if ( $data['field2'] || $data['field3'] || $data['field4'] || $data['field5'] || $data['phone'] || $data['email'] ) : ?>
     7910                                        <?php if ( $data['field2'] || $data['field3'] || $data['field4'] || $data['field5'] || $data['phone'] || $data['email'] || ! empty( $data['dynamic_fields_html'] ) ) : ?>
    77977911                                                <div class="omni-profile-details">
    77987912                                                        <?php if ( $data['field2'] ) : ?>
    … …  
    78507964                                                                </div>
    78517965                                                        <?php endif; ?>
     7966                                                        <?php if ( ! empty( $data['dynamic_fields_html'] ) ) : ?>
     7967                                                                <?php echo wp_kses( $data['dynamic_fields_html'], $this->get_widget_html_allowed_tags() ); ?>
     7968                                                        <?php endif; ?>
    78527969                                                </div>
    78537970                                        <?php endif; ?>
    … …  
    79358052                                                </div>
    79368053                                        <?php endif; ?>
     8054                                        <?php if ( ! empty( $data['dynamic_fields_html'] ) ) : ?>
     8055                                                <?php echo wp_kses( $data['dynamic_fields_html'], $this->get_widget_html_allowed_tags() ); ?>
     8056                                        <?php endif; ?>
    79378057                                </div>
    79388058
    … …  
    79818101                                <?php echo wp_kses( $this->render_language_switcher( $data['languages'] ), $this->get_widget_html_allowed_tags() );?>
    79828102
    7983                                 <?php if ( $data['field2'] || $data['field3'] || $data['field4'] || $data['field5'] || $data['email'] ) : ?>
     8103                                <?php if ( $data['field2'] || $data['field3'] || $data['field4'] || $data['field5'] || $data['email'] || ! empty( $data['dynamic_fields_html'] ) ) : ?>
    79848104                                        <div class="omni-profile-details">
    79858105                                                <?php if ( $data['field2'] ) : ?>
    … …  
    80288148                                                        </div>
    80298149                                                <?php endif; ?>
     8150                                                <?php if ( ! empty( $data['dynamic_fields_html'] ) ) : ?>
     8151                                                        <?php echo wp_kses( $data['dynamic_fields_html'], $this->get_widget_html_allowed_tags() ); ?>
     8152                                                <?php endif; ?>
    80308153                                        </div>
    80318154                                <?php endif; ?>
  • omnidirectory/trunk/readme.txt

    r3717915 r3726427  
    1 === OmniDirectory ===
     1=== OmniDirectory – Directory & Listings ===
    22Contributors: soulja, kerio
    3 Tags: directory, staff directory, team directory, search, multilingual
     3Tags: directory, business directory, member directory, staff directory, multilingual
    44Requires at least: 6.0
    55Tested up to: 7.1
    66Requires PHP: 7.4
    7 Stable tag: 4.3.31
     7Stable tag: 4.3.32
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    1010
    11 A multi-purpose directory plugin with bilingual Arabic/English search, multiple layouts, document uploads, and click-to-reveal privacy protection.
     11A flexible WordPress directory plugin for staff, member, and business listings — with multilingual and Arabic/RTL search.
    1212
    1313== Description ==
    1414
    15 OmniDirectory registers a "Directory Members" custom post type and lets you build a searchable staff/member directory that adapts to almost any profession — doctors, lawyers, consultants, clergy, companies — just by relabeling three custom fields from Directory Settings.
    16 
    17 **Core features:**
    18 
    19 * Custom post type ("Directory Members") with a photo, three admin-configurable fields (e.g. Specialty/Title, Location/Office, Phone), Email, Bio, and an optional "Directory Category" taxonomy.
    20 * Live AJAX search shortcode `[omnidirectory_search]`, restricted to matching Title and Directory Category (no false-positive matches from unrelated meta/content).
    21 * Built-in Arabic + English search: an Arabic search term automatically matches Arabic listings and an English term matches English listings, with Arabic letter-variant normalization (e.g. "احمد" and "أحمد" match each other) baked in, and full RTL support throughout.
    22 * Eight search-results layouts: Modern Grid Cards, Horizontal List Row, Avatar Badge Compact, Structured Table View, Masonry Pinterest Grid, Horizontal Directory List, Compact Table Matrix, and Interactive Hover Flip Cards — switchable from Directory Settings.
    23 * Eight single-profile layouts: Modern Centered Card, Two-Column Sidebar, Hero Banner Top, Tabbed Profile View, Minimalist Clean, Split Screen Cover, Interactive Dashboard, and Floating Card Overlay.
    24 * Attachments / Documents: attach PDF, Word, Excel, or plain-text files to a member's profile, with an optional custom label per file, shown as a labeled download card on their profile.
    25 * Click-to-Reveal privacy option: optionally mask phone numbers and email addresses behind a "Click to show" button, to deter basic spam-harvesting bots.
    26 * Category filter dropdown, a primary accent color picker, an "hide empty fields" display toggle, and a CSV Import/Export module for bulk-adding or backing up your directory.
    27 * Polylang and WPML compatible: the post type, taxonomy, and translatable fields are registered for both, with a `wpml-config.xml` file included for WPML.
     15= Overview: What Is OmniDirectory? =
     16
     17OmniDirectory is one flexible directory engine for building searchable WordPress directories — staff, members, professionals, businesses, organizations, and more. Every entry shares the same underlying structure: a name, a set of relabelable directory fields, a category, and a profile page, presented through your choice of search and profile layouts.
     18
     19Rather than offering separate tools for each type of directory, OmniDirectory adapts to your use case by relabeling its existing fields and choosing appropriate layouts — no code required.
     20
     21= Directory Use Cases =
     22
     23The same directory engine can be adapted for:
     24
     25* Staff Directory
     26* Employee Directory
     27* Team Directory
     28* Member Directory
     29* Professional Directory
     30* Business Directory (can be adapted for)
     31* Company Directory (can be adapted for)
     32* Organization Directory (can be adapted for)
     33* Institution Directory (can be adapted for)
     34* Medical Directory
     35* School / University Directory (can be adapted for)
     36* Church Directory (can be adapted for)
     37* Nonprofit / Community Directory (can be adapted for)
     38* Multilingual Directory
     39* Arabic / RTL Directory
     40
     41These are use cases of one directory engine, reached by relabeling fields and choosing a layout — not separate built-in modes with specialized fields for each industry.
     42
     43= Search and Discovery =
     44
     45Add the `[omnidirectory_search]` shortcode to any page for a live, AJAX-powered search box — results appear as visitors type, without a page reload.
     46
     47By default, search matches an entry's name/title and its Directory Category. Other fields (Phone, Email, Bio, etc.) are not searched by default; a developer can extend what's searched using this plugin's own filter hooks. Live search results are cached briefly and invalidated immediately whenever a directory entry changes, and basic AJAX rate limiting helps reduce the impact of automated request flooding. Results are capped to a configurable limit rather than paginated.
     48
     49= Arabic, Multilingual and RTL Support =
     50
     51Search includes real Arabic-aware normalization: common letter variations (different forms of Alef, ta marbuta versus ha, and similar cases) are matched during search, so visitors find listings even when spelling varies slightly. Every layout renders with the correct `dir="rtl"` or `dir="ltr"` attribute based on the site's active language.
     52
     53OmniDirectory registers its directory entries and category taxonomy as translatable for both **Polylang** and **WPML** (a `wpml-config.xml` file is included), so multilingual sites can maintain a translated version of each entry and category. This covers the directory structure and manual entry translation; it does not mean every workflow is automatically language-aware — CSV-imported entries, for example, are not automatically linked into a translation group (see the CSV section below).
     54
     55= Adaptable Fields =
     56
     57OmniDirectory has a fixed set of built-in directory fields, and their labels can be relabeled from Settings — without code — to fit different use cases. For example:
     58
     59**Medical directory:** Specialty / Location / Phone
     60**Company directory:** Department / Office / Phone
     61**Professional directory:** Profession / Location / Phone
     62**Member directory:** Role / Location / Phone
     63
     64Relabeling changes what visitors see; it does not add new fields. OmniDirectory does not currently include a tool for creating unlimited or arbitrary custom fields.
     65
     66= Search-Result Layouts =
     67
     68Eight layouts for search results, switchable from Settings:
     69
     70* Modern Grid Cards (`grid`) — default
     71* Horizontal List Row (`horizontal`)
     72* Avatar Badge Compact (`badge`)
     73* Structured Table View (`table`)
     74* Masonry Pinterest Grid (`masonry`)
     75* Horizontal Directory List (`directory_list`)
     76* Compact Table Matrix (`table_matrix`)
     77* Interactive Hover Flip Cards (`flip_cards`)
     78
     79= Single-Profile Layouts =
     80
     81Eight layouts for individual profile pages, switchable from Settings:
     82
     83* Modern Centered Card (`modern_card`) — default
     84* Two-Column Sidebar (`two_column_sidebar`)
     85* Hero Banner Top (`hero_banner_top`)
     86* Tabbed Profile View (`tabbed_profile`)
     87* Minimalist Clean (`minimalist_clean`)
     88* Split Screen Cover (`split_screen_cover`)
     89* Interactive Dashboard (`interactive_dashboard`)
     90* Floating Card Overlay (`floating_card_overlay`)
     91
     92= Profile Information and Documents =
     93
     94Each directory entry can include a name, a profile photo, its relabelable directory fields, Phone, Email, a Bio, a Website field, a Directory Category, and a gallery shortcode field (stores and renders a shortcode you provide — this is not a built-in gallery builder).
     95
     96Profiles can also have Documents attached: PDF, DOC, DOCX, XLS, XLSX, and TXT files, each with its own custom label, and checked against its real file type on the server before being accepted.
     97
     98= Click-to-Reveal Contact Details =
     99
     100Optionally keep phone numbers and email addresses out of the initial page source until the visitor clicks Reveal. This is a lightweight deterrent against basic automated scanning of static HTML — it is not anti-scraping or bot-protection software, and does not stop a scraper that fully executes JavaScript.
     101
     102= CSV Import and Reliability =
     103
     104Bulk-add or back up your directory with CSV import/export. Column headers are matched automatically against common naming variants. The entire file — row count, header, and every row — is validated before anything changes; an invalid or oversized file (over 1,000 rows) is rejected up front, with nothing changed.
     105
     106Import then runs as a resumable background process: new entries are staged privately first, then safely swapped in to replace the previous directory only once every new entry is fully ready — with a "Resume Import" option if it ever needs a manual nudge. Featured images referenced in the CSV are downloaded afterward, independently, in the background, using WordPress's own safe HTTP request handling (which rejects unsafe destinations such as private/internal network addresses and their redirects), with a bounded number of retries for any image that fails.
     107
     108**Important:** CSV import replaces your directory's dataset — it does not selectively update individual existing entries. Re-importing a file results in a fresh set of entries matching that file, not in-place edits to the entries already there.
     109
     110= Structured Data =
     111
     112Profile pages include Schema.org (JSON-LD) structured data using the `Person` type by default, to help search engines understand the page. Developers can extend or change this behavior using this plugin's WordPress filters; there is no dashboard setting for configuring Schema.org types.
     113
     114= Performance and Reliability =
     115
     116* Live search results are cached, with immediate invalidation when a directory entry changes.
     117* Live search includes basic AJAX rate limiting.
     118* CSV import runs in the background, in small resumable batches, rather than one long page request.
     119* The whole file is validated before any existing directory entry is changed.
     120* New entries are staged privately, then safely swapped in once staging is complete.
     121* Featured images from CSV import are downloaded in the background, separately from the import itself, with bounded retries for failures.
     122
     123= Who Is OmniDirectory For? =
     124
     125Site owners and developers building a searchable directory of people or listings — staff and team pages, member directories, professional or business listings, and multilingual or Arabic-language directories — who want one adaptable plugin rather than a tool built narrowly for a single industry.
    28126
    29127= Getting Started =
    30128
    311291. Activate the plugin.
    32 2. Go to **Directory Members → Directory Settings** to relabel the custom fields for your use case (e.g. "Specialty / Title" for a medical practice, "Practice Area" for a law firm) and choose your preferred search/profile layouts.
    33 3. Add members under **Directory Members → Add New**, or bulk-import them via **Directory Members → Import / Export**.
     1302. Go to **Directory Members → Directory Settings** to relabel the directory fields for your project and choose your search and profile layouts.
     1313. Add entries under **Directory Members → Add New**, or bulk-import them via **Directory Members → Import / Export**.
    341324. Place the `[omnidirectory_search]` shortcode on any page to show the live search box.
    35133
    … …  
    52150
    531511. Visit **Directory Members → Directory Settings** to configure field labels, layouts, and colors.
    54 2. Add your directory members under **Directory Members → Add New**.
     1522. Add your directory entries under **Directory Members → Add New**.
    551533. Add the `[omnidirectory_search]` shortcode to any page or post where you want the search box to appear.
    56154
    57155== Frequently Asked Questions ==
    58156
    59 = Can I use this for something other than a clergy or medical directory? =
    60 
    61 Yes. The three custom fields (default labels: Specialty/Title, Location/Office, Phone Number) can be relabeled from Directory Settings to fit any profession — law firms, consultancies, companies, or any other member/staff directory.
    62 
    63 = Does the search work in Arabic? =
    64 
    65 Yes. The search automatically detects whether the typed term is Arabic or English (based on the characters typed, regardless of the site's active language) and matches accordingly, including common Arabic letter variations (e.g. different Alef forms, ta marbuta vs. ha).
    66 
    67 = Is it compatible with Polylang or WPML? =
    68 
    69 Yes. The custom post type and Directory Category taxonomy are registered as translatable for both plugins, and a `wpml-config.xml` file is included with the plugin to declare which custom fields WPML should treat as translatable.
    70 
    71 = What file types can I attach to a member's profile? =
    72 
    73 PDF, Word (.doc/.docx), Excel (.xls/.xlsx), and plain text (.txt). Every uploaded file's actual MIME type is validated against this list on the server, not just restricted in the browser's file picker.
    74 
    75 = How do I bulk-add members instead of entering them one at a time? =
    76 
    77 Go to **Directory Members → Import / Export** and upload a .csv file. Column headers are matched automatically against common naming variants (e.g. "Title" or "Full Name" both map to Name), so you don't need to match this plugin's exact export format exactly.
    78 
    79 = Does click-to-reveal fully hide phone/email from all bots? =
    80 
    81 It's a lightweight deterrent (similar to common "email cloaking" plugins), not a guarantee: the real value is kept out of the page's initial HTML and only inserted after a visitor clicks. It's designed to stop basic scrapers that scan static HTML for phone/email patterns, not a bot that fully executes JavaScript and specifically targets this markup.
     157= What can I build with OmniDirectory? =
     158
     159A searchable WordPress directory of people or listings — staff, team members, professionals, businesses, organizations, or a multilingual/Arabic directory — by relabeling the built-in fields and choosing a layout.
     160
     161= Is OmniDirectory only for staff directories? =
     162
     163No. Staff directories are one common use case, but the same fields and layouts can be relabeled for member, professional, business, or other directory types.
     164
     165= Can I create a business directory? =
     166
     167Yes, by adapting the existing directory fields and category taxonomy to your business listings.
     168
     169= Can I create a member directory? =
     170
     171Yes. You can adapt the existing fields and categories for member listings.
     172
     173= Can I create a professional directory? =
     174
     175Yes — the default field labels are already suited to professional/medical-style listings.
     176
     177= Can I create an employee directory? =
     178
     179Yes.
     180
     181= Can I create a medical directory? =
     182
     183Yes, the default field labels (Specialty/Title, Location/Office, Phone) are designed with this in mind.
     184
     185= Can I create a school or university directory? =
     186
     187You can adapt OmniDirectory for this by relabeling its existing fields; there are no fields built specifically for academic departments or courses.
     188
     189= Can I create an Arabic/RTL directory? =
     190
     191Yes. Search includes Arabic letter-variant normalization, and every layout renders with the correct RTL/LTR direction based on the site's language.
     192
     193= Does OmniDirectory support WPML? =
     194
     195Yes. The directory post type and category taxonomy are registered as translatable, and a `wpml-config.xml` file is included.
     196
     197= Does OmniDirectory support Polylang? =
     198
     199Yes. The directory post type and category taxonomy are registered with Polylang automatically.
     200
     201= Can I customize directory field labels? =
     202
     203Yes, from Directory Settings.
     204
     205= Can I add unlimited custom fields? =
     206
     207You can relabel OmniDirectory's existing directory fields to fit different use cases without writing any code. It does not currently include a tool for adding new, unlimited custom fields beyond that fixed set.
     208
     209= Can I customize the directory layouts? =
     210
     211Yes — eight search-results layouts and eight single-profile layouts are available from Directory Settings.
     212
     213= Can I import directory entries from CSV? =
     214
     215Yes, with automatic column mapping, a 1,000-row limit per file, and background processing.
     216
     217= Does CSV import update existing entries? =
     218
     219No. CSV import replaces your directory's dataset with what's in the file; it does not selectively edit individual existing entries in place.
     220
     221= Can I attach documents to profiles? =
     222
     223Yes — PDF, DOC, DOCX, XLS, XLSX, and TXT files, each with an optional custom label.
     224
     225= Can I hide phone/email values until clicked? =
     226
     227Yes, using the Click-to-Reveal option. This keeps the value out of the initial page source until clicked; it is not anti-scraping or bot-protection software.
     228
     229= Does OmniDirectory include a Gutenberg block? =
     230
     231No. OmniDirectory currently uses a shortcode (`[omnidirectory_search]`), which also works inside the Block Editor's Shortcode block.
     232
     233= What is OmniDirectory not designed to do? =
     234
     235OmniDirectory focuses on searchable directory and listings functionality. It does not provide membership management (user registration, logins, or gated content), marketplace or e-commerce features (payments, vendor accounts, product catalogs), booking systems, reviews/ratings, or affiliate functionality.
    82236
    83237== Screenshots ==
    … …  
    88242
    89243== Changelog ==
     244
     245= 4.3.32 =
     246* Updated the plugin name and description to reflect OmniDirectory's broader positioning as an adaptable WordPress directory and listings plugin, rather than a staff-directory-only tool. No functional changes.
     247* Code quality: corrected the placement of several existing code-documentation comments (`phpcs:ignore`) so they correctly annotate the specific, already-reviewed, intentionally bounded queries used by the CSV import swap process. No query logic, behavior, or performance characteristics were changed.
     248* Added `omnidirectory_single_profile_extra_fields` extension point for companion plugins. No effect by itself.
     249* Added `omnidirectory_csv_import_aliases` extension point for companion plugins. No effect by itself.
     250* Added `omnidirectory_csv_row_meta_input` extension point for companion plugins. No effect by itself.
     251* Added `omnidirectory_profile_data_fields` extension point for companion plugins, allowing their own content to be printed inside the profile details area of all 8 single-profile layouts. No effect by itself.
    90252
    91253= 4.3.31 =
Note: See TracChangeset for help on using the changeset viewer.