<?php
namespace DevOwl\RealCookieBanner\view\blocker;

use DevOwl\FastHtmlTag\finder\match\AbstractMatch;
use DevOwl\FastHtmlTag\finder\match\ScriptInlineMatch;
use DevOwl\FastHtmlTag\finder\match\StyleInlineMatch;
use DevOwl\FastHtmlTag\finder\match\TagAttributeMatch;
use DevOwl\HeadlessContentBlocker\AbstractPlugin;
use DevOwl\HeadlessContentBlocker\AbstractBlockable;
use DevOwl\HeadlessContentBlocker\BlockedResult;
use DevOwl\HeadlessContentBlocker\Constants;
use DevOwl\HeadlessContentBlocker\finder\match\StyleInlineAttributeMatch;
use DevOwl\HeadlessContentBlocker\matcher\AbstractMatcher;
use DevOwl\HeadlessContentBlocker\matcher\ScriptInlineMatcher;
use DevOwl\HeadlessContentBlocker\matcher\SelectorSyntaxMatcher;
use DevOwl\HeadlessContentBlocker\matcher\StyleInlineAttributeMatcher;
use DevOwl\HeadlessContentBlocker\matcher\StyleInlineMatcher;
use DevOwl\HeadlessContentBlocker\matcher\TagAttributeMatcher;
use DevOwl\HeadlessContentBlocker\plugins\imagePreview\ImagePreview;
use DevOwl\HeadlessContentBlocker\plugins\StandardPlugin;
use DevOwl\HeadlessContentBlocker\plugins\tcf\TcfForwardGdprStringInUrl;
use DevOwl\RealCookieBanner\base\UtilsProvider;
use DevOwl\RealCookieBanner\lite\view\blocker\WordPressImagePreviewCache;
use DevOwl\RealCookieBanner\settings\TCF;
use Sabberworm\CSS\CSSList\Document;

// @codeCoverageIgnoreStart
defined('ABSPATH') or die('No script kiddies please!'); // Avoid direct file request
// @codeCoverageIgnoreEnd

/**
 * Real Cookie Banner plugin for `HeadlessContentBlocker`.
 */
class Plugin extends AbstractPlugin {
    use UtilsProvider;

    const TABLE_NAME_BLOCKER_THUMBNAILS = 'blocker_thumbnails';

    private $wpContentDir;

    // Documented in AbstractPlugin
    public function init() {
        $this->wpContentDir = basename(constant('WP_CONTENT_DIR'));
        $cb = $this->getHeadlessContentBlocker();

        $cb->setInlineStyleDummyUrlPath(plugins_url('public/images/', RCB_FILE));

        $cb->addPlugin(StandardPlugin::class);
        $cb->addPlugin(PluginAutoplay::class);

        // This class only exists in PRO Version
        if ($this->isPro()) {
            $imagePreviewCache = WordPressImagePreviewCache::create();
            if ($imagePreviewCache !== false) {
                /**
                 * Plugin.
                 *
                 * @var ImagePreview
                 */
                $imagePreviewPlugin = $cb->addPlugin(ImagePreview::class);
                $imagePreviewPlugin->setCache($imagePreviewCache);

                // [Plugin Comp] Divi video slider
                $imagePreviewPlugin->addRevertAttribute('div[class*="et_pb_video_slider_item_"]');
            }
        }

        if ($this->isPro() && TCF::getInstance()->isActive()) {
            /**
             * Plugin.
             *
             * @var TcfForwardGdprStringInUrl
             */
            $tcfForwardGdprStringInUrl = $cb->addPlugin(TcfForwardGdprStringInUrl::class);
            $vendorConfigurations = TCF::getInstance()->getVendorConfigurations();
            foreach ($vendorConfigurations as $vendorConfiguration) {
                $vendor = $vendorConfiguration->getVendor();
                if (isset($vendor['deviceStorageDisclosure']) && isset($vendor['deviceStorageDisclosure']['domains'])) {
                    $tcfForwardGdprStringInUrl->addVendorDisclosureDomains(
                        $vendorConfiguration->getVendorId(),
                        $vendor['deviceStorageDisclosure']['domains']
                    );
                }
            }
        }
    }

