ÿØÿà JFIF      ÿÛ „ 	 ( %!1"%)+...383-7(-.+



-%%--------+------------/----------------------------ÿÀ  ¶" ÿÄ               ÿÄ @       !1AQaq‘¡"±ÁÑð2’BRráS‚ñ3b¢CT²Ò #cÿÄ             ÿÄ '        1!AQ2a"‘±#ÿÚ   ? ò(/p‘—–¢·,M|¯G»R£üešT. ôŸEoCFñ‡ 4ÔkåóxR|µÁ¨ÓDà!Ç´µ˜‡p ÍQ‹§!þž^õWsŒy  åÁ§$á«%ºq˜™JyyË0y«^Ï
-3ÃÏËÈ£Æ\P’ä„s&•‹Üs üô'àWt¹pñCp$bÚF`·7.^)¾!)ÒÙ˜–ÅÏš^h¾ø³·ìe±ŒaeÖ—Í,e=B}Jéß¦$$½—šÙÜCC¯¶è8Î²–Åkèwx
LÉngT±æ¹£Ócœ¿Í/©Éÿ ‰ÚCÒ­mq;Tt¶–}0"zÖPcd1š­¤tˆ„ÐÓâqú[™öUCŠ2Þ¿Ø¯­É(¸ßúçšmW,Ò ºf‹Ån™ÄLêÃ8üÁkÁ€L¥ÍuJ1œôrÂSŒÓöt!7EUíïb)AËi¬â-ÙHk8òÁiXYwÅ)O^"ÇØriÛô{»wRUKÙÏÚlÓ¬€‚’×©T]
ªb—ÊîKC^×M“xùäñz¬zJ×{4[¹M'IÆeH1LCWÕ]œûº WDæà°OÅ‚¹c²ÞuA–rL`HšpCez‡V’— Ž‚Ë„–ñ­'µR0ÕÖ=À:±G»C\nÆÉ5*¢ †3šhQ4ýÒi$Õ1"Ü]¢p-q3zcQ$ªö¨¥æf›‘»´ÝÚHá„e²JÇ–iÊ:·à¬Øc5Åk»Laª*wi
[0Ówk ¦a¦0Õ²ÅÅ‚T¸’³q:Æ<¹ú	Ò˜Æ}2VE•Ð€p¡Ô3Úº …h³^Ç‡ºçì¥Ánó|˜ÖˆÎˆ+«¦dø‘Mº–d\ªDðÛ„Ì×LÝ——Ð9Ç|•FèçVm™¨€™3õS–9Äå¢<Ì‰Ìm¢±`·º˜&Z²ž Ë®D«¶»6™‰©­HÈÀá«h­i³04æ’hxœi3ï¨(ÓL·†‹¶ËpŒ
ƒÜÒ?	:õdsÀ–3æÓ·Üjâ¬9—]8n¼&]I F£¼…ì–—Œ0v°LÇÅfÛ2TÖ’aµ˜N@ÖBSÏ^à´4-±°b×nÈÈœÆD=KdŒø…ÄÈK,›ÖR§fjü$^ÔÎ!„6^ s:Üun\ôkCÞò÷<—Lúl
Í›G¹ÀPá¼Nc¡šÒ…ÙWÍ³œ±vÊáÐ„×9‹P‰cEØ‘™›µŸP<×g1
º©Ø¬wiÁ^l5×I/';Ë+´Z‡ÓÄFÕp[Èð’
HÄ	ó
9¬Dl4“é±Ï”V=^Hð;žLädÉa‚°Ë,#/Ž~²BÔÛ
QãñQtIeórVB,ÏÂf‹4#Îœc‹É3ZIÖ Œ¢¥·üxa‡Ãçè„jŠ[5 Ä!U?,3žÎ×…ðdÓ÷háŠA‰ìÄ4÷ÃÜZÃE{‰®+W\ZÍE[‰»µk»MÝ­`¢©†˜ÃV‹-f¢¡†¢a«eŠ%«Y¨©q%jâd,Ôq`) ¤¦…šˆ ¦E>0Ùb²t–Ÿ
÷jsÏ‚Ò’KÈcø(é›;k(’#ôŒQ
™C>
õ³M9óºÆµ¤çâ;uÀ)5>RÔ¸fÓ~È¦—’-$fe«"FâUˆÊwµPÏu
w+"åÉoÌ¨6s™Û9(À¨ù©hE°½†n`m~“@©›p!Vkhµ Á1šbEqqÊ@ºèl›2 –Yã%‘‚hÛ{a¶‡ÅÞM²¬çL34®Çl]®‹ŽØŒm'RÜe35æöˆ­Á­ cP9šeÁköo´L³Ì=p8]”‡B¶,šº|ÉdzXˆØk*ÇÚ{#ñ‰pã(€¶‡9ý'šÜ³½¯kƒ†Â’ëSOƒ•Å®H6+a¢µˆ‹Y¨b˜b+XˆÖ!°u!¢"µˆˆn2ˆ Å0Ä`Å Ô®c(ÓÜGº•Ä»Œ âWî%q.áÐÄ÷î%umÍ£+ÜQ,Vn¦-GphU¸¢X­&,Gt
·K¢ÅÅ·¥RÄÊÉbKn
N­R2g$VµÕó
Qr#–ÓZUÏð0ÎtÙÀœz
ëöo“Îk¯n„T°"SÞ+³/…€½"ižãO%Í%&Î”â‘ËØ´IˆÓ°Ë‰ÞM<J•›C=ìu*Ü§„Ã¤GËŠô;5ŒÁ£ñùä¬6Ì2¦‘žh¸Rº<Õš%÷ …Ç	‚g‡.
>3.™yçÁzit›`4C`k¢¼á!álÄï sÂ[×!¤l¾æo"ƒIÔ'@ªu)3›ô®“@Z
à¾]
üœ'‡4¸PÞkÁ-qÄH‰ÓÍBd´ÿ —¹°ÄA&ÊUC„öz‚‚RæŠtf¬>˜ÑidšEçÑL÷áý¡V±h§­c¿¥¨ƒ@g¼…Ýö^ÞËc.Å¾„æ“I[ª+·zÐÒÚ,½Ð»¶€C›7jk$à¹–€Ž¾ÅÙpp¶ÍþéÎ¤ ½ØÎŒq ñpæžÅ¥#Øâ49ÃÂK^MYV“,¶Éz_òæ:…;Õ«¦I3âJ£¤û5$7Èxün\â]Êe¯(Ÿ&Ž‹¶2<6Ä†f× æ¢Æ±qWÄ±[(ÙÂŽç‚T7¨@ÈÉÍß‚ïÙ"&3TYlGŽ†)†#5ŠA©^T2ÆÁ†)©†©†¤yGXÁ†©"†)©¼ÅV WS†#©]R–r‹q5ÕbâWþBh¯u+ªÅÄ×ü„nÑ^êkªÁj‰jeœWˆÕÕ`µDµ2Ì+Ä µDµµ5Ô{¤Þ0RF’H÷AÛ<ÊÁ§ Æˆæ0Ñ£ê4 öÚµ[¬/1²€â<-¬šö²r×0o×w ´xÐÒË„
g01¬„åEMè›‡Ã\6µE›‘fÙO	k÷CºnÓ1`iîÒ²áÂ!ÑLÄÿ 
³¬\õ*:M=Ç»c‹2[‰ØsÕLò\ŒK L^™üÄN\¶#»
Ç\……ßïIœÉ$¸ÎñÍ®ÔHÃ‡	Ûí/´>Li”„†g^$ 7NÍÑÑ2M2ªNg=k½Ð}Ÿl6‡	—JWˆ”ÞéT·+µ¦üÂFè¢Vs6ýÆ4»2+2IÝ9ô9®ÇGÙÇ1+Ä8;*Õ§ž½D*îÑWÚçá3Ma¤·ˆ¥t}š²Ü…Q+Æüµ8HíS”üxGéÁˆ¦Ël„éÈ	5 µÄƒ6äÜ(kIš¯S†fÖ&¸®×v|Äyˆ*ç¦ŒÉù@7-®Ä[]ÎÖ¿ê`º…=:…žJVnÝ³vðÖKE¼‚ »mà ÷f®:Ë™–4rð¡4` Ü¹çž~‹C=œ…¾Ïj|VDtK.ƒ"9	Ï
­–éXá³6WÒïIL-Ð‡
Ç
PK     ø‹U]RäÀ·5"  5"  	  README.mdnu „[µü¤        # WProofreader plugin for WordPress

Check spelling and grammar on your site automatically with multilingual [WProofreader plugin for WordPress](https://wordpress.org/plugins/webspellchecker/).

![User Interface of WProofreader Plugin for WordPress](https://ps.w.org/webspellchecker/assets/screenshot-2.png?rev=3041860)

## Description

WProofreader plugin for WordPress scans and checks your website content for spelling, grammar, punctuation and style issues. No banner ads on your website!

Compatible with the latest WordPress versions.

**WProofreader Free** is ideal for a small personal blog or lightweight use:

Add instant spell checking to your WordPress site with support for 20+ languages. The free version comes with a daily word limit and applies to a single website, with a per-browser personal dictionary for storing custom words. Basic spelling and grammar check. No cloud sync. A straightforward, no-cost tool for basic proofreading needs.

**WProofreader Pro** for professional sites and teams:

A full-featured writing assistant for WordPress with unlimited websites, and no daily limits. Includes spell and grammar checking in dozens of languages, AI-powered text rephrasing and proofreading, text autocomplete, and a cloud-synced personal dictionary accessible from any device or browser. Subscription owners can also set up organization-wide custom dictionaries shared across all connected sites for keeping terminology consistent everywhere.

Check more differences between the WProofreader plugin free and Pro versions on the [website](https://wproofreader.com/integrations/wordpress).

[![Watch the video](https://img.youtube.com/vi/mhrZN87jydA/sddefault.jpg)](https://www.youtube.com/watch?v=mhrZN87jydA)

### User functionality

**Proofreading modes**

* The check-as-you-type mode instantaneously detects writing mistakes and provides suggestions on-hover.
* The proofread-in-dialog mode aggregates all the issues detected and suggestions provided.

**Customizable dictionaries**

The Pro version includes:

* **User-level custom dictionary** allows end users to create personal dictionaries with specific words and terms (complex words, acronyms, proper names, etc.) and use them while working with WordPress. Once a word is added to a personal dictionary, it will not be considered as a misspelling, and will appear in the suggestions list. All words you add are stored either in the browser local storage or on our cloud and are accessible from the browser.
* **Organization-level custom dictionary** allows creating dictionaries that extend the vocabulary of the standard dictionary with custom words specific to your organization culture, industry, domain, etc. All the words added to a dictionary by the admin will be available to all users within your organization. This type of dictionary is available only under the Pro version.
* **Specialized medical spelling dictionaries** for English, German, French and Spanish, and legal dictionaries for English. Available for both, Free and Pro versions of the plugin.

**Feature-rich**

* Multi-language support
* Spelling autocorrect functionality for instant correction of common typos
* Text autocomplete suggestions functionality for faster writing
* Style check functionality with pre-defined non-inclusive and profanity language check for professional writing
* Style guide builder for creating language rules
* User-level and organization-level dictionaries

**User-friendly and up-to-date**

* Straightforward plugin configuration ([check the video tutorial](https://youtu.be/mhrZN87jydA))
* Visible and intuitive text check with different colors used for spelling, grammar, and style guide issues
* Easily toggle on/off the plugin button
* Support the latest version of WordPress
* Seamlessly integrates with the WordPress editor (Gutenberg) and compatible with the classic editor. Supported browsers: Chrome, Firefox, Safari, Microsoft Edge.
* _Content elements supported: pages, posts, tag descriptions, category descriptions, WooCommerce, WP eCommerce product descriptions, any custom post types, meta description fields of Yoast SEO plugin, all fields in the Admin panel and user-facing editable fields (disabled by default)._

### Benefits of WProofreader plugin for WordPress

* It saves time proofreading in any of the two modes, check-as-you type and proofread-in-dialog.
* You can create and maintain quality posts in different languages without being worried about possible mistakes.
* Advanced functionality for spelling autocorrect, autocomplete suggestions and style guide for faster and more productive writing.

## Installation

**From your WordPress dashboard:**

1. Visit 'Plugins > Add New'.
2. Search for 'WProofreader'.
3. Activate WProofreader from your Plugins page.

**From WordPress.org:**

1. [Download WProofreader plugin](https://downloads.wordpress.org/plugin/webspellchecker.zip).
2. Upload the 'wproofreader' directory to your '/wp-content/plugins/' directory, using your favorite method (ftp, sftp, scp, etc...)
3. Activate WProofreader from your Plugins page.

## Updating

Automatic updates should run smoothly, but it's always a good idea to back up your site, just in case.

## Frequently asked questions

**What is a license key and how to obtain one?**

> A **license key** is a special data string that verifies authorized access to the Pro version of WProofreader plugin. The license key is provided by the WebSpellChecker when the Pro version is purchased.

**Does the WProofreader plugin check the entire website?**

> The plugin doesn't check the whole website at a time. It is enabled only when the content on your website is in editing mode. Here is the list of content types which can be checked by the plugin: content of pages, content of posts, tag descriptions, category descriptions, WooCommerce and WP eCommerce product descriptions, any Custom Post Type, Meta description fields of Yoast SEO plugin, all fields in the Admin panel and user-facing website elements.

**What other languages are available for WProofreader plugin?**

> The Free version and Pro version of WProofreader plugin support [20+ popular languages](https://docs.wproofreader.com/features/supported-languages).

> *Note that not all languages support grammar checks. Languages with grammar enabled are marked.*

**What is a user-level custom dictionary and how can I create one?**

> WProofreader plugin has a special functionality called a custom dictionary. It allows creating personal dictionaries with specific words and terms (complex words, acronyms, proper names, etc.) and using them across your website. Words added to a personal dictionary will not be marked as misspellings.

> To create a new **user-level custom dictionary**, click the badge in the bottom right corner of the text field and select Settings. Then go to Dictionaries. Name your new dictionary and click Create. All new words will be added to a brand-new dictionary. If you have several personal dictionaries, for example, with different term types, new words will be added to the one, which is currently in use. [Check docs](https://docs.wproofreader.com/features/custom-dictionary/user-dictionary-user-manual#creating-a-dictionary).

> To create an **organization-level custom dictionary** to be used across all the websites under your subscription, please follow the instructions in this [guide](https://docs.wproofreader.com/features/custom-dictionary/organization-dictionary-cloud-admin-panel).

**Does WProofreader plugin check grammar as well?**

> Not all languages in the list support grammar checks. Check the [updated list of supported languages](https://docs.wproofreader.com/features/supported-languages) and the ones that can be used for grammar-check.

**Is technical support available for WProofreader plugin?**

> Priority support is provided by WebSpellChecker for Pro users. Community support is available to all users via the [plugin support forum](https://wordpress.org/support/plugin/webspellchecker/). For Pro inquiries, please [contact us](https://wproofreader.com/contact-us).

**What are the Terms of Service?**

> Please see the complete [WebSpellChecker Terms of Service](https://webspellchecker.com/legal/terms-of-service/) for WProofreader plugin.

**Is my text secure? What is your Privacy Policy?**

> We have an extensive Privacy Policy in place to protect your texts. Please see the details [here](https://webspellchecker.com/legal/privacy-policy/).

**What browsers are supported?**

> The plugin supports the latest versions of Chrome, Firefox, Safari, and MS Edge.

**Can I suggest a feature?**

> Sure, please let us know how we can make the plugin more useful for you. Create a new topic under the support section of this plugin.
PK     ø‹U]&„P§ÜZ  ÜZ    vendor/class.settings-api.phpnu „[µü¤        <?php

if ( !class_exists( 'WSC_Settings_API' ) ):
class WSC_Settings_API {

    /**
     * settings sections array
     *
     * @var array
     */
    protected $settings_sections = array();

    /**
     * Settings fields array
     *
     * @var array
     */
    protected $settings_fields = array();

    public function __construct() {
        add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ) );
    }

    /**
     * Enqueue scripts and styles
     */
    function admin_enqueue_scripts() {
        wp_enqueue_style( 'wp-color-picker' );

        wp_enqueue_media();
        wp_enqueue_script( 'wp-color-picker' );
        wp_enqueue_script( 'jquery' );
    }

    /**
     * Set settings sections
     *
     * @param array   $sections setting sections array
     */
    function set_sections( $sections ) {
        $this->settings_sections = $sections;

        return $this;
    }

    /**
     * Add a single section
     *
     * @param array   $section
     */
    function add_section( $section ) {
        $this->settings_sections[] = $section;

        return $this;
    }

    /**
     * Set settings fields
     *
     * @param array   $fields settings fields array
     */
    function set_fields( $fields ) {
        $this->settings_fields = $fields;

        return $this;
    }

    function add_field( $section, $field ) {
        $defaults = array(
            'name'  => '',
            'label' => '',
            'desc'  => '',
            'type'  => 'text'
        );

        $arg = wp_parse_args( $field, $defaults );
        $this->settings_fields[$section][] = $arg;

        return $this;
    }

    /**
     * Initialize and registers the settings sections and fileds to WordPress
     *
     * Usually this should be called at `admin_init` hook.
     *
     * This function gets the initiated settings sections and fields. Then
     * registers them to WordPress and ready for use.
     */
    function admin_init() {
        //register settings sections
        foreach ( $this->settings_sections as $section ) {
            if ( false == get_option( $section['id'] ) ) {
                add_option( $section['id'] );
            }

            if ( isset($section['desc']) && !empty($section['desc']) ) {
                $desc = '<div class="inside">' . $section['desc'] . '</div>';
                $callback = function () use ( $desc ) {
                    echo wp_kses_post( $desc );
                };
            } else if ( isset( $section['callback'] ) ) {
                $callback = $section['callback'];
            } else {
                $callback = null;
            }

            add_settings_section( $section['id'], $section['title'], $callback, $section['id'] );
        }

        //register settings fields
        foreach ( $this->settings_fields as $section => $field ) {
            foreach ( $field as $option ) {

                $name = $option['name'];
                $type = isset( $option['type'] ) ? $option['type'] : 'text';
                $label = isset( $option['label'] ) ? $option['label'] : '';
                $callback = isset( $option['callback'] ) ? $option['callback'] : array( $this, 'callback_' . $type );

                $args = array(
                    'id'                => $name,
                    'label_for'         => "{$section}[{$name}]",
                    'desc'              => isset( $option['desc'] ) ? $option['desc'] : '',
                    'name'              => $label,
                    'section'           => $section,
                    'size'              => isset( $option['size'] ) ? $option['size'] : null,
                    'options'           => isset( $option['options'] ) ? $option['options'] : '',
                    'std'               => isset( $option['default'] ) ? $option['default'] : '',
                    'sanitize_callback' => isset( $option['sanitize_callback'] ) ? $option['sanitize_callback'] : '',
                    'type'              => $type,
                    'placeholder'       => isset( $option['placeholder'] ) ? $option['placeholder'] : '',
                    'min'               => isset( $option['min'] ) ? $option['min'] : '',
                    'max'               => isset( $option['max'] ) ? $option['max'] : '',
                    'step'              => isset( $option['step'] ) ? $option['step'] : '',
                );

                add_settings_field( "{$section}[{$name}]", $label, $callback, $section, $section, $args );
            }
        }

        // creates our settings in the options table
        foreach ( $this->settings_sections as $section ) {
            register_setting( $section['id'], $section['id'], array( $this, 'sanitize_options' ) );
        }
    }

    /**
     * Get field description for display
     *
     * @param array   $args settings field args
     */
    public function get_field_description( $args ) {
        if ( ! empty( $args['desc'] ) ) {
            $desc = sprintf( '<p class="description">%s</p>', wp_kses_post( $args['desc'] ) );
        } else {
            $desc = '';
        }

        return $desc;
    }

    /**
     * Displays a text field for a settings field
     *
     * @param array   $args settings field args
     */
    function callback_text( $args ) {

        $value       = esc_attr( $this->get_option( $args['id'], $args['section'], $args['std'] ) );
        $size        = isset( $args['size'] ) && !is_null( $args['size'] ) ? $args['size'] : 'regular';
        $type        = isset( $args['type'] ) ? $args['type'] : 'text';
        $placeholder = empty( $args['placeholder'] ) ? '' : ' placeholder="' . esc_attr( $args['placeholder'] ) . '"';

        $html        = sprintf( '<input type="%1$s" class="%2$s-text" id="%3$s[%4$s]" name="%3$s[%4$s]" value="%5$s"%6$s/>', esc_attr( $type ), esc_attr( $size ), esc_attr( $args['section'] ), esc_attr( $args['id'] ), $value, $placeholder );
        $html       .= $this->get_field_description( $args );

        echo $html;
    }

    /**
     * Displays a url field for a settings field
     *
     * @param array   $args settings field args
     */
    function callback_url( $args ) {
        $this->callback_text( $args );
    }

    /**
     * Displays a number field for a settings field
     *
     * @param array   $args settings field args
     */
    function callback_number( $args ) {
        $value       = esc_attr( $this->get_option( $args['id'], $args['section'], $args['std'] ) );
        $size        = isset( $args['size'] ) && !is_null( $args['size'] ) ? $args['size'] : 'regular';
        $type        = isset( $args['type'] ) ? $args['type'] : 'number';
        $placeholder = empty( $args['placeholder'] ) ? '' : ' placeholder="' . esc_attr( $args['placeholder'] ) . '"';
        $min         = empty( $args['min'] ) ? '' : ' min="' . esc_attr( $args['min'] ) . '"';
        $max         = empty( $args['max'] ) ? '' : ' max="' . esc_attr( $args['max'] ) . '"';
        $step        = empty( $args['max'] ) ? '' : ' step="' . esc_attr( $args['step'] ) . '"';

        $html        = sprintf( '<input type="%1$s" class="%2$s-number" id="%3$s[%4$s]" name="%3$s[%4$s]" value="%5$s"%6$s%7$s%8$s%9$s/>', esc_attr( $type ), esc_attr( $size ), esc_attr( $args['section'] ), esc_attr( $args['id'] ), $value, $placeholder, $min, $max, $step );
        $html       .= $this->get_field_description( $args );

        echo $html;
    }

    /**
     * Displays a checkbox for a settings field
     *
     * @param array   $args settings field args
     */
    function callback_checkbox( $args ) {

        $value = esc_attr( $this->get_option( $args['id'], $args['section'], $args['std'] ) );

        $html  = '<fieldset>';
        $html  .= sprintf( '<label for="wpuf-%1$s[%2$s]">', esc_attr( $args['section'] ), esc_attr( $args['id'] ) );
        $html  .= sprintf( '<input type="hidden" name="%1$s[%2$s]" value="off" />', esc_attr( $args['section'] ), esc_attr( $args['id'] ) );
        $html  .= sprintf( '<input type="checkbox" class="checkbox" id="wpuf-%1$s[%2$s]" name="%1$s[%2$s]" value="on" %3$s />', esc_attr( $args['section'] ), esc_attr( $args['id'] ), checked( $value, 'on', false ) );
        $html  .= sprintf( '%1$s</label>', wp_kses_post( $args['desc'] ) );
        $html  .= '</fieldset>';

        echo $html;
    }

    /**
     * Displays a multicheckbox a settings field
     *
     * @param array   $args settings field args
     */
    function callback_multicheck( $args ) {

        $value = $this->get_option( $args['id'], $args['section'], $args['std'] );
        $html  = '<fieldset>';
        $html .= sprintf( '<input type="hidden" name="%1$s[%2$s]" value="" />', esc_attr( $args['section'] ), esc_attr( $args['id'] ) );
        foreach ( $args['options'] as $key => $label ) {
            $checked = isset( $value[$key] ) ? $value[$key] : '0';
            $html    .= sprintf( '<label for="wpuf-%1$s[%2$s][%3$s]">', esc_attr( $args['section'] ), esc_attr( $args['id'] ), esc_attr( $key ) );
            $html    .= sprintf( '<input type="checkbox" class="checkbox" id="wpuf-%1$s[%2$s][%3$s]" name="%1$s[%2$s][%3$s]" value="%3$s" %4$s />', esc_attr( $args['section'] ), esc_attr( $args['id'] ), esc_attr( $key ), checked( $checked, $key, false ) );
            $html    .= sprintf( '%1$s</label><br>',  esc_html( $label ) );
        }

        $html .= $this->get_field_description( $args );
        $html .= '</fieldset>';

        echo $html;
    }

    /**
     * Displays a multicheckbox a settings field
     *
     * @param array   $args settings field args
     */
    function callback_radio( $args ) {

        $value = $this->get_option( $args['id'], $args['section'], $args['std'] );
        $html  = '<fieldset>';

        foreach ( $args['options'] as $key => $label ) {
            $html .= sprintf( '<label for="wpuf-%1$s[%2$s][%3$s]">',  esc_attr( $args['section'] ), esc_attr( $args['id'] ), esc_attr( $key ) );
            $html .= sprintf( '<input type="radio" class="radio" id="wpuf-%1$s[%2$s][%3$s]" name="%1$s[%2$s]" value="%3$s" %4$s />', esc_attr( $args['section'] ), esc_attr( $args['id'] ), esc_attr( $key ), checked( $value, $key, false ) );
            $html .= sprintf( '%1$s</label><br>', esc_html( $label ) );
        }

        $html .= $this->get_field_description( $args );
        $html .= '</fieldset>';

        echo $html;
    }

    /**
     * Displays a selectbox for a settings field
     *
     * @param array   $args settings field args
     */
    function callback_select( $args ) {

        $value = esc_attr( $this->get_option( $args['id'], $args['section'], $args['std'] ) );
        $size  = isset( $args['size'] ) && !is_null( $args['size'] ) ? $args['size'] : 'regular';
        $html  = sprintf( '<select class="%1$s" name="%2$s[%3$s]" id="%2$s[%3$s]">', esc_attr( $size ), esc_attr( $args['section'] ), esc_attr( $args['id'] ) );

        foreach ( $args['options'] as $key => $label ) {
            $html .= sprintf( '<option value="%s"%s>%s</option>', esc_attr( $key ), selected( $value, $key, false ), esc_html( $label ) );
        }

        $html .= sprintf( '</select>' );
        $html .= $this->get_field_description( $args );

        echo $html;
    }

    /**
     * Displays a textarea for a settings field
     *
     * @param array   $args settings field args
     */
    function callback_textarea( $args ) {

        $value       = esc_textarea( $this->get_option( $args['id'], $args['section'], $args['std'] ) );
        $size        = isset( $args['size'] ) && !is_null( $args['size'] ) ? $args['size'] : 'regular';
        $placeholder = empty( $args['placeholder'] ) ? '' : ' placeholder="' . esc_attr( $args['placeholder'] ) . '"';

        $html        = sprintf( '<textarea rows="5" cols="55" class="%1$s-text" id="%2$s[%3$s]" name="%2$s[%3$s]"%4$s>%5$s</textarea>', esc_attr( $size ), esc_attr( $args['section'] ), esc_attr( $args['id'] ), $placeholder, $value );
        $html        .= $this->get_field_description( $args );

        echo $html;
    }

    /**
     * Displays a textarea for a settings field
     *
     * @param array   $args settings field args
     * @return string
     */
    function callback_html( $args ) {
        echo $this->get_field_description( $args );
    }

    /**
     * Displays a rich text textarea for a settings field
     *
     * @param array   $args settings field args
     */
    function callback_wysiwyg( $args ) {

        $value = $this->get_option( $args['id'], $args['section'], $args['std'] );
        $size  = isset( $args['size'] ) && !is_null( $args['size'] ) ? $args['size'] : '500px';

        echo '<div style="max-width: ' . esc_attr( $size ) . ';">';

        $editor_settings = array(
            'teeny'         => true,
            'textarea_name' => $args['section'] . '[' . $args['id'] . ']',
            'textarea_rows' => 10
        );

        if ( isset( $args['options'] ) && is_array( $args['options'] ) ) {
            $editor_settings = array_merge( $editor_settings, $args['options'] );
        }

        wp_editor( $value, $args['section'] . '-' . $args['id'], $editor_settings );

        echo '</div>';

        echo $this->get_field_description( $args );
    }

    /**
     * Displays a file upload field for a settings field
     *
     * @param array   $args settings field args
     */
    function callback_file( $args ) {

        $value = esc_attr( $this->get_option( $args['id'], $args['section'], $args['std'] ) );
        $size  = isset( $args['size'] ) && !is_null( $args['size'] ) ? $args['size'] : 'regular';
        $id    = $args['section']  . '[' . $args['id'] . ']';
        $label = isset( $args['options']['button_label'] ) ? $args['options']['button_label'] : __( 'Choose File' );

        $html  = sprintf( '<input type="text" class="%1$s-text wpsa-url" id="%2$s[%3$s]" name="%2$s[%3$s]" value="%4$s"/>', esc_attr( $size ), esc_attr( $args['section'] ), esc_attr( $args['id'] ), $value );
        $html  .= '<input type="button" class="button wpsa-browse" value="' . esc_attr( $label ) . '" />';
        $html  .= $this->get_field_description( $args );

        echo $html;
    }

    /**
     * Displays a password field for a settings field
     *
     * @param array   $args settings field args
     */
    function callback_password( $args ) {

        $value = esc_attr( $this->get_option( $args['id'], $args['section'], $args['std'] ) );
        $size  = isset( $args['size'] ) && !is_null( $args['size'] ) ? $args['size'] : 'regular';

        $html  = sprintf( '<input type="password" class="%1$s-text" id="%2$s[%3$s]" name="%2$s[%3$s]" value="%4$s"/>', esc_attr( $size ), esc_attr( $args['section'] ), esc_attr( $args['id'] ), $value );
        $html  .= $this->get_field_description( $args );

        echo $html;
    }

    /**
     * Displays a color picker field for a settings field
     *
     * @param array   $args settings field args
     */
    function callback_color( $args ) {

        $value = esc_attr( $this->get_option( $args['id'], $args['section'], $args['std'] ) );
        $size  = isset( $args['size'] ) && !is_null( $args['size'] ) ? $args['size'] : 'regular';

        $html  = sprintf( '<input type="text" class="%1$s-text wp-color-picker-field" id="%2$s[%3$s]" name="%2$s[%3$s]" value="%4$s" data-default-color="%5$s" />', esc_attr( $size ), esc_attr( $args['section'] ), esc_attr( $args['id'] ), $value, esc_attr( $args['std'] ) );
        $html  .= $this->get_field_description( $args );

        echo $html;
    }

    /**
     * Sanitize callback for Settings API
     *
     * @return mixed
     */
    function sanitize_options( $options ) {

        if ( !$options ) {
            return $options;
        }

        foreach( $options as $option_slug => $option_value ) {
            $sanitize_callback = $this->get_sanitize_callback( $option_slug );

            // If callback is set, call it
            if ( $sanitize_callback ) {
                $options[ $option_slug ] = call_user_func( $sanitize_callback, $option_value );
                continue;
            }
        }

        return $options;
    }

    /**
     * Get sanitization callback for given option slug
     *
     * @param string $slug option slug
     *
     * @return mixed string or bool false
     */
    function get_sanitize_callback( $slug = '' ) {
        if ( empty( $slug ) ) {
            return false;
        }

        // Iterate over registered fields and see if we can find proper callback
        foreach( $this->settings_fields as $section => $options ) {
            foreach ( $options as $option ) {
                if ( $option['name'] != $slug ) {
                    continue;
                }

                // Return the callback name
                return isset( $option['sanitize_callback'] ) && is_callable( $option['sanitize_callback'] ) ? $option['sanitize_callback'] : false;
            }
        }

        return false;
    }

    /**
     * Get the value of a settings field
     *
     * @param string  $option  settings field name
     * @param string  $section the section name this field belongs to
     * @param string  $default default text if it's not found
     * @return string
     */
    function get_option( $option, $section, $default = '' ) {

        $options = get_option( $section );

        if ( isset( $options[$option] ) ) {
            return $options[$option];
        }

        return $default;
    }

    /**
     * Show navigations as tab
     *
     * Shows all the settings section labels as tab
     */
    function show_navigation() {
        $html = '<h2 class="nav-tab-wrapper">';

        $count = count( $this->settings_sections );

        // don't show the navigation if only one section exists
        if ( $count === 1 ) {
            return;
        }

        foreach ( $this->settings_sections as $tab ) {
            $html .= sprintf( '<a href="#%1$s" class="nav-tab" id="%1$s-tab">%2$s</a>', esc_attr( $tab['id'] ), esc_html( $tab['title'] ) );
        }

        $html .= '</h2>';

        echo $html;
    }

    /**
     * Show the section settings forms
     *
     * This function displays every sections in a different form
     */
    function show_forms() {
        ?>
        <div class="metabox-holder">
            <?php foreach ( $this->settings_sections as $form ) { ?>
                <div id="<?php echo esc_attr( $form['id'] ); ?>" class="group" style="display: none;">
                    <form method="post" action="options.php">
                        <?php
                        do_action( 'wsa_form_top_' . $form['id'], $form );
                        settings_fields( $form['id'] );
                        do_settings_sections( $form['id'] );
                        do_action( 'wsa_form_bottom_' . $form['id'], $form );
                        if ( isset( $this->settings_fields[ $form['id'] ] ) ):
                        ?>
                            <div><i>We encourage you to take the time to review our revised <a href="<?php echo esc_url( 'https://webspellchecker.com/privacy-policy/' ); ?>" target="_blank">Privacy Policy</a> ‌and <a href="<?php echo esc_url( 'https://webspellchecker.com/terms-of-service/' ); ?>" target="_blank">Terms of Service</a>. By continuing to use WebSpellChecker Services, you acknowledge our Privacy Policy and agree to our Terms of Service.</i></div>
                        <div style="padding-left: 10px">
                            <?php submit_button(); ?>
                        </div>
                        <?php endif; ?>
                    </form>
                </div>
            <?php } ?>
        </div>
        <?php
        $this->script();
    }

    /**
     * Tabbable JavaScript codes & Initiate Color Picker
     *
     * This code uses localstorage for displaying active tabs
     */
    function script() {
        ?>
        <script>
            jQuery(document).ready(function($) {
                //Initiate Color Picker
                $('.wp-color-picker-field').wpColorPicker();

                // Switches option sections
                $('.group').hide();
                var activetab = '';
                if (typeof(localStorage) != 'undefined' ) {
                    activetab = localStorage.getItem("activetab");
                }
                if (activetab != '' && $(activetab).length ) {
                    $(activetab).fadeIn();
                } else {
                    $('.group:first').fadeIn();
                }
                $('.group .collapsed').each(function(){
                    $(this).find('input:checked').parent().parent().parent().nextAll().each(
                    function(){
                        if ($(this).hasClass('last')) {
                            $(this).removeClass('hidden');
                            return false;
                        }
                        $(this).filter('.hidden').removeClass('hidden');
                    });
                });

                if (activetab != '' && $(activetab + '-tab').length ) {
                    $(activetab + '-tab').addClass('nav-tab-active');
                }
                else {
                    $('.nav-tab-wrapper a:first').addClass('nav-tab-active');
                }
                $('.nav-tab-wrapper a').click(function(evt) {
                    $('.nav-tab-wrapper a').removeClass('nav-tab-active');
                    $(this).addClass('nav-tab-active').blur();
                    var clicked_group = $(this).attr('href');
                    if (typeof(localStorage) != 'undefined' ) {
                        localStorage.setItem("activetab", $(this).attr('href'));
                    }
                    $('.group').hide();
                    $(clicked_group).fadeIn();
                    evt.preventDefault();
                });

                $('.wpsa-browse').on('click', function (event) {
                    event.preventDefault();

                    var self = $(this);

                    // Create the media frame.
                    var file_frame = wp.media.frames.file_frame = wp.media({
                        title: self.data('uploader_title'),
                        button: {
                            text: self.data('uploader_button_text'),
                        },
                        multiple: false
                    });

                    file_frame.on('select', function () {
                        attachment = file_frame.state().get('selection').first().toJSON();
                        self.prev('.wpsa-url').val(attachment.url).change();
                    });

                    // Finally, open the modal
                    file_frame.open();
                });
        });
        </script>
        <?php
        $this->_style_fix();
    }

    function _style_fix() {
        global $wp_version;

        if (version_compare($wp_version, '3.8', '<=')):
        ?>
        <style type="text/css">
            /** WordPress 3.8 Fix **/
            .form-table th { padding: 20px 10px; }
            #wpbody-content .metabox-holder { padding-top: 5px; }
        </style>
        <?php
        endif;
    }

}

endif;
PK     ø‹U]1Gn%  %    tests/test-ajax-payload.phpnu „[µü¤        <?php
/**
 * Standalone regression tests for WProofreader_Ajax payload parsing.
 *
 * Usage: php tests/test-ajax-payload.php
 */

define( 'ABSPATH', dirname( __DIR__ ) . '/' );
define( 'DAY_IN_SECONDS', 86400 );

function wp_unslash( $value ) {
	return is_array( $value ) ? array_map( 'wp_unslash', $value ) : stripslashes( $value );
}

function wp_json_encode( $value ) {
	return json_encode( $value );
}

require dirname( __DIR__ ) . '/includes/class-wproofreader-ajax.php';

$failures = 0;
$method   = new ReflectionMethod( 'WProofreader_Ajax', 'parse_payload' );

function check_ajax_payload( string $name, bool $condition ) {
	global $failures;

	if ( $condition ) {
		echo "PASS  {$name}\n";
		return;
	}

	$failures++;
	echo "FAIL  {$name}\n";
}

$valid = array(
	'langList' => array(
		'ltr' => array( 'en_US' => 'English' ),
		'rtl' => array(),
	),
);

check_ajax_payload(
	'valid JSON payload accepted',
	$method->invoke( null, wp_json_encode( $valid ) ) === $valid
);
check_ajax_payload(
	'valid array payload accepted',
	$method->invoke( null, $valid ) === $valid
);
check_ajax_payload(
	'oversized JSON payload rejected',
	null === $method->invoke( null, wp_json_encode( array( 'data' => str_repeat( 'x', 65536 ) ) ) )
);
check_ajax_payload(
	'oversized array payload rejected',
	null === $method->invoke( null, array( 'data' => str_repeat( 'x', 65536 ) ) )
);
check_ajax_payload(
	'invalid JSON payload rejected',
	null === $method->invoke( null, '{"langList":' )
);

echo $failures ? "\n{$failures} FAILURE(S)\n" : "\nALL PASS\n";
exit( $failures ? 1 : 0 );
PK     ø‹U]¯äb±X  X    tests/test-content-cleaner.phpnu „[µü¤        <?php
/**
 * Standalone test runner for WProofreader_Content_Cleaner.
 *
 * Usage (inside a WP container): php tests/test-content-cleaner.php /path/to/wp-load.php
 * Exits non-zero on failure.
 */

$wp_load = $argv[1] ?? '/var/www/web/wp/wp-load.php';
require $wp_load;

if ( ! class_exists( 'WProofreader_Content_Cleaner' ) ) {
	require dirname( __DIR__ ) . '/includes/class-wproofreader-content-cleaner.php';
}

$failures = 0;

function check( string $name, bool $condition, string $detail = '' ) {
	global $failures;
	if ( $condition ) {
		echo "PASS  {$name}\n";
	} else {
		$failures++;
		echo "FAIL  {$name}" . ( $detail ? " — {$detail}" : '' ) . "\n";
	}
}

function clean( string $html ): string {
	return WProofreader_Content_Cleaner::clean_content( $html );
}

function hook_path( string $html ): string {
	$data = WProofreader_Content_Cleaner::clean_post_data(
		array( 'post_content' => wp_slash( $html ), 'post_status' => 'private' ),
		array()
	);
	return wp_unslash( $data['post_content'] );
}

// 1. Clean content without artifact markers is untouched (wsc- URLs included).
$plain = '<p>Hello <a href="https://webspellchecker.com/wsc-proofreader/">link</a></p>';
check( 'clean content untouched', clean( $plain ) === $plain );

// 2. Simple artifact span is unwrapped.
check(
	'simple span unwrapped',
	clean( '<p><span class="wsc-spelling-problem">word</span> rest</p>' ) === '<p>word rest</p>'
);

// 3. Multi-class artifact span with extra attributes.
check(
	'multi-class span with attributes',
	clean( '<p><span data-wsc-id="9" class="wsc-spelling-problem wsc-problem-42" style="background:red">word</span></p>' ) === '<p>word</p>'
);

// 4. Nested artifact spans.
check(
	'nested artifact spans',
	clean( '<p><span class="wsc-grammar-problem">a <span class="wsc-spelling-problem">b</span> c</span></p>' ) === '<p>a b c</p>'
);

// 5. Artifact span nested inside a regular span: regular span preserved byte-identical.
check(
	'regular span preserved',
	clean( "<p><span class='keep' data-x='1'>a <span class=\"wsc-spelling-problem\">b</span></span></p>" ) === "<p><span class='keep' data-x='1'>a b</span></p>"
);

// 6. rangySelectionBoundary removal.
check(
	'rangy boundary removed',
	clean( "<p>a<span class=\"rangySelectionBoundary\">\xE2\x80\x8B</span>b</p>" ) === "<p>a\xE2\x80\x8Bb</p>"
);

// 7. Unclosed artifact opener: opener dropped, rest intact.
check(
	'unclosed artifact opener',
	clean( '<p><span class="wsc-spelling-problem">word</p>' ) === '<p>word</p>'
);

// 8. Quoting / entities / boolean attributes outside artifacts stay byte-identical.
$tricky = '<figure class=\'"wp-block-table\' custom><a href=\'"https://x"\' rel=\'"nofollow"\'>x</a>'
	. '<span class="wsc-spelling-problem">w</span>&nbsp;&amp;  <br><svg viewBox="0 0 1 1"><rect/></svg></figure>';
$expected = '<figure class=\'"wp-block-table\' custom><a href=\'"https://x"\' rel=\'"nofollow"\'>x</a>'
	. 'w&nbsp;&amp;  <br><svg viewBox="0 0 1 1"><rect/></svg></figure>';
check( 'byte-identical outside splices', clean( $tricky ) === $expected );

// 9. Gutenberg block comments survive.
$blocks = "<!-- wp:paragraph -->\n<p><span class=\"wsc-grammar-problem\">Текст українською</span> 🚀</p>\n<!-- /wp:paragraph -->";
check(
	'block comments and unicode survive',
	clean( $blocks ) === "<!-- wp:paragraph -->\n<p>Текст українською 🚀</p>\n<!-- /wp:paragraph -->"
);

// 10. Slashed hook path: regression for the 3.1.1 corruption bug.
// Realistic Gutenberg fixture: quoted block attributes, existing &quot; entities,
// and a wsc- URL that used to false-positive the old substring guard.
$baseline = '<!-- wp:heading {"level":2,"anchor":"h-custom"} -->' . "\n"
	. '<h2 class="wp-block-heading" id="h-custom">Say &quot;hi&quot;</h2>' . "\n"
	. '<!-- /wp:heading -->' . "\n"
	. '<!-- wp:image {"id":42,"sizeSlug":"large","linkDestination":"custom"} -->' . "\n"
	. '<figure class="wp-block-image size-large"><a href="https://webspellchecker.com/wsc-proofreader/">'
	. '<img src="https://x.test/a.png" alt="alt with &quot;quotes&quot;"/></a></figure>' . "\n"
	. '<!-- /wp:image -->' . "\n"
	. '<!-- wp:paragraph -->' . "\n"
	. '<p>Plain <a href="https://webspellchecker.com/wsc-proofreader/" rel="noopener">link</a> text.</p>' . "\n"
	. '<!-- /wp:paragraph -->';
if ( file_exists( '/tmp/before_editor_save.html' ) ) {
	$real = file_get_contents( '/tmp/before_editor_save.html' );
	check( 'slashed hook path lossless on real post baseline', hook_path( $real ) === $real );
}
check( 'slashed hook path lossless on clean content', hook_path( $baseline ) === $baseline );

// 11. Slashed hook path with artifacts: cleaned, no &quot; inflation.
$dirty = str_replace( '<p>', '<p><span class="wsc-spelling-problem">x</span> ', $baseline );
$cleaned = hook_path( $dirty );
check( 'slashed hook path removes artifacts', strpos( $cleaned, 'wsc-spelling-problem' ) === false );
check(
	'slashed hook path adds no quot entities',
	substr_count( $cleaned, '&quot;' ) === substr_count( $baseline, '&quot;' )
);

// 11b. Double invocation (Gutenberg REST save + meta-box-loader re-save) is idempotent.
$pass1 = hook_path( $dirty );
$pass2 = hook_path( $pass1 );
check( 'double hook run idempotent', $pass2 === $pass1 );
check( 'double hook run adds no quot entities', substr_count( $pass2, '&quot;' ) === substr_count( $baseline, '&quot;' ) );

// 12. Autosave/revision statuses are no longer skipped.
$autosave = WProofreader_Content_Cleaner::clean_post_data(
	array( 'post_content' => wp_slash( '<p><span class="wsc-spelling-problem">w</span></p>' ), 'post_status' => 'inherit' ),
	array()
);
check( 'inherit status cleaned', wp_unslash( $autosave['post_content'] ) === '<p>w</p>' );

// 13. Empty content untouched.
$empty = WProofreader_Content_Cleaner::clean_post_data( array( 'post_content' => '' ), array() );
check( 'empty content untouched', '' === $empty['post_content'] );

// 14. Span with artifact class substring in another class name must NOT match.
$lookalike = '<p><span class="wsc-spelling-problem-note">keep wrapper</span></p>';
check( 'class lookalike preserved', clean( $lookalike ) === $lookalike );

echo $failures ? "\n{$failures} FAILURE(S)\n" : "\nALL PASS\n";
exit( $failures ? 1 : 0 );
PK     ø‹U]{۝$&  &    uninstall.phpnu „[µü¤        <?php

/**
 * The code in this file runs when a plugin is uninstalled from the WordPress dashboard.
 */

/* If uninstall is not called from WordPress exit. */
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
	exit;
}

/**
 * Remove all plugin options for the current site.
 */
function wsc_proofreader_uninstall_site() {
	delete_option( 'wsc' );
	delete_option( 'wsc_proofreader_version' );
	delete_option( 'wsc_proofreader_info' );
	delete_option( 'wsc_proofreader' );
	delete_transient( 'wsc_proofreader_info_cache' );
}

if ( is_multisite() ) {
	$wsc_site_ids = get_sites( array( 'fields' => 'ids', 'number' => 0 ) );
	foreach ( $wsc_site_ids as $wsc_site_id ) {
		switch_to_blog( $wsc_site_id );
		wsc_proofreader_uninstall_site();
		restore_current_blog();
	}
} else {
	wsc_proofreader_uninstall_site();
}
PK     ø‹U]ÐâšV  V    assets/instance.jsnu „[µü¤        /**
 * WProofreader Settings Page
 * Initializes the spell checker instance and fetches languages.
 */
jQuery(function ($) {
    'use strict';

    // Guards
    if (typeof WEBSPELLCHECKER === 'undefined' || typeof ProofreaderInstance === 'undefined') return;

    const serviceConfig = window.WSCServiceConfig || {};
    const $root = $('#wsc_proofreader');
    let $select = $root.find('select[name="wsc_proofreader[slang]"]');
    if (!$select.length) return;

    const enableGrammar = (ProofreaderInstance.enableGrammar === 'true');

    // Init WebSpellChecker
    const app = WEBSPELLCHECKER.initWebApi({
        autoSearch: true,
        serviceProtocol: serviceConfig.serviceProtocol || 'https',
        serviceHost: serviceConfig.serviceHost || 'svc.webspellchecker.net',
        servicePath: serviceConfig.servicePath || 'api',
        servicePort: serviceConfig.servicePort || '443',
        enableGrammar: enableGrammar,
        serviceId: ProofreaderInstance.key_for_proofreader,
        lang: ProofreaderInstance.slang,
        appType: 'wp_plugin'
    });
    // Get info → send to WP → render languages
    app.getInfo({
        success(result) {
            $.ajax({
                type: 'POST',
                url: window.ajaxurl,
                data: {
                    action: 'get_proofreader_info_callback',
                    security: ProofreaderInstance.ajax_nonce,
                    getInfoResult: result
                }
            })
                .done((res) => {
                    // Support both: raw HTML string OR {success:true, data:"..."} OR {success:true, data:{html:"..."}}
                    const html =
                        (typeof res === 'string') ? res :
                            (res && res.success && res.data && typeof res.data.html === 'string') ? res.data.html :
                                (res && res.success && typeof res.data === 'string') ? res.data : '';

                    if (!html) {
                        displayError('Failed to load language list. Invalid response format.');
                        return;
                    }

                    replaceLanguageSelect(html);
                })
                .fail((jqXHR, textStatus) => {
                    let msg = 'Failed to load language list.';
                    if (jqXHR && jqXHR.status === 403) {
                        msg = 'Your session has expired. Reload the page and try again.';
                    } else if (jqXHR && jqXHR.responseJSON && jqXHR.responseJSON.data && jqXHR.responseJSON.data.message) {
                        msg = jqXHR.responseJSON.data.message;
                    } else if (textStatus) {
                        msg += ` (${textStatus})`;
                    }
                    displayError(msg);
                });
        },
        error(err) {
            displayError((err && err.message) || 'Unexpected initialization error.');
        }
    });

    function replaceLanguageSelect(html) {
        const nodes = $.parseHTML(html, document, false) || [];
        const $newSelect = $(nodes).filter('select[name="wsc_proofreader[slang]"]').first();

        if ($newSelect.length) {
            $select.replaceWith($newSelect);
            $select = $root.find('select[name="wsc_proofreader[slang]"]');
            return;
        }

        const $options = $(nodes).filter('option');
        if ($options.length) {
            $select.empty().append($options);
            return;
        }

        displayError('Failed to load language list. Invalid markup.');
    }

    function displayError(message) {
        const html =
            '<div class="error">' +
            `<p><strong>WProofreader:</strong> ${escapeHtml(message)}</p>` +
            '<p><a target="_blank" href="https://webspellchecker.com/contact-us/">Contact us</a> for technical assistance.</p>' +
            '</div>';
        $root.prepend(html);
    }

    function escapeHtml(str) {
        return String(str)
            .replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#039;');
    }
});
PK     ø‹U]ÝÐgK  K    assets/classic-environment.jsnu „[µü¤        (function () {
    'use strict';

    const EDITOR_ID = 'content';
    // Own marker (set below) plus the attribute the SDK sets on its container.
    const OWN_INSTANCE_ATTRIBUTE = 'data-wsc-init';
    const SDK_INSTANCE_ATTRIBUTE = 'data-wpr-instance';
    const INIT_DELAY = 100;
    const INIT_TIMEOUT = 5000;
    const MAX_ATTEMPTS = 50;

    let attempts = 0;

    function editorInstance() {
        if (!window.tinymce || typeof window.tinymce.get !== 'function') {
            return null;
        }

        return window.tinymce.get(EDITOR_ID);
    }

    function editorIframe(editor) {
        const contentArea = editor.getContentAreaContainer && editor.getContentAreaContainer();

        return editor.iframeElement ||
            (contentArea && contentArea.querySelector('iframe'));
    }

    function hasInstance(iframe) {
        // The SDK marks the container (the iframe element) with data-wpr-instance;
        // the temporary own marker prevents duplicate concurrent initialization.
        if (iframe.hasAttribute(OWN_INSTANCE_ATTRIBUTE) || iframe.hasAttribute(SDK_INSTANCE_ATTRIBUTE)) {
            return true;
        }

        const body = iframe.contentDocument && iframe.contentDocument.body;

        return Boolean(body && (body.hasAttribute(OWN_INSTANCE_ATTRIBUTE) || body.hasAttribute(SDK_INSTANCE_ATTRIBUTE)));
    }

    function initializeClassicEditor() {
        attempts++;

        const editor = editorInstance();
        const iframe = editor && editorIframe(editor);

        if (!editor || !iframe || !window.WEBSPELLCHECKER || !window.WEBSPELLCHECKER_CONFIG) {
            if (attempts < MAX_ATTEMPTS) {
                window.setTimeout(initializeClassicEditor, INIT_DELAY);
            }

            return;
        }

        if (hasInstance(iframe)) {
            return;
        }

        iframe.setAttribute(OWN_INSTANCE_ATTRIBUTE, '1');

        // The SDK merges the global WEBSPELLCHECKER_CONFIG itself; pass only the container
        // (same convention as gutenberg-environment.js).
        try {
            window.WEBSPELLCHECKER.init({
                container: iframe,
            });

            window.setTimeout(() => {
                const body = iframe.contentDocument && iframe.contentDocument.body;
                const sdkInstanceCreated = iframe.hasAttribute(SDK_INSTANCE_ATTRIBUTE) ||
                    Boolean(body && body.hasAttribute(SDK_INSTANCE_ATTRIBUTE));

                if (!sdkInstanceCreated) {
                    iframe.removeAttribute(OWN_INSTANCE_ATTRIBUTE);
                }
            }, INIT_TIMEOUT);
        } catch (e) {
            iframe.removeAttribute(OWN_INSTANCE_ATTRIBUTE);
        }
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initializeClassicEditor);
    } else {
        initializeClassicEditor();
    }
})();
PK     ø‹U]N
‡c  c    assets/environmentChecker.jsnu „[µü¤        window.addEventListener("load", (event) => {
    var serviceConfig = window.WSCServiceConfig || {};
    var bundlePath = serviceConfig.bundleUrl || 'https://svc.webspellchecker.net/spellcheck31/wscbundle/wscbundle.js';

    function loadScript(doc, src) {
        // One bundle copy per document. This script runs both in the top frame
        // and inside the editor-canvas iframe, and the top copy also injects
        // into the iframe, so without the flag the bundle could load twice.
        var win = doc.defaultView;
        if (!win || win.__wscBundleRequested) {
            return;
        }
        win.__wscBundleRequested = true;

        const script = doc.createElement('script');
        const appendTo = doc.head || doc.documentElement;

        script.type = 'text/javascript';
        script.async = false;
        script.src = src;

        appendTo.appendChild(script);
    }

    if (!window.WEBSPELLCHECKER_CONFIG) {
        return;
    }

    window.gutenbergIframe = document.querySelector('[name=editor-canvas]');

    if (window.gutenbergIframe) {
        // The iframe document hosts the editable content and shows the badge;
        // the top frame keeps a badge-less SDK for classic metabox fields.
        const iframeConfig = window.gutenbergIframe.contentWindow.WEBSPELLCHECKER_CONFIG;
        if (iframeConfig && window.WEBSPELLCHECKER_CONFIG.globalBadge) {
            window.WEBSPELLCHECKER_CONFIG.globalBadge = false;
        }
        loadScript(window.gutenbergIframe.contentWindow.document, bundlePath);
        window.gutenbergIframe = null;
    }

    loadScript(document, bundlePath);
});
PK     ø‹U]ì(eš  š    assets/gutenberg-environment.jsnu „[µü¤        (function () {
    'use strict';
    const SELECTORS = {
        RICH_TEXT: '.rich-text',
        MCE_CONTENT_BODY: '.mce-content-body',
        GUTENBERG_PAGE: 'block-editor-page',
        GUTENBERG_IFRAME: 'block-editor-iframe__body',
        TABLE_CELL: 'wp-block-table__cell-content'
    };

    // Own marker set right before init: keeps re-init idempotent even when the
    // SDK instance fails to start (e.g. service unreachable).
    const OWN_INSTANCE_ATTRIBUTE = 'data-wsc-init';
    // Attribute the SDK sets on a container once an instance is created.
    const SDK_INSTANCE_ATTRIBUTE = 'data-wpr-instance';

    const INIT_DELAY = 100;
    const INIT_TIMEOUT = 5000;
    const MAX_ATTEMPTS = 50;
    const OBSERVER_DEBOUNCE = 250;

    let attempts = 0;
    let observer = null;
    let observerTimer = null;
    const pendingRoots = new Set();

    const isGutenbergActive = () => {
        return document.body.classList.contains(SELECTORS.GUTENBERG_PAGE) ||
            document.body.classList.contains(SELECTORS.GUTENBERG_IFRAME);
    };

    function isContentEditable(element) {
        return element.isContentEditable;
    }

    function isInstanceCreated(element) {
        return element.hasAttribute(OWN_INSTANCE_ATTRIBUTE) ||
            element.hasAttribute(SDK_INSTANCE_ATTRIBUTE);
    }

    function isGutenbergTableCell(element) {
        return element.classList.contains(SELECTORS.TABLE_CELL);
    }

    const shouldIgnoreElement = (element) => {
        if (!isContentEditable(element)) {
            return true;
        }

        if (isInstanceCreated(element)) {
            return true;
        }

        if (isGutenbergTableCell(element)) {
            return true;
        }

        return false;
    };

    const createInstance = (element) => {
        element.setAttribute(OWN_INSTANCE_ATTRIBUTE, '1');

        try {
            WEBSPELLCHECKER.init({
                container: element,
            });

            // The SDK reports successful creation with data-wpr-instance. Do
            // not let our pending marker suppress retries forever otherwise.
            window.setTimeout(() => {
                if (!element.hasAttribute(SDK_INSTANCE_ATTRIBUTE)) {
                    element.removeAttribute(OWN_INSTANCE_ATTRIBUTE);
                }
            }, INIT_TIMEOUT);
        } catch (e) {
            // Allow a later retry if init threw synchronously.
            element.removeAttribute(OWN_INSTANCE_ATTRIBUTE);
        }
    };

    const initializeElements = (selector, root = document) => {
        let found = 0;
        const elements = [];

        if (root.nodeType === Node.ELEMENT_NODE && root.matches(selector)) {
            elements.push(root);
        }

        root.querySelectorAll(selector).forEach((element) => {
            elements.push(element);
        });

        elements.forEach((element) => {
            found++;

            if (shouldIgnoreElement(element)) {
                return;
            }

            createInstance(element);
        });

        return found;
    };

    const initializeAll = (root = document) => {
        return initializeElements(SELECTORS.RICH_TEXT, root) +
            initializeElements(SELECTORS.MCE_CONTENT_BODY, root);
    };

    // Blocks added after load (new paragraphs, async patterns) get instances too.
    const observeLateBlocks = () => {
        if (observer || typeof MutationObserver === 'undefined') {
            return;
        }

        observer = new MutationObserver((mutations) => {
            mutations.forEach((mutation) => {
                mutation.addedNodes.forEach((node) => {
                    if (node.nodeType === Node.ELEMENT_NODE) {
                        pendingRoots.add(node);
                    }
                });
            });

            if (pendingRoots.size === 0) {
                return;
            }

            if (observerTimer) {
                window.clearTimeout(observerTimer);
            }

            observerTimer = window.setTimeout(() => {
                pendingRoots.forEach((root) => {
                    if (root.isConnected) {
                        initializeAll(root);
                    }
                });
                pendingRoots.clear();
            }, OBSERVER_DEBOUNCE);
        });

        observer.observe(document.body, { childList: true, subtree: true });
    };

    const handleGutenbergReady = () => {
        attempts++;

        const found = initializeAll();

        if (found === 0 && attempts < MAX_ATTEMPTS) {
            window.setTimeout(handleGutenbergReady, INIT_DELAY);
            return;
        }

        observeLateBlocks();
    };

    const handleGutenbergReadyWithDelay = () => {
        setTimeout(handleGutenbergReady, INIT_DELAY);
    };

    window.webspellcheckerAlreadyLoaded = () => {
        if (!isGutenbergActive() || !window.WEBSPELLCHECKER_CONFIG?.globalBadge) {
            return;
        }

        handleGutenbergReadyWithDelay();
    };
})();
PK     ø‹U]UŒÓW  W    assets/proofreaderConfig.jsnu „[µü¤        (function () {
    'use strict';

    function asBoolean(value, fallback) {
        if (value === true || value === 'true') return true;
        if (value === false || value === 'false') return false;
        return fallback;
    }

    function asArray(value) {
        return Array.isArray(value) ? value : [];
    }

    const serviceConfig = window.WSCServiceConfig || {};
    const proofreaderConfig = window.WSCProofreaderConfig || {};

    // TinyMCE does not treat a bare DOM input event as an edit; without this the
    // classic editor may consider the document unchanged and skip saving a correction.
    function markTinyMceDirty(element) {
        try {
            const win = element.ownerDocument.defaultView;
            const tinymce = win.tinymce || (win.parent && win.parent.tinymce);
            if (!tinymce || !tinymce.editors) {
                return;
            }

            Array.prototype.forEach.call(tinymce.editors, function (editor) {
                const body = editor.getBody && editor.getBody();
                if (body && (body === element || body.contains(element))) {
                    editor.setDirty(true);
                }
            });
        } catch (e) {
            // Cross-frame access or missing TinyMCE — nothing to mark.
        }
    }

    const isBadgeEnabled = asBoolean(proofreaderConfig.enableBadgeButton, true);
    const badgeActions = isBadgeEnabled
        ? ['addWord', 'ignoreAll', 'settings', 'toggle', 'proofreadDialog']
        : ['addWord', 'ignoreAll', 'settings', 'proofreadDialog'];

    const disableAutoSearchIn = [
        '.wp-block-table__cell-content',
        '.ui-autocomplete-input',
        '#wp-link-url',
        '#url',
        '#billing_phone',
        '#shipping_phone',
        '#siteurl',
        '#new_admin_email',
        '#home',
        '#billing_postcode',
        '#billing_email',
        '#shipping_postcode',
        '#mailserver_url',
        '#mailserver_login',
        '#ping_sites',
        '#permalink_structure',
        '.inline-edit-password-input',
        '#_sale_price',
        '#_regular_price',
        '#_weight',
    ];

    window.WEBSPELLCHECKER_CONFIG = {
        autoSearch: true,
        appType: 'wp_plugin',
        serviceProtocol: serviceConfig.serviceProtocol || 'https',
        serviceHost: serviceConfig.serviceHost || 'svc.webspellchecker.net',
        servicePath: serviceConfig.servicePath || 'api',
        servicePort: serviceConfig.servicePort || '443',
        enableGrammar: asBoolean(proofreaderConfig.enableGrammar, false),
        aiWritingAssistant: asBoolean(proofreaderConfig.aiWritingAssistant, false),
        settingsSections: asArray(proofreaderConfig.settingsSections),
        serviceId: proofreaderConfig.key_for_proofreader,
        lang: proofreaderConfig.slang,
        enableBadgeButton: isBadgeEnabled,
        actionItems: badgeActions,
        disableAutoSearchIn: disableAutoSearchIn,
        disableOptionsStorage: asArray(proofreaderConfig.disableOptionsStorage),
        disableDictionariesPreferences: asBoolean(proofreaderConfig.disableDictionariesPreferences, false),
        autocomplete: asBoolean(proofreaderConfig.autocomplete, false),
        globalBadge: asBoolean(proofreaderConfig.globalBadge, true),
        compactBadge: asBoolean(proofreaderConfig.compactBadge, false),
        allSuggestionsMode: true,
        onLoad: function () {
            const instance = this;

            this.subscribe('replaceProblem', function () {
                try {
                    const element = instance.getContainerNode();
                    // Gutenberg's RichText syncs block state from the DOM on input.
                    element.dispatchEvent(new Event('input', { bubbles: true }));
                    markTinyMceDirty(element);
                } catch (e) {
                    // Container may have been detached by the host editor — safe to ignore.
                }
            });
        },
        onBeforeAutoSearchInstanceCreate: function (activeElement) {
            const id = activeElement && activeElement.element ? activeElement.element.id : '';
            return !(id && id.indexOf('url-input-control') === 0);
        },
    };

    if (Array.isArray(proofreaderConfig.generalOptions) && proofreaderConfig.generalOptions.length) {
        window.WEBSPELLCHECKER_CONFIG.generalOptions = proofreaderConfig.generalOptions;
    }
})();
PK     ø‹U]ºZ¼Î%$  %$    webspellchecker.phpnu „[µü¤        <?php
/**
 * Plugin Name:  WProofreader
 * Plugin URI:   https://webspellchecker.com/
 * Description:  WProofreader checks spelling, grammar, and style in real-time while editing in WordPress.
 * Version:      3.2.0
 * Author:       WebSpellChecker
 * Author URI:   https://webspellchecker.com/
 * Text Domain:  webspellchecker
 * Domain Path:  /languages
 * Requires PHP: 7.4
 * Requires at least: 6.3
 * Tested up to: 6.9
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( ! class_exists( 'WProofreader', false ) ) {
	final class WProofreader {

		const TRIAL_CUSTOMER_ID           = '1:cma3h3-HTiyU3-JL08g4-SRyuS1-a9c0F3-kH6Cu-OlMHS-thcSV2-HlGmv3-YzRCN2-qrKY42-uPc';
		const DEFAULT_LANGUAGE            = 'en_US';
		const DEFAULT_BADGE_TOGGLE_OPTION = 'on';
		const PLUGIN_VERSION              = '3.2.0';

		const SCRIPT_HANDLE_BUNDLE        = 'wsc_bundle';
		const SCRIPT_HANDLE_CONFIG        = 'wsc_config';
		const SCRIPT_HANDLE_ENV_CHECKER   = 'wsc_environment_checker';
		const SCRIPT_HANDLE_INSTANCE      = 'wsc_instance';
		const SCRIPT_HANDLE_GUTENBERG_ENV = 'wsc_gutenberg_environment';
		const SCRIPT_HANDLE_CLASSIC_ENV   = 'wsc_classic_environment';

		const L10N_OBJECT_CONFIG   = 'WSCProofreaderConfig';
		const L10N_OBJECT_INSTANCE = 'ProofreaderInstance';
		const L10N_OBJECT_SERVICE  = 'WSCServiceConfig';

		const SETTING_ENABLE_POSTS      = 'enable_on_posts';
		const SETTING_ENABLE_PAGES      = 'enable_on_pages';
		const SETTING_ENABLE_PRODUCTS   = 'enable_on_products';
		const SETTING_ENABLE_CATEGORIES = 'enable_on_categories';
		const SETTING_ENABLE_TAGS       = 'enable_on_tags';
		const SETTING_ENABLE_ADMIN      = 'enable_in_admin';
		const SETTING_ENABLE_FRONTEND   = 'enable_on_frontend';
		const SETTING_LANGUAGE          = 'slang';
		const SETTING_BADGE_BUTTON      = 'disable_badge_button';
		const SETTING_CUSTOMER_ID       = 'customer_id';

		const OPTION_ON  = 'on';
		const OPTION_OFF = 'off';

		const POST_TYPE_POST         = 'post';
		const POST_TYPE_PAGE         = 'page';
		const POST_TYPE_PRODUCT      = 'product';
		const POST_TYPE_WPSC_PRODUCT = 'wpsc-product';

		/** @var self|null */
		private static $instance = null;

		/** @var WSC_Settings */
		private $settings;

		/** @var array */
		private $options = array();

		/** @var string */
		private $plugin_url;

		/** @var string */
		private $plugin_dir;

		/** @var WProofreader_Context_Detector */
		private $context_detector;

		/** @var WProofreader_Context_Evaluator */
		private $context_evaluator;

		/** @var WProofreader_Config_Builder */
		private $config_builder;

		/** @var WProofreader_Assets */
		private $assets;

		/**
		 * Retrieve singleton instance.
		 *
		 * @return self
		 */
		public static function instance() {
			if ( null === self::$instance ) {
				self::$instance = new self();
			}

			return self::$instance;
		}

		private function __construct() {
			$this->plugin_url = plugin_dir_url( __FILE__ );
			$this->plugin_dir = plugin_dir_path( __FILE__ );

			$this->include_dependencies();
			$this->bootstrap_settings();
			$this->maybe_migrate_version();
			$this->bootstrap_services();
			$this->register_hooks();

			do_action( 'wsc_loaded' );
		}

		private function __clone() {}

		public function __wakeup() {
			throw new \RuntimeException( 'WProofreader cannot be unserialized.' );
		}

		/**
		 * Include required dependencies.
		 */
		private function include_dependencies() {
			require_once __DIR__ . '/vendor/class.settings-api.php';
			require_once __DIR__ . '/includes/class-wsc-settings.php';
			require_once __DIR__ . '/includes/class-wproofreader-context-detector.php';
			require_once __DIR__ . '/includes/class-wproofreader-context-evaluator.php';
			require_once __DIR__ . '/includes/class-wproofreader-config-builder.php';
			require_once __DIR__ . '/includes/class-wproofreader-assets.php';
			require_once __DIR__ . '/includes/class-wproofreader-ajax.php';
			require_once __DIR__ . '/includes/class-wproofreader-content-cleaner.php';
		}

		/**
		 * Initialize settings and merge with defaults.
		 */
		private function bootstrap_settings() {
			$this->settings = new WSC_Settings(
				__( 'WProofreader', 'webspellchecker' ),
				__( 'WProofreader', 'webspellchecker' ),
				'spell-checker-settings'
			);

			$default_options = array(
				self::SETTING_ENABLE_POSTS      => self::OPTION_ON,
				self::SETTING_ENABLE_PAGES      => self::OPTION_ON,
				self::SETTING_ENABLE_PRODUCTS   => self::OPTION_ON,
				self::SETTING_ENABLE_CATEGORIES => self::OPTION_ON,
				self::SETTING_ENABLE_TAGS       => self::OPTION_ON,
				self::SETTING_ENABLE_ADMIN      => self::OPTION_OFF,
				self::SETTING_ENABLE_FRONTEND   => self::OPTION_OFF,
				self::SETTING_LANGUAGE          => self::DEFAULT_LANGUAGE,
				self::SETTING_BADGE_BUTTON      => self::DEFAULT_BADGE_TOGGLE_OPTION,
			);

			$stored_options = get_option( WSC_Settings::OPTION_NAME, array() );
			$this->options  = wp_parse_args( $stored_options, $default_options );
			$this->options[ self::SETTING_CUSTOMER_ID ] = $this->get_customer_id();
		}

		/**
		 * Build collaborator objects.
		 */
		private function bootstrap_services() {
			$this->context_detector  = new WProofreader_Context_Detector();
			$this->context_evaluator = new WProofreader_Context_Evaluator( $this );
			$this->config_builder    = new WProofreader_Config_Builder( $this );
			$this->assets            = new WProofreader_Assets(
				$this,
				$this->context_detector,
				$this->context_evaluator,
				$this->config_builder,
				$this->plugin_url,
				$this->plugin_dir
			);
		}

		/**
		 * Register WordPress hooks.
		 */
		private function register_hooks() {
			add_action( 'init', array( $this, 'load_textdomain' ) );
			add_action( 'init', array( $this->assets, 'register_scripts' ) );
			add_action( 'enqueue_block_assets', array( $this->assets, 'enqueue_for_block_editor' ), 110 );
			add_action( 'admin_enqueue_scripts', array( $this->assets, 'enqueue_for_admin' ), 110 );
			add_action( 'elementor/editor/after_enqueue_scripts', array( $this->assets, 'enqueue_for_elementor_editor' ), 110 );
			add_action( 'wp_enqueue_scripts', array( $this->assets, 'enqueue_for_frontend' ), 10 );
			add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), array( $this, 'add_action_links' ) );
			add_action( 'wp_ajax_get_proofreader_info_callback', array( 'WProofreader_Ajax', 'get_proofreader_info' ) );
			add_action( 'init', array( 'WProofreader_Content_Cleaner', 'register' ) );
		}

		/**
		 * Load translations.
		 */
		public function load_textdomain() {
			load_plugin_textdomain( 'webspellchecker', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' );
		}

		/**
		 * Handle version migration and legacy cleanup.
		 *
		 * Runs in admin requests only so REST/cron/front-end loads never write options.
		 */
		private function maybe_migrate_version() {
			if ( ! is_admin() ) {
				return;
			}

			if ( get_option( 'wsc_proofreader_version' ) !== self::PLUGIN_VERSION ) {
				delete_option( 'wsc' );
				update_option( 'wsc_proofreader_version', self::PLUGIN_VERSION );
			}
		}

		/**
		 * Whether the current installation uses the Free trial service id.
		 *
		 * @return bool
		 */
		public function is_free_edition(): bool {
			return $this->get_customer_id() === self::TRIAL_CUSTOMER_ID;
		}

		/**
		 * Get customer ID or fall back to trial ID.
		 *
		 * @return string
		 */
		public function get_customer_id(): string {
			$customer_id = (string) $this->get_option( self::SETTING_CUSTOMER_ID, self::TRIAL_CUSTOMER_ID );

			return ( '' === trim( $customer_id ) ) ? self::TRIAL_CUSTOMER_ID : $customer_id;
		}

		/**
		 * Get selected language with fallback.
		 *
		 * @return string
		 */
		public function get_language(): string {
			$language = (string) $this->get_option( self::SETTING_LANGUAGE, self::DEFAULT_LANGUAGE );

			return ( '' === trim( $language ) ) ? self::DEFAULT_LANGUAGE : $language;
		}

		/**
		 * Get badge button option.
		 *
		 * @return string
		 */
		public function get_badge_button_option(): string {
			return (string) $this->get_option( self::SETTING_BADGE_BUTTON, self::DEFAULT_BADGE_TOGGLE_OPTION );
		}

		/**
		 * Get option value.
		 *
		 * @param string $name Option key.
		 * @param mixed  $default Default value.
		 * @return mixed
		 */
		public function get_option( $name, $default = '' ) {
			return isset( $this->options[ $name ] ) ? $this->options[ $name ] : $default;
		}

		/**
		 * Get all normalized options.
		 *
		 * @return array
		 */
		public function get_options(): array {
			return $this->options;
		}

		/**
		 * Add settings link in plugins list.
		 *
		 * @param array $links Existing action links.
		 * @return array
		 */
		public function add_action_links( $links ) {
			$url = esc_url( add_query_arg( 'page', 'spell-checker-settings', admin_url( 'options-general.php' ) ) );

			$settings_link = sprintf(
				'<a href="%s">%s</a>',
				$url,
				esc_html__( 'Settings', 'webspellchecker' )
			);

			return array_merge( $links, array( $settings_link ) );
		}
	}
}

if ( ! function_exists( 'wsc_proofreader' ) ) {
	/**
	 * Get singleton instance.
	 *
	 * @return WProofreader
	 */
	function wsc_proofreader() {
		return WProofreader::instance();
	}
}

wsc_proofreader();
PK     ø‹U]΅M«F  «F    license.txtnu „[µü¤                            GNU GENERAL PUBLIC LICENSE
                       Version 2, June 1991

 Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 Everyone is permitted to copy and distribute verbatim copies
 of this license document, but changing it is not allowed.

                            Preamble

  The licenses for most software are designed to take away your
freedom to share and change it.  By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users.  This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it.  (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.)  You can apply it to
your programs, too.

  When we speak of free software, we are referring to freedom, not
price.  Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.

  To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.

  For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have.  You must make sure that they, too, receive or can get the
source code.  And you must show them these terms so they know their
rights.

  We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.

  Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software.  If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.

  Finally, any free program is threatened constantly by software
patents.  We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary.  To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.

  The precise terms and conditions for copying, distribution and
modification follow.

                    GNU GENERAL PUBLIC LICENSE
   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION

  0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License.  The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language.  (Hereinafter, translation is included without limitation in
the term "modification".)  Each licensee is addressed as "you".

Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope.  The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.

  1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.

You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.

  2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:

    a) You must cause the modified files to carry prominent notices
    stating that you changed the files and the date of any change.

    b) You must cause any work that you distribute or publish, that in
    whole or in part contains or is derived from the Program or any
    part thereof, to be licensed as a whole at no charge to all third
    parties under the terms of this License.

    c) If the modified program normally reads commands interactively
    when run, you must cause it, when started running for such
    interactive use in the most ordinary way, to print or display an
    announcement including an appropriate copyright notice and a
    notice that there is no warranty (or else, saying that you provide
    a warranty) and that users may redistribute the program under
    these conditions, and telling the user how to view a copy of this
    License.  (Exception: if the Program itself is interactive but
    does not normally print such an announcement, your work based on
    the Program is not required to print an announcement.)

These requirements apply to the modified work as a whole.  If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works.  But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.

Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.

In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.

  3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:

    a) Accompany it with the complete corresponding machine-readable
    source code, which must be distributed under the terms of Sections
    1 and 2 above on a medium customarily used for software interchange; or,

    b) Accompany it with a written offer, valid for at least three
    years, to give any third party, for a charge no more than your
    cost of physically performing source distribution, a complete
    machine-readable copy of the corresponding source code, to be
    distributed under the terms of Sections 1 and 2 above on a medium
    customarily used for software interchange; or,

    c) Accompany it with the information you received as to the offer
    to distribute corresponding source code.  (This alternative is
    allowed only for noncommercial distribution and only if you
    received the program in object code or executable form with such
    an offer, in accord with Subsection b above.)

The source code for a work means the preferred form of the work for
making modifications to it.  For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable.  However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.

If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.

  4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License.  Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.

  5. You are not required to accept this License, since you have not
signed it.  However, nothing else grants you permission to modify or
distribute the Program or its derivative works.  These actions are
prohibited by law if you do not accept this License.  Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.

  6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions.  You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.

  7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License.  If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all.  For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.

If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.

It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices.  Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.

This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.

  8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded.  In such case, this License incorporates
the limitation as if written in the body of this License.

  9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time.  Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.

Each version is given a distinguishing version number.  If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation.  If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.

  10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission.  For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this.  Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.

                            NO WARRANTY

  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.

  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.

                     END OF TERMS AND CONDITIONS

            How to Apply These Terms to Your New Programs

  If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.

  To do so, attach the following notices to the program.  It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.

    <one line to give the program's name and a brief idea of what it does.>
    Copyright (C) <year>  <name of author>

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License along
    with this program; if not, write to the Free Software Foundation, Inc.,
    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

Also add information on how to contact you by electronic and paper mail.

If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:

    Gnomovision version 69, Copyright (C) year name of author
    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
    This is free software, and you are welcome to redistribute it
    under certain conditions; type `show c' for details.

The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License.  Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.

You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary.  Here is a sample; alter the names:

  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
  `Gnomovision' (which makes passes at compilers) written by James Hacker.

  <signature of Ty Coon>, 1 April 1989
  Ty Coon, President of Vice

This General Public License does not permit incorporating your program into
proprietary programs.  If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library.  If this is what you want to do, use the GNU Lesser General
Public License instead of this License.PK     ø‹U]ªd¶&)&  )&    CHANGELOG.mdnu „[µü¤        # WProofreader Plugin for WordPress Changelog

## 3.2.0 - 2026-06-12

* Fixed: post content corruption on save in the block editor. The content cleanup filter processed slashed post data, which added escaped quotes to attribute values (href, id, class) on every save, and it also ran on posts that merely contained "wsc-" in URLs rather than actual proofreading artifacts.
* Reworked content cleanup to splice artifact markup out of the original byte stream (WP_HTML_Tag_Processor) instead of re-serializing the whole document; content outside removed tags is never modified. Removed the lossy DOMDocument and regex fallbacks.
* Content cleanup now also covers autosaves and revisions, so restored autosaves no longer contain proofreading markup.
* Fixed the editor re-init guard to use the data-wpr-instance attribute the SDK actually sets, preventing duplicate instances; added a temporary init marker with timeout recovery so failed initializations can retry safely.
* Block editor integration now retries initialization while the editor is still rendering and picks up blocks added after load.
* Corrections in the classic editor now mark TinyMCE as dirty so they are reliably saved.
* The proofreading bundle is loaded once per document (previously it could be requested twice in iframed block editors).
* Settings page: clearer message when the session nonce has expired; settings fields are only registered on the settings screen and when saving.
* Version migration no longer writes options during REST/cron/front-end requests; uninstall now cleans options on all sites in multisite.

## 3.1.0 - 2026-04-24

* Refactored plugin internals into focused classes while preserving the public WProofreader class, option names, filters, and AJAX action.
* Fixed PHP 8 compatibility issue caused by deprecated create_function() usage in the bundled settings API.
* Enabled local user dictionary access for Free edition and disabled autocomplete for Free edition via supported bundle configuration.
* Enabled AI writing assistant for Premium edition by default.
* Added filterable WProofreader service configuration via wsc_service_config.
* Hardened the settings AJAX language endpoint and post-content cleanup.

## 3.0.0 - 2025-10-31

* Added a few global activation toggles (disabled by default):
* * Enable in admin area to run WProofreader across the WordPress admin.
* * Enable on front end to run WProofreader on the public site.
* Fixed issue where spelling autocorrect could modify URLs; now disabled by default in link-related fields.
* Updated service path to remove the legacy component ssrv.cgi, which is no longer used.
* Improved dedicated loading for the Block Editor (enqueue_block_assets), with safe exclusions for the Site Editor and Permalinks settings screen.
* Improved asset versioning based on file modification time to reduce caching issues.
* Improved unified script handles and localization objects for consistency.
* Hardened AJAX endpoint for the language list with nonce verification, capability checks, strict sanitization, and structured JSON response.
* Comprehensive refactoring and code cleanup: simplified initialization flow for admin, Block Editor, and front end; consolidated settings bootstrap and defaults.

## 2.8.0 - 2025-06-25

* Fixed issue with user preferences not being remembered. A recent issue caused user-selected options (e.g., selected language, ignore settings, enabled check types) to reset on every page reload. Now, these preferences are stored.
* Compact badge enabled by default. Users can now click directly on the badge to launch the proofreading dialog. In earlier versions, this required hovering over the badge and selecting the “proofread in dialog” option.
* Improved suggestion mode in dialog. The dialog now opens in an extended view that shows all suggestions at once. Previously, users had to go through suggestions one by one.

## 2.7.1 - 2025-06-09
* Renamed WeDevs_Settings_API to WSC_Settings_API and WeDevs_Settings_API_Test to WSC_Settings to avoid naming conflicts with other plugins using similar settings API classes.

## 2.7.0 – 2025-02-18 
* Fixed issue with RTL (right-to-left) languages visibility in the Language dropdown on the plugin Settings page.
## 2.6.9 - 2024-02-27
* Updated plugin’s graphics and texts.

## 2.6.8 – 2023-05-17
* Enabled the global badge view instead of the standard one. It will add just one badge at the very bottom of the page instead of adding a small badge on each editable element within Gutenber editor. This badge can be disabled from the plugin settings page.
* Fixed issue when WProofreader doesn't start automatically in classic editor inside default Gutenberg editor.

## 2.6.7 – 2022-09-14
* Fixed warning in class-wsc-settings.php on line 180.

## 2.6.6 – 2022-07-29
* Fixed issue with the dialog mode.
* Updated text.

## 2.6.5 – 2021-09-28
* Enabled WProofreader badge by default. If needed, it can be disabled from the plugin's settings tab.
* Enabled the General section on the settings dialog for managing spelling autocorrection. By default, the spelling autocorrection is enabled. If needed, the user can disable it. To do so, please navigate to the badge (orange bubble located at the right bottom corner of the editable block) > click on the gear icon > toggle “Correct spelling automatically”.

## 2.6.4 - 2020-11-26
* Fixed a minor issue with the plugin permissions and an error shown on the settings page.

## 2.6.3 - 2020-11-20
* WProofreader is disabled in the table element in the Gutenberg editor to prevent breaking a table and overall glitching.

## 2.6.2 - 2020-09-22
* Fixed the integration issue that appeared in v2.6 with the Classic editor when Gutenberg is disabled.

## 2.6.1 - 2020-09-21
* Enabled error logging mechanism. Now errors will be displayed on the plugin settings page, which will help to troubleshoot issues faster.

## 2.6 - 2020-09-14
* Improved the integration with the Gutenberg editor. Now WProofreader will be automatically started in all editable elements of the editor without the necessity to click on each element to enable the spelling/grammar checking.
* The orange badge is disabled by default for all types of elements including the Gutenberg editor. There is a setting allowing to reactivate the badge.
* The dialog mode option for proofreading is disabled by default for the Gutenberg editor.


## 2.5 - 2019-07-29

* Enabled spelling and grammar check in the Meta description fields of [Yoast SEO](https://wordpress.org/plugins/wordpress-seo/) plugin.
* Enabled support of Custom Post Types using the 'wproofreader_add_cpt' filter. To do so, you need to add a special 'wproofreader_add_cpt' filter in your function.php. 

Example:

```
function wproofreader_add_cpt_callback() {
    return  array(
            'my-custom-post-type'
            );
}
add_filter( 'wproofreader_add_cpt', 'wproofreader_add_cpt_callback' );
```


## 2.4 - 2019-04-15

* Added a new option “Disable WProofreader Badge” to the plugin settings which allows disabling an orange badge button. If disabled, the orange badge won’t appear in each editable box on the page. Users will see only underlined spelling and grammar errors that were detected by WProofreader. 
* Fixed the issue with “Uncaught TypeError” in the browser console which appeared after the update of the event system in the WProofreader core.

## 2.3 - 2019-02-25

* Fixed the issue: The Gutenberg editor doesn’t react on the text changes (e.g. replace word) that were made by WProofreader.
* Blocked saving of WProofreader span elements to database.
* Fixed the issue: After replacing a word in IE11 or MS Edge, the word remains underlined in the Gutenberg editor.
* Fixed the issue: After replacing a word in IE11 or MS Edge, the input event doesn’t work properly in the Gutenberg editor.

## 2.2 - 2019-02-12

* Resolved the [issue](https://wordpress.org/support/topic/description-says-spanish-is-supported-but-no-spanish/) with not working plugin under PHP version 7.1. and higher. Consequently eliminated PHP warnings “Invalid argument supplied for foreach()“. 

## 2.1 - 2019-01-29

* Fixed the issue with not working grammar checking option in the Pro version.

## 2.0 - 2018-12-21

The new version of the WProofreader (WebSpellChecker Proofreader) plugin introduces the following updates:

* Upgraded to be fully compatible with latest versions of WordPress: WordPress 4.x, WordPress 5.x and  Gutenberg editor.
* Enabled two comfortable modes for proofreading: instant, underlining mistakes while you type, and dialog, proofreading all the text at once in a separate pop-up window.
* Improved engine to check not only spelling but grammar as well. Checking spelling for 6 languages in a Free version, checking both spelling and grammar for 14 in Pro + spell checking in Finnish and Norwegian Bokmal.
* Introduced new clean and comfy UI: intuitive suggestion boxes, spinner indicating the progress of proofreading, Maximize/Restore icon in a dialog mode, save changes notifications.
* Checks spelling and grammar on Pages, Posts, Tags, Categories, WooCommerce Product Descriptions, and WP eCommerce Product Descriptions including product tags and categories.
* Removed banner ad in the Free version.
* Extended the list of supported browsers: Chrome (the latest), Firefox (the latest), Safari (the latest), Internet Explorer 11, MS Edge (the latest).

## 1.1

New version of the WebSpellChecker plugin introduces the following updates:

* Support of the Yoast SEO and ACF text fields
* Spell Checking in the Title fields
* Fixes for minor issues

## 1.0

Initial Release of the WebSpellChecker plugin. The multi-language spell checking functionality is available for Visual Editor and excerpt all fields.
PK     ø‹U]šn‚¹·E  ·E  
  readme.txtnu „[µü¤        === WProofreader spell & grammar check plugin for WordPress ===
Contributors: webspellchecker
Tags: spell check, grammar, Gutenberg, proofreading, corrector
Donate link: https://www.webspellchecker.com/
Requires at least: 6.3
Tested up to: 7.0
Requires PHP: 7.4
Stable tag: 3.2.0
License: GPL-2.0+
License URI: http://www.gnu.org/licenses/gpl-2.0.txt

WProofreader checks spelling, grammar, and style in real-time while editing in WordPress.

== Description ==

WProofreader plugin for WordPress scans and checks your website content for spelling, grammar, punctuation and style issues. No banner ads on your website!

Compatible with the latest WordPress versions.

**WProofreader Free** is ideal for a small personal blog or lightweight use:

Add instant spell checking to your WordPress site with support for 20+ languages. The free version comes with a daily word limit and applies to a single website, with a per-browser personal dictionary for storing custom words. Basic spelling and grammar check. No cloud sync. A straightforward, no-cost tool for basic proofreading needs.

**WProofreader Pro** for professional sites and teams:

A full-featured writing assistant for WordPress with unlimited websites, and no daily limits. Includes spell and grammar checking in dozens of languages, AI-powered text rephrasing and proofreading, text autocomplete, and a cloud-synced personal dictionary accessible from any device or browser. Subscription owners can also set up organization-wide custom dictionaries shared across all connected sites for keeping terminology consistent everywhere.

Check more differences between the WProofreader plugin free and Pro versions on the [website](https://wproofreader.com/integrations/wordpress).

[youtube https://youtu.be/mhrZN87jydA]

### User functionality

**Proofreading modes**

- The check-as-you-type mode instantaneously detects writing mistakes and provides suggestions on-hover.
- The proofread-in-dialog mode aggregates all the issues detected and suggestions provided.

**Customizable dictionaries**

The Pro version includes:

- **User-level custom dictionary** allows end users to create personal dictionaries with specific words and terms (complex words, acronyms, proper names, etc.) and use them while working with WordPress. Once a word is added to a personal dictionary, it will not be considered as a misspelling, and will appear in the suggestions list. All words you add are stored either in the browser local storage or on our cloud and are accessible from the browser.
- **Organization-level custom dictionary** allows creating dictionaries that extend the vocabulary of the standard dictionary with custom words specific to your organization culture, industry, domain, etc. All the words added to a dictionary by the admin will be available to all users within your organization. This type of dictionary is available only under the Pro version.
- **Specialized medical spelling dictionaries** for English, German, French and Spanish, and legal dictionaries for English. Available for both, Free and Pro versions of the plugin.

**Feature-rich**

- Multi-language support
- Spelling autocorrect functionality for instant correction of common typos
- Text autocomplete suggestions functionality for faster writing
- Style check functionality with pre-defined non-inclusive and profanity language check for professional writing
- Style guide builder for creating language rules
- User-level and organization-level dictionaries

**User-friendly and up-to-date**

- Straightforward plugin configuration ([check the video tutorial](https://youtu.be/mhrZN87jydA))
- Visible and intuitive text check with different colors used for spelling, grammar, and style guide issues
- Easily toggle on/off the plugin button
- Support the latest version of WordPress
- Seamlessly integrates with the WordPress editor (Gutenberg) and compatible with the classic editor. Supported browsers: Chrome, Firefox, Safari, Microsoft Edge.
- Content elements supported: *pages, posts, tag descriptions, category descriptions, WooCommerce, WP eCommerce product descriptions, any custom post types, meta description fields of Yoast SEO plugin, all fields in the Admin panel and user-facing editable fields (disabled by default).*

### Benefits of WProofreader plugin for WordPress

- It saves time proofreading in any of the two modes, check-as-you type and proofread-in-dialog.
- You can create and maintain quality posts in different languages without being worried about possible mistakes.
- Advanced functionality for spelling autocorrect, autocomplete suggestions and style guide for faster and more productive writing.

== Installation ==
#### From your WordPress dashboard

1. Visit 'Plugins > Add New'.
2. Search for 'WProofreader'.
3. Activate WProofreader from your Plugins page.

#### From WordPress.org

1. [Download WProofreader plugin](https://downloads.wordpress.org/plugin/webspellchecker.zip).
2. Upload the 'wproofreader' directory to your '/wp-content/plugins/' directory, using your favorite method (ftp, sftp, scp, etc...)
3. Activate WProofreader from your Plugins page.

#### Updating

Automatic updates should run smoothly, but it's always a good idea to back up your site, just in case.

== Frequently Asked Questions ==
= What is a license key and how to obtain one? =
A **license key** is a special data string that verifies authorized access to the Pro version of WProofreader plugin. The license key is provided by the WebSpellChecker when the Pro version is purchased.

= Does the WProofreader plugin check the entire website? =
The plugin doesn't check the whole website at a time. It is enabled only when the content on your website is in editing mode. Here is the list of content types which can be checked by the plugin: content of pages, content of posts, tag descriptions, category descriptions, WooCommerce and WP eCommerce product descriptions, any Custom Post Type, Meta description fields of Yoast SEO plugin, all fields in the Admin panel and user-facing website elements.

= What other languages are available for WProofreader plugin? =
The Free version and Pro version of WProofreader plugin support [20+ popular languages](https://docs.wproofreader.com/features/supported-languages).

* *Note that not all languages support grammar checks. Languages with grammar enabled are marked.*

= What is a user-level custom dictionary and how can I create one? =
WProofreader plugin has a special functionality called **a custom dictionary**. It allows creating personal dictionaries with specific words and terms (complex words, acronyms, proper names, etc.) and using them across your website. Words added to a personal dictionary will not be marked as misspellings.

To create a new **user-level custom dictionary**, click the badge in the bottom right corner of the text field and select Settings. Then go to Dictionaries. Name your new dictionary and click Create. All new words will be added to a brand-new dictionary. If you have several personal dictionaries, for example, with different term types, new words will be added to the one, which is currently in use. [Check docs](https://docs.wproofreader.com/features/custom-dictionary/user-dictionary-user-manual#creating-a-dictionary).

To create an **organization-level custom dictionary** to be used across all the websites under your subscription, please follow the instructions in this [guide](https://docs.wproofreader.com/features/custom-dictionary/organization-dictionary-cloud-admin-panel).

= Does WProofreader plugin check grammar as well? =

Not all languages in the list support grammar checks. Check the [updated list of supported languages](https://docs.wproofreader.com/features/supported-languages) and the ones that can be used for grammar-check.

= Is technical support available for WProofreader plugin? =
Priority support is provided by WebSpellChecker for Pro users. Community support is available to all users via the [plugin support forum](https://wordpress.org/support/plugin/webspellchecker/). For Pro inquiries, please [contact us](https://wproofreader.com/contact-us).

= What are the Terms of Service? =
Please see the complete [WebSpellChecker Terms of Service](https://webspellchecker.com/legal/terms-of-service/) for WProofreader plugin.

= Is my text secure? What is your Privacy Policy? =
We have an extensive Privacy Policy in place to protect your texts. Please see the details [here](https://webspellchecker.com/legal/privacy-policy/).

= What browsers are supported? =
The plugin supports the latest versions of Chrome, Firefox, Safari, and MS Edge.

= Can I suggest a feature? =
Sure, please let us know how we can make the plugin more useful for you. Create a new topic under the support section of this plugin.

== Screenshots ==
1. WProofreader checks spelling, grammar and style on Posts.
2. WProofreader spelling suggestion.
3. WProofreader grammar suggestion.
4. WProofreader style suggestion.
5. WProofreader settings dialog with check types and user dictionary.
6. WProofreader ‘Proofread in dialog mode’ with spelling suggestion.
7. WProofreader user custom dictionary view.
8. WProofreader spelling autocorrect suggestion.
9. WProofreader text autocomplete suggestion.
10. WProofreader WordPress plugin settings page.

== Changelog ==

= 3.2.0 - 2026-06-12 =
* Fixed: post content corruption on save in the block editor (content cleanup processed slashed post data and ran on posts merely containing "wsc-" in URLs).
* Reworked content cleanup to remove proofreading markup without re-serializing the rest of the document; autosaves and revisions are now cleaned too.
* Improved block editor integration: correct re-init guard, retry while the editor renders, support for blocks added after load, single bundle load per document.
* Corrections in the classic editor now reliably mark the document as changed.
* Minor settings page and housekeeping improvements; multisite-aware uninstall.

= 3.1.0 - 2026-04-24 =
* Refactored plugin internals into focused classes while preserving the public WProofreader class and option names.
* Fixed PHP 8 compatibility issue in the bundled settings API.
* Enabled local user dictionary access for Free edition and disabled autocomplete for Free edition via supported bundle configuration.
* Enabled AI writing assistant for Premium edition by default.
* Added filterable WProofreader service configuration and reduced hard-coded endpoint duplication.
* Hardened the settings AJAX handler and content cleanup.

= 3.0.0 - 2025-10-31 =
* Added a few global activation toggles (disabled by default):
* * Enable in admin area to run WProofreader across the WordPress admin.
* * Enable on front end to run WProofreader on the public site.
* Fixed issue where spelling autocorrect could modify URLs; now disabled by default in link-related fields.
* Updated service path to remove the legacy component ssrv.cgi, which is no longer used.
* Improved dedicated loading for the Block Editor (enqueue_block_assets), with safe exclusions for the Site Editor and Permalinks settings screen.
* Improved asset versioning based on file modification time to reduce caching issues.
* Improved unified script handles and localization objects for consistency.
* Hardened AJAX endpoint for the language list with nonce verification, capability checks, strict sanitization, and structured JSON response.
* Comprehensive refactoring and code cleanup: simplified initialization flow for admin, Block Editor, and front end; consolidated settings bootstrap and defaults.

= 2.8.0 – 2025-06-25 =
* Fixed issue with user preferences not being remembered. A recent issue caused user-selected options (e.g., selected language, ignore settings, enabled check types) to reset on every page reload. Now, these preferences are stored.
* Compact badge enabled by default. Users can now click directly on the badge to launch the proofreading dialog. In earlier versions, this required hovering over the badge and selecting the “proofread in dialog” option.
* Improved suggestion mode in dialog. The dialog now opens in an extended view that shows all suggestions at once. Previously, users had to go through suggestions one by one.

= 2.7.1 – 2025-06-09 =
* Renamed WeDevs_Settings_API to WSC_Settings_API and WeDevs_Settings_API_Test to WSC_Settings to avoid naming conflicts with other plugins using similar settings API classes.

= 2.7.0 – 2025-02-18 =
* Fixed issue with RTL (right-to-left) languages visibility in the Language dropdown on the plugin Settings page.

= 2.6.10 – 2024-09-23 =
* Minor update resolving the URL input problem in the Classic Editor.

= 2.6.9 – 2024-02-27 =
* Updated plugin’s graphics and texts

= 2.6.8 – 2023-05-17 =
* Enabled the global badge view instead of the standard one. It will add just one badge at the very bottom of the page instead of adding a small badge on each editable element within Gutenber editor. This badge can be disabled from the plugin settings page.
* Fixed issue when WProofreader doesn't start automatically in classic editor inside default Gutenberg editor.

= 2.6.7 – 2022-09-14 =
* Fixed warning in class-wsc-settings.php on line 180.

= 2.6.6 – 2022-07-29 =
* Fixed issue with the dialog mode.
* Updated text.

= 2.6.5 – 2021-09-28 =
* Enabled WProofreader badge by default. If needed, it can be disabled from the plugin's settings tab.
* Enabled the General section on the settings dialog for managing spelling autocorrection. By default, the spelling autocorrection is enabled. If needed, the user can disable it. To do so, please navigate to the badge (orange bubble located at the right bottom corner of the editable block) > click on the gear icon > toggle “Correct spelling automatically”.

= 2.6.4 - 2020-11-26 =
* Fixed a minor issue with the plugin permissions and an error shown on the settings page.

= 2.6.3 - 2020-11-20 =
* WProofreader is disabled in the table element in the Gutenberg editor to prevent breaking a table and overall glitching.

= 2.6.2 - 2020-09-22 =
* Fixed the integration issue that appeared in v2.6 with the Classic editor when Gutenberg is disabled.


= 2.6.1 - 2020-09-21 =
* Enabled error logging mechanism. Now errors will be displayed on the plugin settings page, which will help to troubleshoot issues faster.

= 2.6 - 2020-09-14 =
* Improved the integration with the Gutenberg editor. Now WProofreader will be automatically started in all editable elements of the editor without the necessity to click on each element to enable the spelling/grammar checking.
* The orange badge is disabled by default for all types of elements including the Gutenberg editor. There is a setting allowing to reactivate the badge.
* The dialog mode option for proofreading is disabled by default for the Gutenberg editor.

= 2.5 - 2019-07-29 =
* Enabled spelling and grammar check in the Meta description fields of Yoast SEO plugin.
* Enabled support of Custom Post Types using the “wproofreader_add_cpt“ filter.

= 2.4 - 2019-04-15 =
* Added a new option “Disable WProofreader Badge” to the plugin settings which allows disabling an orange badge button. If disabled, the orange badge won’t appear in each editable box on the page. Users will see only underlined spelling and grammar errors that were detected by WProofreader.
* Fixed the issue with “Uncaught TypeError” in the browser console which appeared after the update of the event system in the WProofreader core.

= 2.3 - 2019-02-25 =
* Fixed the issue: The Gutenberg editor doesn’t react on the text changes (e.g. replace word) that were made by WProofreader.
* Blocked saving of WProofreader span elements to database.
* Fixed the issue: After replacing a word in IE11 or MS Edge, the word remains underlined in the Gutenberg editor.
* Fixed the issue: After replacing a word in IE11 or MS Edge, the input event doesn’t work properly in the Gutenberg editor.

= 2.2 - 2019-02-12 =
* Resolved the issue with not working plugin under PHP version 7.1. and higher. Consequently eliminated PHP warnings “Invalid argument supplied for foreach()“.

= 2.1 - 2019-01-29 =
* Fixed the issue with not working grammar checking option in the Pro version.

= 2.0 - 2018-12-21 =
The new version of the WProofreader (WebSpellChecker Proofreader) plugin introduces the following updates:

* Upgraded to be fully compatible with latest versions of WordPress: WordPress 4.x, WordPress 5.x and  Gutenberg editor.
* Enabled two comfortable modes for proofreading: instant, underlining mistakes while you type, and dialog, proofreading all the text at once in a separate pop-up window.
* Improved engine to check not only spelling but grammar as well. Checking spelling for 6 languages in a Free version, checking both spelling and grammar for 14 in Pro + spell checking in Finnish and Norwegian Bokmal.
* Introduced new clean and comfy UI: intuitive suggestion boxes, spinner indicating the progress of proofreading, Maximize/Restore icon in a dialog mode, save changes notifications.
* Checks spelling and grammar on Pages, Posts, Tags, Categories, WooCommerce Product Descriptions, and WP eCommerce Product Descriptions including product tags and categories.
* Removed banner ad in the Free version.
* Extended the list of supported browsers: Chrome (the latest), Firefox (the latest), Safari (the latest), Internet Explorer 11, MS Edge (the latest).



= 1.1 =
New version of the WebSpellChecker plugin introduces the following updates:

* Support of the Yoast SEO and ACF text fields
* Spell Checking in the Titles field
* Fixes for minor issues


= 1.0 =
Initial Release of the WebSpellChecker plugin. The multi-language spell checking functionality is available for Visual Editor and excerpt all fields.

== Upgrade Notice ==

= 2.0 =
2.0 is a major update. Make a full site backup.
PK     ø‹U]·œ?©   ©   &  includes/class-wproofreader-assets.phpnu „[µü¤        <?php
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Registers and enqueues scripts for admin, editor, frontend, and settings demo.
 */
class WProofreader_Assets {

	/** @var WProofreader */
	private $plugin;

	/** @var WProofreader_Context_Detector */
	private $context_detector;

	/** @var WProofreader_Context_Evaluator */
	private $context_evaluator;

	/** @var WProofreader_Config_Builder */
	private $config_builder;

	/** @var string */
	private $plugin_url;

	/** @var string */
	private $plugin_dir;

	/**
	 * @param WProofreader                   $plugin Plugin facade.
	 * @param WProofreader_Context_Detector  $context_detector Context detector.
	 * @param WProofreader_Context_Evaluator $context_evaluator Context evaluator.
	 * @param WProofreader_Config_Builder    $config_builder Config builder.
	 * @param string                         $plugin_url Plugin URL.
	 * @param string                         $plugin_dir Plugin directory.
	 */
	public function __construct(
		WProofreader $plugin,
		WProofreader_Context_Detector $context_detector,
		WProofreader_Context_Evaluator $context_evaluator,
		WProofreader_Config_Builder $config_builder,
		string $plugin_url,
		string $plugin_dir
	) {
		$this->plugin            = $plugin;
		$this->context_detector  = $context_detector;
		$this->context_evaluator = $context_evaluator;
		$this->config_builder    = $config_builder;
		$this->plugin_url        = $plugin_url;
		$this->plugin_dir        = $plugin_dir;
	}

	/**
	 * Register all plugin scripts.
	 */
	public function register_scripts() {
		wp_register_script(
			WProofreader::SCRIPT_HANDLE_BUNDLE,
			$this->config_builder->bundle_url(),
			array(),
			WProofreader::PLUGIN_VERSION,
			true
		);

		$this->register_local_script( WProofreader::SCRIPT_HANDLE_CONFIG, 'assets/proofreaderConfig.js', array(), false );
		$this->register_local_script( WProofreader::SCRIPT_HANDLE_ENV_CHECKER, 'assets/environmentChecker.js', array(), false );
		$this->register_local_script( WProofreader::SCRIPT_HANDLE_INSTANCE, 'assets/instance.js', array( 'jquery' ), true );
		$this->register_local_script( WProofreader::SCRIPT_HANDLE_GUTENBERG_ENV, 'assets/gutenberg-environment.js', array(), true );
		$this->register_local_script( WProofreader::SCRIPT_HANDLE_CLASSIC_ENV, 'assets/classic-environment.js', array( WProofreader::SCRIPT_HANDLE_BUNDLE ), true );
	}

	/**
	 * Enqueue assets for the block editor.
	 */
	public function enqueue_for_block_editor() {
		if ( ! is_admin() ) {
			return;
		}

		$screen = $this->context_detector->get_screen();
		if ( ! $this->context_detector->is_block_editor_screen( $screen ) || $this->context_detector->is_site_editor_screen( $screen ) ) {
			return;
		}

		$context = $this->context_detector->detect();
		if ( ! $this->context_evaluator->should_enable_filtered( $context ) ) {
			return;
		}

		$this->enqueue_runtime_config();
		wp_enqueue_script( WProofreader::SCRIPT_HANDLE_ENV_CHECKER );
		wp_enqueue_script( WProofreader::SCRIPT_HANDLE_GUTENBERG_ENV );
	}

	/**
	 * Enqueue assets for classic admin pages.
	 *
	 * @param string $hook_suffix Hook suffix.
	 */
	public function enqueue_for_admin( $hook_suffix ) {
		if ( ! is_admin() ) {
			return;
		}

		$screen = $this->context_detector->get_screen();
		if ( ! $this->is_eligible_admin_screen( $screen ) ) {
			return;
		}

		if ( WProofreader_Context_Detector::SCREEN_SETTINGS === ( $screen->base ?? '' ) ) {
			$this->enqueue_settings_demo();
			return;
		}

		$context = $this->context_detector->detect();
		if ( ! $this->context_evaluator->should_enable_filtered( $context ) ) {
			return;
		}

		$badge_config = $this->classic_editor_badge_config( $screen, $context );

		$this->enqueue_runtime_config( $badge_config );
		wp_enqueue_script( WProofreader::SCRIPT_HANDLE_BUNDLE );

		if ( ! empty( $badge_config['globalBadge'] ) ) {
			wp_enqueue_script( WProofreader::SCRIPT_HANDLE_CLASSIC_ENV );
		}
	}

	/**
	 * Enqueue assets on the frontend.
	 */
	public function enqueue_for_frontend() {
		if ( WProofreader::OPTION_ON !== $this->plugin->get_option( WProofreader::SETTING_ENABLE_FRONTEND, WProofreader::OPTION_OFF ) ) {
			return;
		}

		$this->enqueue_runtime_config( array( 'globalBadge' => false ) );
		wp_enqueue_script( WProofreader::SCRIPT_HANDLE_BUNDLE );
	}

	/**
	 * Enqueue assets inside the Elementor editor shell.
	 */
	public function enqueue_for_elementor_editor() {
		// Elementor resets the global WP_Scripts registry before this hook.
		$this->register_scripts();

		$context = $this->elementor_context();
		if ( ! $this->context_evaluator->should_enable_filtered( $context ) ) {
			return;
		}

		$this->enqueue_runtime_config( array( 'globalBadge' => false ) );
		wp_enqueue_script( WProofreader::SCRIPT_HANDLE_BUNDLE );
	}

	/**
	 * @param string $handle Script handle.
	 * @param string $relative_path Relative asset path.
	 * @param array  $deps Dependencies.
	 * @param bool   $in_footer Whether to load in footer.
	 */
	private function register_local_script( string $handle, string $relative_path, array $deps, bool $in_footer ) {
		wp_register_script(
			$handle,
			$this->plugin_url . $relative_path,
			$deps,
			$this->asset_version_for( $relative_path ),
			$in_footer
		);
	}

	/**
	 * @param string $relative_path Relative asset path.
	 * @return string
	 */
	private function asset_version_for( string $relative_path ): string {
		if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) {
			return (string) time();
		}

		$file_path = $this->plugin_dir . ltrim( $relative_path, '/\\' );

		if ( ! is_readable( $file_path ) ) {
			return WProofreader::PLUGIN_VERSION;
		}

		$mtime = filemtime( $file_path );

		return $mtime ? (string) $mtime : WProofreader::PLUGIN_VERSION;
	}

	/**
	 * @param WP_Screen|null $screen Screen object.
	 * @return bool
	 */
	private function is_eligible_admin_screen( $screen ): bool {
		if ( ! $screen ) {
			return false;
		}

		if ( $this->context_detector->is_block_editor_screen( $screen ) || $this->context_detector->is_site_editor_screen( $screen ) ) {
			return false;
		}

		if ( 'plugins' === ( $screen->base ?? '' ) ) {
			return false;
		}

		return ! $this->context_detector->is_permalinks_settings_screen( $screen );
	}

	/**
	 * @param WP_Screen|null $screen Screen object.
	 * @param array          $context Context.
	 * @return array
	 */
	private function classic_editor_badge_config( $screen, array $context ): array {
		$is_single_editor_screen = ( isset( $screen->base ) && 'post' === $screen->base );
		$post_type               = $context['post_type'] ?? '';
		$is_post_or_page         = in_array( $post_type, array( WProofreader::POST_TYPE_POST, WProofreader::POST_TYPE_PAGE ), true );

		return array( 'globalBadge' => (bool) ( $is_single_editor_screen && $is_post_or_page ) );
	}


	/**
	 * Build an editor context for Elementor's custom admin app.
	 *
	 * @return array
	 */
	private function elementor_context(): array {
		$post_id   = isset( $_GET['post'] ) ? absint( wp_unslash( $_GET['post'] ) ) : 0;
		$post_type = $post_id ? get_post_type( $post_id ) : '';

		return array(
			'place'       => WProofreader_Context_Detector::PLACE_POST_TYPE,
			'screen_id'   => 'elementor',
			'screen_base' => 'elementor',
			'post_type'   => $post_type ? (string) $post_type : WProofreader::POST_TYPE_POST,
			'taxonomy'    => '',
		);
	}

	/**
	 * @param array $overrides Config overrides.
	 */
	private function enqueue_runtime_config( array $overrides = array() ) {
		wp_enqueue_script( WProofreader::SCRIPT_HANDLE_CONFIG );
		wp_localize_script( WProofreader::SCRIPT_HANDLE_CONFIG, WProofreader::L10N_OBJECT_CONFIG, $this->config_builder->build( $overrides ) );
		wp_localize_script( WProofreader::SCRIPT_HANDLE_CONFIG, WProofreader::L10N_OBJECT_SERVICE, $this->config_builder->service_config() );
	}

	/**
	 * Enqueue the settings page demo instance.
	 */
	private function enqueue_settings_demo() {
		wp_enqueue_script( WProofreader::SCRIPT_HANDLE_INSTANCE );
		wp_localize_script(
			WProofreader::SCRIPT_HANDLE_INSTANCE,
			WProofreader::L10N_OBJECT_INSTANCE,
			$this->config_builder->build_instance( wp_create_nonce( 'webspellchecker-proofreader' ) )
		);
		wp_localize_script( WProofreader::SCRIPT_HANDLE_INSTANCE, WProofreader::L10N_OBJECT_SERVICE, $this->config_builder->service_config() );
		wp_enqueue_script( WProofreader::SCRIPT_HANDLE_BUNDLE );
	}
}
PK     ø‹U]¡=ä—  —  /  includes/class-wproofreader-content-cleaner.phpnu „[µü¤        <?php
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Removes WProofreader editing artifacts before post content is saved.
 *
 * Artifact spans are spliced out of the original byte stream with
 * WP_HTML_Tag_Processor offsets, so content outside the removed tags is
 * never re-serialized or normalized.
 */
class WProofreader_Content_Cleaner {

	/** @var array */
	private static $artifact_classes = array(
		'wsc-spelling-problem',
		'wsc-grammar-problem',
		'rangySelectionBoundary',
	);

	/**
	 * Register content cleanup filter.
	 */
	public static function register() {
		add_filter( 'wp_insert_post_data', array( __CLASS__, 'clean_post_data' ), 100, 2 );
	}

	/**
	 * @param array $data Post data (slashed, as provided by wp_insert_post_data).
	 * @param array $postarr Raw post array.
	 * @return array
	 */
	public static function clean_post_data( $data, $postarr ) {
		if ( empty( $data['post_content'] ) ) {
			return $data;
		}

		// Data on wp_insert_post_data is slashed; clean unslashed HTML and re-slash.
		$original_content = wp_unslash( (string) $data['post_content'] );
		$clean_content    = self::clean_content( $original_content );

		// Cleanup only removes bytes; anything else means a bug, keep the original.
		if ( strlen( $clean_content ) > strlen( $original_content ) ) {
			do_action( 'wproofreader_cleanup_failed', $original_content, $clean_content, $postarr );
			$clean_content = $original_content;
		}

		$data['post_content'] = wp_slash( $clean_content );

		return $data;
	}

	/**
	 * Remove artifact span tags (open and close), keeping their inner content.
	 *
	 * @param string $content Post content.
	 * @return string
	 */
	public static function clean_content( string $content ): string {
		$has_artifacts = false;
		foreach ( self::$artifact_classes as $class ) {
			if ( false !== strpos( $content, $class ) ) {
				$has_artifacts = true;
				break;
			}
		}

		if ( ! $has_artifacts || ! class_exists( 'WP_HTML_Tag_Processor' ) ) {
			return $content;
		}

		$scanner = new WProofreader_HTML_Scanner( $content );
		$ranges  = $scanner->artifact_span_ranges( self::$artifact_classes );

		if ( empty( $ranges ) ) {
			return $content;
		}

		// Splice from the end so earlier offsets stay valid.
		usort(
			$ranges,
			static function ( $a, $b ) {
				return $b['start'] - $a['start'];
			}
		);

		foreach ( $ranges as $range ) {
			$content = substr_replace( $content, '', $range['start'], $range['length'] );
		}

		return $content;
	}
}

if ( class_exists( 'WP_HTML_Tag_Processor' ) ) {
	/**
	 * Read-only scanner exposing byte ranges of artifact span tags.
	 */
	class WProofreader_HTML_Scanner extends WP_HTML_Tag_Processor {

		const BOOKMARK = 'wproofreader_token';

		/**
		 * Collect byte ranges of every artifact span opener and its matching closer.
		 *
		 * @param array $artifact_classes Class names marking artifact spans.
		 * @return array[] List of array{start: int, length: int}.
		 */
		public function artifact_span_ranges( array $artifact_classes ): array {
			$ranges = array();
			$stack  = array();

			while ( $this->next_tag( array( 'tag_closers' => 'visit' ) ) ) {
				if ( 'SPAN' !== $this->get_tag() ) {
					continue;
				}

				if ( $this->is_tag_closer() ) {
					$opener = array_pop( $stack );
					if ( null !== $opener ) {
						$ranges[] = $opener;
						$ranges[] = $this->current_token_range();
					}
					continue;
				}

				$is_artifact = false;
				foreach ( $artifact_classes as $class ) {
					if ( true === $this->has_class( $class ) ) {
						$is_artifact = true;
						break;
					}
				}

				$stack[] = $is_artifact ? $this->current_token_range() : null;
			}

			// Unclosed artifact openers: drop the opener tag alone.
			foreach ( $stack as $entry ) {
				if ( null !== $entry ) {
					$ranges[] = $entry;
				}
			}

			return $ranges;
		}

		/**
		 * Byte range of the token the processor is currently stopped on.
		 *
		 * @return array{start: int, length: int}
		 */
		private function current_token_range(): array {
			$this->set_bookmark( self::BOOKMARK );
			$span = $this->bookmarks[ self::BOOKMARK ];
			$this->release_bookmark( self::BOOKMARK );

			return array(
				'start'  => $span->start,
				'length' => $span->length,
			);
		}
	}
}
PK     ø‹U]'$@¾#  ¾#    includes/class-wsc-settings.phpnu „[µü¤        <?php
if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * Admin settings controller for WProofreader.
 */
if ( ! class_exists( 'WSC_Settings' ) ) {

	class WSC_Settings {
		/** Option bag name for all plugin settings (single section). */
		const OPTION_NAME = 'wsc_proofreader';

		/** @var WSC_Settings_API */
		private $settings_api;

		/** @var string */
		private $page_title;

		/** @var string */
		private $menu_title;

		/** @var string */
		private $menu_slug;

		/**
		 * @param string $page_title Settings page title (H1).
		 * @param string $menu_title Menu label under Settings.
		 * @param string $menu_slug Menu slug.
		 */
		public function __construct( $page_title, $menu_title, $menu_slug ) {
			$this->settings_api = new WSC_Settings_API();
			$this->menu_title   = (string) $menu_title;
			$this->menu_slug    = (string) $menu_slug;
			$this->page_title   = (string) $page_title;

			add_action( 'admin_init', array( $this, 'on_admin_init' ) );
			add_action( 'admin_menu', array( $this, 'on_admin_menu' ) );
		}

		/**
		 * Register sections/fields and augment fields if e-commerce is active.
		 */
		public function on_admin_init() {
			// Field/section setup is only needed when rendering the settings page
			// or when options.php is persisting this option group.
			if ( ! $this->is_settings_request() ) {
				return;
			}

			if ( $this->is_ecommerce_active() ) {
				add_filter( 'wsc_admin_fields', array( $this, 'add_products_toggle_field' ), 1 );
			}

			$this->settings_api->set_sections( $this->get_settings_sections() );
			$this->settings_api->set_fields( $this->get_settings_fields() );

			$this->settings_api->admin_init();
		}

		/**
		 * Add the settings page under "Settings".
		 */
		public function on_admin_menu() {
			add_options_page(
				$this->page_title,
				$this->menu_title,
				'manage_options',
				$this->menu_slug,
				array( $this, 'render_settings_page' )
			);
		}

		/**
		 * Whether the current request renders or saves this settings page.
		 *
		 * @return bool
		 */
		protected function is_settings_request(): bool {
			global $pagenow;

			if ( 'options.php' === $pagenow ) {
				return true;
			}

			// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only routing check.
			$page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : '';

			return $this->menu_slug === $page;
		}

		/**
		 * Detect if WooCommerce or WP eCommerce is active.
		 *
		 * @return bool
		 */
		protected function is_ecommerce_active(): bool {
			return class_exists( 'WooCommerce' ) || class_exists( 'WP_eCommerce' );
		}

		/**
		 * Filter callback: insert "Check Products" field after "Check Pages".
		 *
		 * @param array $settings_fields
		 *
		 * @return array
		 */
		public function add_products_toggle_field( array $settings_fields ): array {
			if ( empty( $settings_fields[ self::OPTION_NAME ] ) || ! is_array( $settings_fields[ self::OPTION_NAME ] ) ) {
				return $settings_fields;
			}

			$products_field = array(
				'name'    => 'enable_on_products',
				'label'   => __( 'Check Products', 'webspellchecker' ),
				'type'    => 'checkbox',
				'default' => 'on',
			);

			$fields     = $settings_fields[ self::OPTION_NAME ];
			$new_fields = array();
			$inserted   = false;

			foreach ( $fields as $field ) {
				$new_fields[] = $field;

				if ( ! $inserted && isset( $field['name'] ) && 'enable_on_pages' === $field['name'] ) {
					$new_fields[] = $products_field;
					$inserted     = true;
				}
			}

			// If we did not find "enable_on_pages", append before the last two tail fields (admin/frontend).
			if ( ! $inserted ) {
				$tail_count = 2;
				if ( count( $new_fields ) > $tail_count ) {
					$tail         = array_splice( $new_fields, - $tail_count );
					$new_fields[] = $products_field;
					$new_fields   = array_merge( $new_fields, $tail );
				} else {
					$new_fields[] = $products_field;
				}
			}

			$settings_fields[ self::OPTION_NAME ] = $new_fields;

			return $settings_fields;
		}

		/**
		 * Define the single settings section.
		 *
		 * @return array[]
		 */
		protected function get_settings_sections(): array {
			return array(
				array(
					'id'    => self::OPTION_NAME,
					'title' => '',
				),
			);
		}

		/**
		 * Define all settings fields.
		 *
		 * @return array
		 */
		protected function get_settings_fields(): array {
			$language_options = $this->get_language_options();
			if ( empty( $language_options ) ) {
				$language_options = array(
					'en_US' => 'English',
					'en_GB' => 'British English',
					'en_CA' => 'Canadian English',
					'fr_FR' => 'French',
					'fr_CA' => 'Canadian French',
					'de_DE' => 'German',
					'it_IT' => 'Italian',
					'pt_PT' => 'Portuguese',
					'pt_BR' => 'Brazilian Portuguese',
					'da_DK' => 'Danish',
				);
			}

			$fields = array(
				self::OPTION_NAME => array(
					array(
						'name'              => 'customer_id',
						'label'             => __( 'License Key', 'webspellchecker' ),
						'desc'              => __( 'Upgrade to WProofreader Pro to access the grammar/style checking, text autocomplete capabilities and lift the usage limits for your websites. Leave it empty if you prefer to use the Free version. <br><a href="https://webspellchecker.com/free-trial/" target="_blank">Try Pro for 14-days free ></a>.', 'webspellchecker' ),
						'type'              => 'text',
						'default'           => '',
						'sanitize_callback' => 'sanitize_text_field',
					),
					array(
						'name'              => 'slang',
						'label'             => __( 'Default Language', 'webspellchecker' ),
						'type'              => 'select',
						'options'           => $language_options,
						'default'           => 'en_US',
						'sanitize_callback' => 'sanitize_text_field',
					),
					array(
						// Historical toggle: 'on' means "badge shown".
						'name'    => 'disable_badge_button',
						'label'   => __( 'Enable Badge', 'webspellchecker' ),
						'type'    => 'checkbox',
						'default' => 'on',
					),
					array(
						'name'    => 'enable_on_posts',
						'label'   => __( 'Check Posts', 'webspellchecker' ),
						'type'    => 'checkbox',
						'default' => 'on',
					),
					array(
						'name'    => 'enable_on_pages',
						'label'   => __( 'Check Pages', 'webspellchecker' ),
						'type'    => 'checkbox',
						'default' => 'on',
					),
					// "Check Products" may be injected here if e-commerce is active.
					array(
						'name'    => 'enable_on_categories',
						'label'   => __( 'Check Categories', 'webspellchecker' ),
						'type'    => 'checkbox',
						'default' => 'on',
					),
					array(
						'name'    => 'enable_on_tags',
						'label'   => __( 'Check Tags', 'webspellchecker' ),
						'type'    => 'checkbox',
						'default' => 'on',
					),
					// Keep these at the end for predictable UI.
					array(
						'name'    => 'enable_in_admin',
						'label'   => __( 'Enable in admin area', 'webspellchecker' ),
						'desc'    => __( 'Enable WProofreader in editable fields across the WordPress dashboard.', 'webspellchecker' ),
						'type'    => 'checkbox',
						'default' => 'off',
					),
					array(
						'name'    => 'enable_on_frontend',
						'label'   => __( 'Enable on public site', 'webspellchecker' ),
						'desc'    => __( 'Enable WProofreader in editable fields on the public site (e.g., contact forms, forums).', 'webspellchecker' ),
						'type'    => 'checkbox',
						'default' => 'off',
					),
				),
			);

			return apply_filters( 'wsc_admin_fields', $fields );
		}

		/**
		 * Render the settings page.
		 */
		public function render_settings_page() {
			if ( ! current_user_can( 'manage_options' ) ) {
				wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'webspellchecker' ) );
			}

			echo '<div class="wrap">';
			echo '<h1>' . esc_html( $this->page_title ) . '</h1>';
			$this->settings_api->show_navigation();
			$this->settings_api->show_forms();
			echo '</div>';
		}

		/**
		 * Utility: return available languages from cached option.
		 *
		 * @return array<string,string> key => label
		 */
		protected function get_language_options(): array {
			$info = get_transient( 'wsc_proofreader_info_cache' );
			if ( false === $info ) {
				$info = get_option( 'wsc_proofreader_info' );
			}

			if ( ! is_array( $info ) || empty( $info['langList'] ) || ! is_array( $info['langList'] ) ) {
				return array();
			}

			$lang_list = $info['langList'];

			$ltr = array();
			$rtl = array();

			if ( isset( $lang_list['ltr'] ) && is_array( $lang_list['ltr'] ) ) {
				$ltr = $lang_list['ltr'];
			}
			if ( isset( $lang_list['rtl'] ) && is_array( $lang_list['rtl'] ) ) {
				$rtl = $lang_list['rtl'];
			}

			// Ensure strings and remove empties.
			$sanitize_map = static function ( $arr ) {
				$out = array();
				foreach ( $arr as $code => $label ) {
					$code  = (string) $code;
					$label = (string) $label;
					if ( '' !== $code && '' !== $label ) {
						$out[ $code ] = $label;
					}
				}

				return $out;
			};

			return array_merge( $sanitize_map( $ltr ), $sanitize_map( $rtl ) );
		}
	}
}
PK     ø‹U]BÙÝæ±  ±  1  includes/class-wproofreader-context-evaluator.phpnu „[µü¤        <?php
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Converts a detected context and settings into an enable/disable decision.
 */
class WProofreader_Context_Evaluator {

	/** @var WProofreader */
	private $plugin;

	/** @var array */
	private $post_type_options;

	/** @var array */
	private $taxonomy_options;

	/**
	 * @param WProofreader $plugin Plugin facade.
	 */
	public function __construct( WProofreader $plugin ) {
		$this->plugin = $plugin;

		$this->post_type_options = array(
			WProofreader::POST_TYPE_POST         => WProofreader::SETTING_ENABLE_POSTS,
			WProofreader::POST_TYPE_PAGE         => WProofreader::SETTING_ENABLE_PAGES,
			WProofreader::POST_TYPE_PRODUCT      => WProofreader::SETTING_ENABLE_PRODUCTS,
			WProofreader::POST_TYPE_WPSC_PRODUCT => WProofreader::SETTING_ENABLE_PRODUCTS,
		);

		$this->taxonomy_options = array(
			WProofreader_Context_Detector::TAXONOMY_CATEGORY => WProofreader::SETTING_ENABLE_CATEGORIES,
			WProofreader_Context_Detector::TAXONOMY_TAG      => WProofreader::SETTING_ENABLE_TAGS,
		);
	}

	/**
	 * Decide whether WProofreader should be enabled in current context.
	 *
	 * @param array $context Detected context.
	 * @return bool
	 */
	public function should_enable( array $context ): bool {
		$place = isset( $context['place'] ) ? (string) $context['place'] : WProofreader_Context_Detector::PLACE_OTHER;

		if ( WProofreader_Context_Detector::PLACE_SETTINGS === $place ) {
			return true;
		}

		$admin_enabled  = $this->is_option_enabled( WProofreader::SETTING_ENABLE_ADMIN, WProofreader::OPTION_OFF );
		$admin_behavior = apply_filters( 'wproofreader_admin_behavior', 'with_exclusions' );

		if ( $admin_enabled && 'override' === $admin_behavior ) {
			return true;
		}

		if ( WProofreader_Context_Detector::PLACE_TAXONOMY === $place ) {
			return $this->should_enable_for_taxonomy( $context, $admin_enabled );
		}

		if ( WProofreader_Context_Detector::PLACE_POST_TYPE === $place ) {
			return $this->should_enable_for_post_type( $context, $admin_enabled );
		}

		return $admin_enabled;
	}

	/**
	 * Apply filters around the raw context decision.
	 *
	 * @param array $context Detected context.
	 * @return bool
	 */
	public function should_enable_filtered( array $context ): bool {
		return (bool) apply_filters(
			'wproofreader_should_enable_here',
			$this->should_enable( $context ),
			$context,
			$this->plugin->get_options()
		);
	}

	/**
	 * @param array $context Context.
	 * @param bool  $admin_enabled Whether broad admin mode is enabled.
	 * @return bool
	 */
	private function should_enable_for_taxonomy( array $context, bool $admin_enabled ): bool {
		$taxonomy   = isset( $context['taxonomy'] ) ? (string) $context['taxonomy'] : '';
		$option_key = isset( $this->taxonomy_options[ $taxonomy ] ) ? $this->taxonomy_options[ $taxonomy ] : '';

		if ( '' === $option_key ) {
			return $admin_enabled;
		}

		return $this->is_option_enabled( $option_key, WProofreader::OPTION_ON );
	}

	/**
	 * @param array $context Context.
	 * @param bool  $admin_enabled Whether broad admin mode is enabled.
	 * @return bool
	 */
	private function should_enable_for_post_type( array $context, bool $admin_enabled ): bool {
		$post_type  = isset( $context['post_type'] ) ? (string) $context['post_type'] : '';
		$option_key = isset( $this->post_type_options[ $post_type ] ) ? $this->post_type_options[ $post_type ] : '';

		if ( '' !== $option_key ) {
			return $this->is_option_enabled( $option_key, WProofreader::OPTION_ON );
		}

		if ( $this->is_additional_post_type( $post_type ) ) {
			return true;
		}

		return $admin_enabled;
	}

	/**
	 * @param string $post_type Post type.
	 * @return bool
	 */
	private function is_additional_post_type( string $post_type ): bool {
		$additional = apply_filters( 'wproofreader_add_cpt', array() );
		if ( ! is_array( $additional ) ) {
			return false;
		}

		foreach ( $additional as $cpt ) {
			if ( 0 === strcasecmp( (string) $cpt, $post_type ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * @param string $key Option key.
	 * @param string $default Default value.
	 * @return bool
	 */
	private function is_option_enabled( string $key, string $default ): bool {
		return WProofreader::OPTION_ON === $this->plugin->get_option( $key, $default );
	}
}
PK     ø‹U]|KCÑ  Ñ  .  includes/class-wproofreader-config-builder.phpnu „[µü¤        <?php
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Builds data passed from WordPress to the WProofreader JavaScript bundle.
 */
class WProofreader_Config_Builder {

	/** @var WProofreader */
	private $plugin;

	/**
	 * @param WProofreader $plugin Plugin facade.
	 */
	public function __construct( WProofreader $plugin ) {
		$this->plugin = $plugin;
	}

	/**
	 * Build localization payload for vendor scripts.
	 *
	 * @param array $overrides Config overrides.
	 * @return array
	 */
	public function build( array $overrides = array() ): array {
		$is_free          = $this->plugin->is_free_edition();
		$is_badge_enabled = ( $this->plugin->get_badge_button_option() === WProofreader::DEFAULT_BADGE_TOGGLE_OPTION );
		$base_config = array(
			'key_for_proofreader'            => $this->plugin->get_customer_id(),
			'slang'                          => $this->plugin->get_language(),
			'settingsSections'               => $is_free
				? array( 'dictionaries', 'languages', 'general', 'options' )
				: array( 'options', 'languages', 'dictionaries', 'about', 'general' ),
			'enableGrammar'                  => ! $is_free,
			'aiWritingAssistant'             => ! $is_free,
			'enableBadgeButton'              => $is_badge_enabled,
			'globalBadge'                    => true,
			'compactBadge'                   => true,
			'autocomplete'                   => false,
			'disableOptionsStorage'          => $is_free ? array( 'autocomplete' ) : array(),
			'disableDictionariesPreferences' => $is_free,
		);

		if ( $is_free ) {
			$base_config['generalOptions'] = array( 'spellingSuggestions', 'grammarSuggestions', 'styleGuideSuggestions', 'autocorrect' );
		}

		$config = wp_parse_args( $overrides, $base_config );

		if ( ! is_array( $config['disableOptionsStorage'] ) ) {
			$config['disableOptionsStorage'] = array();
		}

		return $this->normalize_booleans( $config );
	}

	/**
	 * Build config for the settings-page Web API instance.
	 *
	 * @param string $ajax_nonce AJAX nonce.
	 * @return array
	 */
	public function build_instance( string $ajax_nonce ): array {
		return $this->normalize_booleans(
			array(
				'key_for_proofreader' => $this->plugin->get_customer_id(),
				'slang'               => $this->plugin->get_language(),
				'ajax_nonce'          => $ajax_nonce,
				'enableGrammar'       => ! $this->plugin->is_free_edition(),
			)
		);
	}

	/**
	 * Shared WSC service configuration. Filterable via `wsc_service_config`.
	 *
	 * @return array
	 */
	public function service_config(): array {
		$defaults = array(
			'serviceProtocol' => 'https',
			'serviceHost'     => 'svc.webspellchecker.net',
			'servicePath'     => 'api',
			'servicePort'     => '443',
			'bundleUrl'       => 'https://svc.webspellchecker.net/spellcheck31/wscbundle/wscbundle.js',
		);

		$filtered = apply_filters( 'wsc_service_config', $defaults );

		return wp_parse_args( is_array( $filtered ) ? $filtered : array(), $defaults );
	}

	/**
	 * The remote bundle URL for script registration.
	 *
	 * @return string
	 */
	public function bundle_url(): string {
		$service_config = $this->service_config();

		return esc_url_raw( (string) $service_config['bundleUrl'] );
	}

	/**
	 * Keep legacy JS compatibility by passing booleans as "true"/"false" strings.
	 *
	 * @param array $config Raw config.
	 * @return array
	 */
	private function normalize_booleans( array $config ): array {
		foreach ( $config as $key => $value ) {
			if ( is_bool( $value ) ) {
				$config[ $key ] = $value ? 'true' : 'false';
			}
		}

		return $config;
	}
}
PK     ø‹U]ØïŸÕ~  ~  0  includes/class-wproofreader-context-detector.phpnu „[µü¤        <?php
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Detects WordPress admin/editor context without deciding plugin eligibility.
 */
class WProofreader_Context_Detector {

	const SCREEN_SETTINGS              = 'settings_page_spell-checker-settings';
	const SCREEN_SITE_EDITOR           = 'site-editor';
	const SCREEN_PERMALINKS            = 'options-permalink';
	const SCREEN_EDIT_CATEGORY         = 'edit-category';
	const SCREEN_EDIT_PRODUCT_CATEGORY = 'edit-product_cat';
	const SCREEN_EDIT_WPSC_PRODUCT_CAT = 'edit-wpsc_product_category';
	const SCREEN_EDIT_PRODUCT_TAG      = 'edit-product_tag';
	const SCREEN_EDIT_POST_TAG         = 'edit-post_tag';

	const PLACE_SETTINGS  = 'settings';
	const PLACE_TAXONOMY  = 'taxonomy';
	const PLACE_POST_TYPE = 'post_type';
	const PLACE_OTHER     = 'other';

	const TAXONOMY_CATEGORY = 'category';
	const TAXONOMY_TAG      = 'tag';

	/**
	 * Get current admin screen if available.
	 *
	 * @return WP_Screen|null
	 */
	public function get_screen() {
		return function_exists( 'get_current_screen' ) ? get_current_screen() : null;
	}

	/**
	 * Check if current screen is the block editor.
	 *
	 * @param WP_Screen|null $screen Screen object.
	 * @return bool
	 */
	public function is_block_editor_screen( $screen ): bool {
		return ( $screen && method_exists( $screen, 'is_block_editor' ) && $screen->is_block_editor() );
	}

	/**
	 * Check if current screen is the site editor.
	 *
	 * @param WP_Screen|null $screen Screen object.
	 * @return bool
	 */
	public function is_site_editor_screen( $screen ): bool {
		if ( $this->screen_matches(
			$screen,
			array( self::SCREEN_SITE_EDITOR ),
			array( 'gutenberg-edit-site' )
		) ) {
			return true;
		}

		return $this->request_path_contains( 'site-editor.php' );
	}

	/**
	 * Check if current screen is the permalinks settings page.
	 *
	 * @param WP_Screen|null $screen Screen object.
	 * @return bool
	 */
	public function is_permalinks_settings_screen( $screen ): bool {
		if ( $this->screen_matches( $screen, array( self::SCREEN_PERMALINKS ), array() ) ) {
			return true;
		}

		return $this->request_path_contains( 'options-permalink.php' );
	}

	/**
	 * Fallback when get_current_screen() is not yet available.
	 *
	 * @param string $needle Filename to look for in PHP_SELF.
	 * @return bool
	 */
	private function request_path_contains( string $needle ): bool {
		if ( empty( $_SERVER['PHP_SELF'] ) ) {
			return false;
		}

		return false !== strpos( (string) $_SERVER['PHP_SELF'], $needle );
	}

	/**
	 * Detect current admin context.
	 *
	 * @return array
	 */
	public function detect(): array {
		$screen = $this->get_screen();

		$context = array(
			'place'       => self::PLACE_OTHER,
			'screen_id'   => $screen->id ?? '',
			'screen_base' => $screen->base ?? '',
			'post_type'   => $screen->post_type ?? '',
			'taxonomy'    => '',
		);

		if ( self::SCREEN_SETTINGS === $context['screen_base'] ) {
			$context['place'] = self::PLACE_SETTINGS;
			return $context;
		}

		$taxonomy = $this->taxonomy_for_screen( $context['screen_id'] );
		if ( '' !== $taxonomy ) {
			$context['place']    = self::PLACE_TAXONOMY;
			$context['taxonomy'] = $taxonomy;
			return $context;
		}

		if ( ! empty( $context['post_type'] ) ) {
			$context['place'] = self::PLACE_POST_TYPE;
			return $context;
		}

		return $context;
	}

	/**
	 * Match by screen id/base and optional base substrings.
	 *
	 * @param WP_Screen|null $screen Screen object.
	 * @param array          $ids Exact ids/bases.
	 * @param array          $base_substrings Base substrings.
	 * @return bool
	 */
	private function screen_matches( $screen, array $ids, array $base_substrings ): bool {
		if ( ! $screen ) {
			return false;
		}

		$screen_id   = isset( $screen->id ) ? (string) $screen->id : '';
		$screen_base = isset( $screen->base ) ? (string) $screen->base : '';

		if ( in_array( $screen_id, $ids, true ) || in_array( $screen_base, $ids, true ) ) {
			return true;
		}

		foreach ( $base_substrings as $substring ) {
			if ( '' !== $screen_base && false !== strpos( $screen_base, (string) $substring ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Map edit taxonomy screens to option groups.
	 *
	 * @param string $screen_id Screen id.
	 * @return string
	 */
	private function taxonomy_for_screen( string $screen_id ): string {
		$map = array(
			self::SCREEN_EDIT_CATEGORY         => self::TAXONOMY_CATEGORY,
			self::SCREEN_EDIT_PRODUCT_CATEGORY => self::TAXONOMY_CATEGORY,
			self::SCREEN_EDIT_WPSC_PRODUCT_CAT => self::TAXONOMY_CATEGORY,
			self::SCREEN_EDIT_PRODUCT_TAG      => self::TAXONOMY_TAG,
			self::SCREEN_EDIT_POST_TAG         => self::TAXONOMY_TAG,
		);

		return isset( $map[ $screen_id ] ) ? $map[ $screen_id ] : '';
	}
}
PK     ø‹U]ê�n    $  includes/class-wproofreader-ajax.phpnu „[µü¤        <?php
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * AJAX endpoints for the settings page.
 */
class WProofreader_Ajax {

	const INFO_OPTION    = 'wsc_proofreader_info';
	const INFO_TRANSIENT = 'wsc_proofreader_info_cache';
	const INFO_TTL       = DAY_IN_SECONDS;

	/**
	 * AJAX handler to fetch and render language list.
	 */
	public static function get_proofreader_info() {
		check_ajax_referer( 'webspellchecker-proofreader', 'security' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Unauthorized', 'webspellchecker' ) ), 403 );
		}

		$proofreader_info = self::parse_payload( $_POST['getInfoResult'] ?? '' );
		if ( ! is_array( $proofreader_info ) ) {
			wp_send_json_error( array( 'message' => __( 'Invalid payload', 'webspellchecker' ) ), 400 );
		}

		$sanitized = self::sanitize_info( $proofreader_info );

		update_option( self::INFO_OPTION, $sanitized, false );
		set_transient( self::INFO_TRANSIENT, $sanitized, self::INFO_TTL );

		wp_send_json_success(
			array(
				'html' => self::render_language_select( $sanitized ),
			)
		);
	}

	/** Upper bound for the getInfo JSON payload (a language list is a few KB). */
	const MAX_PAYLOAD_BYTES = 65536;

	/**
	 * The SDK getInfo result arrives either as a JSON string or, when jQuery
	 * serializes the result object into form fields, as a nested array.
	 *
	 * @param mixed $payload Raw POST payload.
	 * @return array|null
	 */
	private static function parse_payload( $payload ) {
		if ( is_string( $payload ) ) {
			$payload = wp_unslash( $payload );
			if ( '' === $payload || strlen( $payload ) > self::MAX_PAYLOAD_BYTES ) {
				return null;
			}

			$decoded = json_decode( $payload, true );

			return ( JSON_ERROR_NONE === json_last_error() && is_array( $decoded ) ) ? $decoded : null;
		}

		if ( is_array( $payload ) ) {
			$payload = wp_unslash( $payload );

			// jQuery may submit getInfoResult as nested form fields. Measure the
			// normalized representation so this path cannot bypass the size cap.
			$encoded = wp_json_encode( $payload );
			if ( false === $encoded || strlen( $encoded ) > self::MAX_PAYLOAD_BYTES ) {
				return null;
			}

			return $payload;
		}

		return null;
	}

	/**
	 * @param array $proofreader_info Raw info.
	 * @return array
	 */
	private static function sanitize_info( array $proofreader_info ): array {
		$sanitized = array(
			'langList' => array(
				'ltr' => array(),
				'rtl' => array(),
			),
		);

		if ( empty( $proofreader_info['langList'] ) || ! is_array( $proofreader_info['langList'] ) ) {
			return $sanitized;
		}

		foreach ( array( 'ltr', 'rtl' ) as $text_direction ) {
			if ( empty( $proofreader_info['langList'][ $text_direction ] ) || ! is_array( $proofreader_info['langList'][ $text_direction ] ) ) {
				continue;
			}

			foreach ( $proofreader_info['langList'][ $text_direction ] as $code => $label ) {
				$code  = sanitize_text_field( (string) $code );
				$label = sanitize_text_field( (string) $label );
				if ( '' !== $code && '' !== $label ) {
					$sanitized['langList'][ $text_direction ][ $code ] = $label;
				}
			}
		}

		return $sanitized;
	}

	/**
	 * @param array $info Sanitized proofreader info.
	 * @return string
	 */
	private static function render_language_select( array $info ): string {
		$current_language = WProofreader::instance()->get_language();
		$all_languages    = array_merge( $info['langList']['ltr'], $info['langList']['rtl'] );

		ob_start();
		?>
		<select class="regular" name="wsc_proofreader[slang]" id="wsc_proofreader[slang]">
			<?php foreach ( $all_languages as $code => $label ) : ?>
				<option value="<?php echo esc_attr( $code ); ?>" <?php selected( $current_language, $code ); ?>>
					<?php echo esc_html( $label ); ?>
				</option>
			<?php endforeach; ?>
		</select>
		<?php

		return (string) ob_get_clean();
	}
}
PK       ø‹U]RäÀ·5"  5"  	                README.mdnu „[µü¤        PK       ø‹U]&„P§ÜZ  ÜZ              n"  vendor/class.settings-api.phpnu „[µü¤        PK       ø‹U]1Gn%  %              —}  tests/test-ajax-payload.phpnu „[µü¤        PK       ø‹U]¯äb±X  X              „  tests/test-content-cleaner.phpnu „[µü¤        PK       ø‹U]{۝$&  &              ­œ  uninstall.phpnu „[µü¤        PK       ø‹U]ÐâšV  V                 assets/instance.jsnu „[µü¤        PK       ø‹U]ÝÐgK  K              ¨°  assets/classic-environment.jsnu „[µü¤        PK       ø‹U]N
‡c  c              @¼  assets/environmentChecker.jsnu „[µü¤        PK       ø‹U]ì(eš  š              ïÂ  assets/gutenberg-environment.jsnu „[µü¤        PK       ø‹U]UŒÓW  W              ØÖ  assets/proofreaderConfig.jsnu „[µü¤        PK       ø‹U]ºZ¼Î%$  %$              zè  webspellchecker.phpnu „[µü¤        PK       ø‹U]΅M«F  «F              â license.txtnu „[µü¤        PK       ø‹U]ªd¶&)&  )&              ÈS CHANGELOG.mdnu „[µü¤        PK       ø‹U]šn‚¹·E  ·E  
            -z readme.txtnu „[µü¤        PK       ø‹U]·œ?©   ©   &            À includes/class-wproofreader-assets.phpnu „[µü¤        PK       ø‹U]¡=ä—  —  /            á includes/class-wproofreader-content-cleaner.phpnu „[µü¤        PK       ø‹U]'$@¾#  ¾#              ò includes/class-wsc-settings.phpnu „[µü¤        PK       ø‹U]BÙÝæ±  ±  1              includes/class-wproofreader-context-evaluator.phpnu „[µü¤        PK       ø‹U]|KCÑ  Ñ  .            2' includes/class-wproofreader-config-builder.phpnu „[µü¤        PK       ø‹U]ØïŸÕ~  ~  0            a5 includes/class-wproofreader-context-detector.phpnu „[µü¤        PK       ø‹U]ê�n    $            ?H includes/class-wproofreader-ajax.phpnu „[µü¤        PK      Œ  £W   