    /**
     * See `AbstractPlugin`.
     *
     * @param BlockedResult $result
     * @param AbstractMatcher $matcher
     * @param AbstractMatch $match
     */
    public function blockedMatch($result, $matcher, $match) {
        if ($matcher instanceof TagAttributeMatcher) {
            /**
             * Var.
             *
             * @var TagAttributeMatch
             */
            $match = $match;

            /**
             * A tag got blocked, e. g. `iframe`. We can now modify the attributes again to add an additional attribute to
             * the blocked content.
             *
             * @hook RCB/Blocker/HTMLAttributes
             * @param {array} $attributes
             * @param {BlockedResult} $isBlocked Since 3.0.0 this is an instance of [BlockedResult](../php/classes/DevOwl-HeadlessContentBlocker-BlockedResult.html)
             * @param {string} $newLinkAttribute
             * @param {string} $linkAttribute
             * @param {string} $link
             * @return {array}
             */
            $attributes = apply_filters(
                'RCB/Blocker/HTMLAttributes',
                $match->getAttributes(),
                $result,
                $result->getData('newLinkAttribute'),
                $match->getLinkAttribute(),
                $match->getLink()
            );
            $match->setAttributes($attributes);
        } elseif ($match instanceof ScriptInlineMatcher) {
            /**
             * Var.
             *
             * @var ScriptInlineMatch
             */
            $match = $match;

            /**
             * An inline script got blocked, e. g. `iframe`. We can now modify the attributes again to add an additional attribute to
             * the blocked script. Do not forget to hook into the frontend and transform the modified attributes!
             *
             * @hook RCB/Blocker/InlineScript/HTMLAttributes
             * @param {array} $attributes
             * @param {array} $isBlocked Since 3.0.0 this is an instance of [BlockedResult](../php/classes/DevOwl-HeadlessContentBlocker-BlockedResult.html)
             * @param {string} $script
             * @return {array}
             */
            $attributes = apply_filters(
                'RCB/Blocker/InlineScript/HTMLAttributes',
                $match->getAttributes(),
                $result,
                $match->getAttribute(Constants::HTML_ATTRIBUTE_INLINE)
            );
            $match->setAttributes($attributes);
        }
    }

    /**
     * See `AbstractPlugin`.
     *
     * @param BlockedResult $result
     * @param AbstractMatcher $matcher
     * @param AbstractMatch $match
     */
    public function checkResult($result, $matcher, $match) {
        if ($matcher instanceof TagAttributeMatcher) {
            /**
             * Var.
             *
             * @var TagAttributeMatch
             */
            $match = $match;

            /**
             * Check if a given tag, link attribute and link is blocked.
             *
             * @hook RCB/Blocker/IsBlocked
             * @param {BlockedResult} $isBlocked Since 3.0.0 this is an instance of [BlockedResult](../php/classes/DevOwl-HeadlessContentBlocker-BlockedResult.html)
             * @param {string} $linkAttribute
             * @param {string} $link
             * @return {BlockedResult}
             */
            $result = apply_filters('RCB/Blocker/IsBlocked', $result, $match->getLinkAttribute(), $match->getLink());
        } elseif ($matcher instanceof ScriptInlineMatcher) {
            /**
             * Var.
             *
             * @var ScriptInlineMatch
             */
            $match = $match;

            /**
             * Check if a given inline script is blocked.
             *
             * @hook RCB/Blocker/InlineScript/IsBlocked
             * @param {BlockedResult} $isBlocked Since 3.0.0 this is an instance of [BlockedResult](../php/classes/DevOwl-HeadlessContentBlocker-BlockedResult.html)
             * @param {string} $script
             * @return {BlockedResult}
             */
            $result = apply_filters('RCB/Blocker/InlineScript/IsBlocked', $result, $match->getScript());
        } elseif ($matcher instanceof StyleInlineMatcher) {
            /**
             * Var.
             *
             * @var StyleInlineMatch
             */
            $match = $match;

            /**
             * Check if a given inline style is blocked.
             *
             * @hook RCB/Blocker/InlineStyle/IsBlocked
             * @param {BlockedResult} $isBlocked Since 3.0.0 this is an instance of [BlockedResult](../php/classes/DevOwl-HeadlessContentBlocker-BlockedResult.html)
             * @param {string} $style
             * @return {BlockedResult}
             */
            $result = apply_filters('RCB/Blocker/InlineStyle/IsBlocked', $result, $match->getStyle());
        } elseif ($matcher instanceof SelectorSyntaxMatcher) {
            /**
             * Check if a element blocked by custom element blocking (Selector Syntax) is blocked.
             *
             * @hook RCB/Blocker/SelectorSyntax/IsBlocked
             * @param {BlockedResult} $isBlocked Since 3.0.0 this is an instance of [BlockedResult](../php/classes/DevOwl-HeadlessContentBlocker-BlockedResult.html)
             * @param {SelectorSyntaxMatch} $match
             * @return {BlockedResult}
             * @since 2.6.0
             */
            $result = apply_filters('RCB/Blocker/SelectorSyntax/IsBlocked', $result, $match);
        }

        return $result;
    }

    /**
     * See `AbstractPlugin`.
     *
     * @param string[] $keepAttributes
     * @param AbstractMatcher $matcher
     * @param AbstractMatch $match
     * @return string[]
     */
    public function keepAlwaysAttributes($keepAttributes, $matcher, $match) {
        if ($matcher instanceof TagAttributeMatcher) {
            /**
             * Var.
             *
             * @var TagAttributeMatch
             */
            $match = $match;

            /**
             * In some cases we need to keep the attributes as original instead of prefix it with `consent-original-`.
             * Keep in mind, that no external data should be loaded if the attribute is set!
             *
             * @hook RCB/Blocker/KeepAttributes
             * @param {string[]} $keepAttributes
             * @param {string} $tag
             * @param {array} $attributes
             * @param {string} $linkAttribute
             * @param {string} $link
             * @return {string[]}
             * @since 1.5.0
             */
            $keepAttributes = apply_filters(
                'RCB/Blocker/KeepAttributes',
                $keepAttributes,
                $match->getTag(),
                $match->getAttributes(),
                $match->getLinkAttribute(),
                $match->getLink()
            );
        }

        return $keepAttributes;
    }

    /**
     * See `AbstractPlugin`.
     *
     * @param string[] $names
     * @param ScriptInlineMatcher $matcher
     * @param ScriptInlineMatch $match
     * @return string[]
     */
    public function skipInlineScriptVariableAssignment($names, $matcher, $match) {
        /**
         * Check if a given inline script is blocked by a localized variable name (e.g. `wp_localize_script`).
         *
         * @hook RCB/Blocker/InlineScript/AvoidBlockByLocalizedVariable
         * @param {string[]} $variables
         * @param {string} $script
         * @return {string[]}
         * @since 1.14.1
         */
        return apply_filters('RCB/Blocker/InlineScript/AvoidBlockByLocalizedVariable', $names, $match->getScript());
    }

    /**
     * See `AbstractPlugin`.
     *
     * @param boolean $extract
     * @param StyleInlineMatcher|StyleInlineAttributeMatcher $matcher
     * @param StyleInlineMatch|StyleInlineAttributeMatch $match
     * @return boolean
     */
    public function inlineStyleShouldBeExtracted($extract, $matcher, $match) {
        /**
         * Determine, if the current inline style should be split into two inline styles. One inline style
         * with only CSS rules without blocked URLs and the second one with only CSS rules with blocked URLs.
         *
         * @hook RCB/Blocker/InlineStyle/Extract
         * @param {boolean} $extract
         * @param {string} $style
         * @param {array} $attributes
         * @return {boolean}
         * @since 1.13.2
         */
        return apply_filters('RCB/Blocker/InlineStyle/Extract', true, $match->getStyle(), $match->getAttributes());
    }

    /**
     * See `AbstractPlugin`.
     *
     * @param Document $document
     * @param Document $extractedDocument
     * @param StyleInlineMatcher|StyleInlineAttributeMatcher $matcher
     * @param StyleInlineMatch|StyleInlineAttributeMatch $match
     * @return boolean
     */
    public function inlineStyleModifyDocuments($document, $extractedDocument, $matcher, $match) {
        /**
         * An inline style got blocked. We can now modify the rules again with the help of `\Sabberworm\CSS\CSSList\Document`.
         *
         * @hook RCB/Blocker/InlineStyle/Document
         * @param {Document} $document `\Sabberworm\CSS\CSSList\Document`
         * @param {Document} $extractedDocument `\Sabberworm\CSS\CSSList\Document`
         * @param {array} $attributes
         * @param {AbstractBlockable[]} $blockables
         * @param {string} $style
         * @see https://github.com/sabberworm/PHP-CSS-Parser
         * @since 1.13.2
         */
        do_action(
            'RCB/Blocker/InlineStyle/Document',
            $document,
            $extractedDocument,
            $match->getAttributes(),
            $this->getHeadlessContentBlocker()->getBlockables(),
            $match->getStyle()
        );
    }

    /**
     * See `AbstractPlugin`.
     *
     * @param BlockedResult $result
     * @param string $url
     * @param StyleInlineMatcher|StyleInlineAttributeMatcher $matcher
     * @param StyleInlineMatch|StyleInlineAttributeMatch $match
     * @return boolean
     */
    public function inlineStyleBlockRule($result, $url, $matcher, $match) {
        /**
         * Check if a given inline CSS rule is blocked.
         *
         * @hook RCB/Blocker/InlineStyle/Rule/IsBlocked
         * @param {BlockedResult} $isBlocked Since 3.0.0 this is an instance of [BlockedResult](../php/classes/DevOwl-HeadlessContentBlocker-BlockedResult.html)
         * @param {string} $url
         * @return {BlockedResult}
         * @since 1.13.2
         */
        return apply_filters('RCB/Blocker/InlineStyle/Rule/IsBlocked', $result, $url);
    }

    /**
     * See `AbstractPlugin`.
     *
     * @param boolean|string|number $visualParent
     * @param AbstractMatcher $matcher
     * @param AbstractMatch $match
     * @return boolean|string|number
     */
    public function visualParent($visualParent, $matcher, $match) {
        if ($matcher instanceof TagAttributeMatcher) {
            /**
             * Var.
             *
             * @var TagAttributeMatch
             */
            $match = $match;

            /**
             * A tag got blocked, e. g. `iframe`. We can now determine the "Visual Parent". The visual parent is the
             * closest parent where the content blocker should be placed to. The visual parent can be configured as follow:
             *
             *- `false` = Use original element
             * - `true` = Use parent element
             * - `number` = Go upwards x element (parentElement)
             * - `string` = Go upwards until parentElement matches a selector
             * - `string` = Starting with `children:` you can `querySelector` down to create the visual parent for a children (since 2.0.4)
             *
             * @hook RCB/Blocker/VisualParent
             * @param {boolean|string|number} $useVisualParent
             * @param {string} $tag
             * @param {array} $attributes
             * @return {boolean|string|number}
             * @since 1.5.0
             */
            $visualParent = apply_filters(
                'RCB/Blocker/VisualParent',
                $visualParent,
                $match->getTag(),
                $match->getAttributes()
            );
        }

        return $visualParent;
    }

    /**
     * See `AbstractPlugin`.
     *
     * @param string $html
     */
    public function modifyHtmlAfterProcessing($html) {
        /**
         * Modify HTML content for content blockers. This is called directly after the core
         * content blocker has done its job for common HTML tags (iframe, scripts, ... ) and
         * inline scripts.
         *
         * @hook RCB/Blocker/HTML
         * @param {string} $html
         * @return {string}
         */
        return apply_filters('RCB/Blocker/HTML', $html);
    }

    /**
     * See `AbstractPlugin`.
     *
     * @param string $expression
     * @param AbstractBlockable $blockable
     */
    public function blockableStringExpression($expression, $blockable) {
        // Modify `wp-content/{themes,plugins}` to the configured folder
        $expression = str_replace(
            ['wp-content/themes', 'wp-content/plugins'],
            [$this->wpContentDir . '/themes', $this->wpContentDir . '/plugins'],
            $expression
        );
        return $expression;
    }

    /**
     * See `AbstractPlugin`. In this plugin we do only allow `wordpress-filter:%s*` expressions for pseudo elements
     * catched by the e.g. `pre_http_request` filter.
     *
     * @param BlockedResult $result
     * @param AbstractBlockable $blockable
     * @param string $expression
     * @param AbstractMatcher $matcher
     * @return boolean
     */
    public function beforeSetBlockedInResult($result, $blockable, $expression, $matcher) {
        $attributes = $result->getAttributes();
        if (isset($attributes['wordpress-filter'])) {
            $allowedExpressions = sprintf('wordpress-filter:%s*', $attributes['wordpress-filter']);
            return strpos($expression, $allowedExpressions) === 0;
        }

        return true;
    }
}
