<?php
/**
 * Fastly CDN for Magento
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Fastly CDN for Magento End User License Agreement
 * that is bundled with this package in the file LICENSE_FASTLY_CDN.txt.
 *
 * DISCLAIMER
 *
 * Do not edit or add to this file if you wish to upgrade Fastly CDN to newer
 * versions in the future. If you wish to customize this module for your
 * needs please refer to http://www.magento.com for more information.
 *
 * @category    Fastly
 * @package     Fastly_Cdn
 * @copyright   Copyright (c) 2016 Fastly, Inc. (http://www.fastly.com)
 * @license     BSD, see LICENSE_FASTLY_CDN.txt
 */
namespace Fastly\Cdn\Model;

use Magento\Framework\Filesystem;
use Magento\Framework\Module\Dir;

/**
 * Model is responsible for replacing default vcl template
 * file configuration with user-defined from configuration
 *
 * @author     Magento Core Team <core@magentocommerce.com>
 */

/**
 * Class Config
 *
 * @package Fastly\Cdn\Model
 */
class Config extends \Magento\PageCache\Model\Config
{
    /**
     * Cache types
     */
    const FASTLY = 'fastly';

    /**
     * Magento module prefix used for naming vcl snippets, condition and request
     */
    const FASTLY_MAGENTO_MODULE = 'magentomodule';

    /**
     * Edge module prefix used for naming edge module snippets
     */
    const FASTLY_MODLY_MODULE = 'edgemodule';

    /**
     * Magento Error Page Response Object Name
     */
    const ERROR_PAGE_RESPONSE_OBJECT = self::FASTLY_MAGENTO_MODULE.'_error_page_response_object';

    /**
     * WAF Page Response Object Name
     */
    const WAF_PAGE_RESPONSE_OBJECT = 'WAF_Response';

    /**
     * GeoIP action "dialog"
     */
    const GEOIP_ACTION_DIALOG = 'dialog';

    /**
     * GeoIP action "redirect"
     */
    const GEOIP_ACTION_REDIRECT = 'redirect';

    /**
     * Blocking snippets directory path
     */
    const VCL_BLOCKING_PATH = '/vcl_snippets_blocking';

    /**
     * VCL blocking snippet
     */
    const VCL_BLOCKING_SNIPPET = 'recv.vcl';

    /**
     * Blocking setting name
     */
    const BLOCKING_SETTING_NAME = self::FASTLY_MAGENTO_MODULE.'_blocking_recv';

    /**
     * Rate Limiting snippets directory path
     */
    const VCL_RATE_LIMITING_PATH = '/vcl_snippets_rate_limiting';

    /**
     * Rate limiting snippet
     */
    const VCL_RATE_LIMITING_SNIPPET = 'recv.vcl';

    /**
     * Rate Limiting setting name
     */
    const RATE_LIMITING_SETTING_NAME = self::FASTLY_MAGENTO_MODULE.'_rate_limiting';

    /**
     * WAF snippets directory path
     */
    const VCL_WAF_PATH = '/vcl_snippets_waf';

    /**
     * VCL WAF snippet
     */
    const VCL_WAF_ALLOWLIST_SNIPPET = 'recv.vcl';

    /**
     * WAF setting name
     */
    const WAF_SETTING_NAME = self::FASTLY_MAGENTO_MODULE.'_waf_recv';

    /**
     * Authentication snippets directory path
     */
    const VCL_AUTH_SNIPPET_PATH = '/vcl_snippets_basic_auth';

    /**
     * Maintenance snippets directory path
     */
    const VCL_MAINT_SNIPPET_PATH = '/vcl_snippets_maintenance';

    /**
     * Authentication dictionary name
     */
    const AUTH_DICTIONARY_NAME = self::FASTLY_MAGENTO_MODULE.'_basic_auth';

    /**
     * Image optimization setting name
     */
    const IMAGE_SETTING_NAME = self::FASTLY_MAGENTO_MODULE.'_image_optimization_recv';

    /**
     * Force TLS snippet path
     */
    const FORCE_TLS_PATH = '/vcl_snippets_force_tls';

    /**
     * Force TLS setting name
     */
    const FORCE_TLS_SETTING_NAME = self::FASTLY_MAGENTO_MODULE.'_force_tls_recv';

    /**
     * Configure Dictionary name
     */
    const CONFIG_DICTIONARY_NAME = self::FASTLY_MAGENTO_MODULE.'_config';

    /**
     * Maintenance Allowlist name
     */
    const MAINT_ACL_NAME = 'maint_allowlist';

    /**
     * Config Dictionary key
     */
    const CONFIG_DICTIONARY_KEY = 'allow_super_users_during_maint';

    /**
     * Custom snippet path
     */
    const CUSTOM_SNIPPET_PATH = 'vcl_snippets_custom/';

    /**
     * Image optimization condition name
     */
    const IO_CONDITION_NAME = 'fastly-image-optimizer-condition';

    /**
     * Image optimization header name
     */
    const IO_HEADER_NAME = 'fastly-image-optimizer-header';

    /**
     * Image optimization snippet path
     */
    const IO_VCL_SNIPPET_PATH = '/vcl_snippets_image_optimizations';

    /**
     * Error page snippet path
     */
    const VCL_ERROR_SNIPPET_PATH = '/vcl_snippets_error_page';

    /**
     * Error page snippet
     */
    const VCL_ERROR_SNIPPET = 'deliver.vcl';

    /**
     * XML path to Fastly config template path
     */
    const FASTLY_CONFIGURATION_PATH = 'system/full_page_cache/fastly/path';

    /**
     * Path to Fastly service ID
     */
    const FASTLY_API_ENDPOINT = 'https://api.fastly.com/';

    /**
     * XML path to Fastly service ID
     */
    const XML_FASTLY_SERVICE_ID = 'system/full_page_cache/fastly/fastly_service_id';

    /**
     * XML path to Fastly API token
     */
    const XML_FASTLY_API_KEY = 'system/full_page_cache/fastly/fastly_api_key';

    /**
     * XML path to stale ttl path
     */
    const XML_FASTLY_STALE_TTL = 'system/full_page_cache/fastly/fastly_advanced_configuration/stale_ttl';

    /**
     * config path to basic auth status
     */
    const FASTLY_BASIC_AUTH_ENABLE = 'system/full_page_cache/fastly/fastly_basic_auth/enable_basic_auth';

    /**
     * XML path to stale error ttl path
     */
    const XML_FASTLY_STALE_ERROR_TTL = 'system/full_page_cache/fastly/fastly_advanced_configuration/stale_error_ttl';

    /**
     * XML path to Fastly admin path timeout
     */
    const XML_FASTLY_ADMIN_PATH_TIMEOUT
        = 'system/full_page_cache/fastly/fastly_advanced_configuration/admin_path_timeout';

    /**
     * Max first byte timeout value
     */
    const XML_FASTLY_MAX_FIRST_BYTE_TIMEOUT = 600;

    /**
     * XML path to Fastly ignored url parameters
     */
    const XML_FASTLY_IGNORED_URL_PARAMETERS
        = 'system/full_page_cache/fastly/fastly_advanced_configuration/ignored_url_parameters';

    /**
     * XML path to X-Magento-Tags size value
     */

    const XML_FASTLY_X_MAGENTO_TAGS_SIZE
        = 'system/full_page_cache/fastly/fastly_advanced_configuration/x_magento_tags_size';

    /**
     * XML path to purge catalog category
     */
    const XML_FASTLY_PURGE_CATALOG_CATEGORY
        = 'system/full_page_cache/fastly/fastly_advanced_configuration/purge_catalog_category';

    /**
     * XML path to purge catalog product
     */
    const XML_FASTLY_PURGE_CATALOG_PRODUCT
        = 'system/full_page_cache/fastly/fastly_advanced_configuration/purge_catalog_product';

    /**
     * XML path to purge CMS page
     */
    const XML_FASTLY_PURGE_CMS_PAGE = 'system/full_page_cache/fastly/fastly_advanced_configuration/purge_cms_page';

    /**
     * XML path to config preserve_static
     */
    const XML_FASTLY_PRESERVE_STATIC = 'system/full_page_cache/fastly/fastly_advanced_configuration/preserve_static';

    /**
     * XML path to soft purge
     */
    const XML_FASTLY_SOFT_PURGE = 'system/full_page_cache/fastly/fastly_advanced_configuration/soft_purge';

    /**
     * XML path to enable GeoIP
     */
    const XML_FASTLY_GEOIP_ENABLED = 'system/full_page_cache/fastly/fastly_advanced_configuration/enable_geoip';

    /**
     * XML path to GeoIP action
     */
    const XML_FASTLY_GEOIP_ACTION = 'system/full_page_cache/fastly/fastly_advanced_configuration/geoip_action';

    /**
     * XML path to GeoIP redirect mapping
     */
    const XML_FASTLY_GEOIP_COUNTRY_MAPPING
        = 'system/full_page_cache/fastly/fastly_advanced_configuration/geoip_country_mapping';

    /**
     * XML path to Rate Limiting paths
     */
    const XML_FASTLY_RATE_LIMITING_PATHS
        = 'system/full_page_cache/fastly/fastly_rate_limiting_settings/fastly_path_protection/rate_limiting_paths';

    /**
     * XML path to Rate Limiting limit
     */
    const XML_FASTLY_RATE_LIMITING_LIMIT
        = 'system/full_page_cache/fastly/fastly_rate_limiting_settings/fastly_path_protection/rate_limiting_limit';

    /**
     * XML path to Rate Limiting TTL
     */
    const XML_FASTLY_RATE_LIMITING_TTL
        = 'system/full_page_cache/fastly/fastly_rate_limiting_settings/fastly_path_protection/rate_limiting_ttl';

    /**
     * XML path to image optimizations flag
     */
    const XML_FASTLY_IMAGE_OPTIMIZATIONS
        = 'system/full_page_cache/fastly/fastly_image_optimization_configuration/image_optimizations';

    /**
     * XML path to image optimization force lossy flag
     */
    const XML_FASTLY_FORCE_LOSSY
        = 'system/full_page_cache/fastly/fastly_image_optimization_configuration/image_optimization_force_lossy';

    /**
     * XML path to image optimization bg color flag
     */
    const XML_FASTLY_IMAGE_OPTIMIZATION_BG_COLOR
        = 'system/full_page_cache/fastly/fastly_image_optimization_configuration/image_optimization_bg_color';

    /**
     * XML path to image optimization image quality value
     */
    const XML_FASTLY_IMAGE_OPTIMIZATION_IMAGE_QUALITY
        = 'system/full_page_cache/fastly/fastly_image_optimization_configuration/image_optimization_image_quality';

    /**
     * XML path to image optimization canvas flag
     */
    const XML_FASTLY_IMAGE_OPTIMIZATION_CANVAS
        = 'system/full_page_cache/fastly/fastly_image_optimization_configuration/image_optimization_canvas';

    /**
     * XML path to image optimizations pixel ratio flag
     */
    const XML_FASTLY_IMAGE_OPTIMIZATIONS_PIXEL_RATIO
        = 'system/full_page_cache/fastly/fastly_image_optimization_configuration/image_optimizations_pixel_ratio';

    /**
     * XML path to image optimizations pixel ratios
     */
    const XML_FASTLY_IMAGE_OPTIMIZATIONS_RATIOS
        = 'system/full_page_cache/fastly/fastly_image_optimization_configuration/image_optimizations_ratios';

    /**
     * XML path to Google analytics CID
     */
    const XML_FASTLY_GA_CID = 'system/full_page_cache/fastly/fastly_ga_cid';

    /**
     * XML path to Last checked issued Fastly M2 version
     */
    const XML_FASTLY_LAST_CHECKED_ISSUED_VERSION = 'system/full_page_cache/fastly/last_checked_issues_version';

    /**
     * XML path to Fastly module version
     */
    const XML_FASTLY_MODULE_VERSION = 'system/full_page_cache/fastly/current_version';

    /**
     * XML path to Fastly list of blocked countries
     */
    const XML_FASTLY_BLOCK_BY_COUNTRY = 'system/full_page_cache/fastly/fastly_blocking/block_by_country';

    /**
     * XML path to Fastly list of blocked Acls
     */
    const XML_FASTLY_BLOCK_BY_ACL = 'system/full_page_cache/fastly/fastly_blocking/block_by_acl';

    /**
     * XML path to the Fastly Blocking Type flag
     */
    const XML_FASTLY_BLOCKING_TYPE = 'system/full_page_cache/fastly/fastly_blocking/blocking_type';

    /**
     * XML path to Fastly list of WAF allowed Acls
     */
    const XML_FASTLY_WAF_ALLOW_BY_ACL =
        'system/full_page_cache/fastly/fastly_web_application_firewall/waf_allow_by_acl';

    /**
     * XML path to enable Webhooks
     */
    const XML_FASTLY_WEBHOOKS_ENABLED = 'system/full_page_cache/fastly/fastly_web_hooks/enable_webhooks';

    /**
     * XML path to Webhook Username
     */
    const XML_FASTLY_WEBHOOKS_USERNAME = 'system/full_page_cache/fastly/fastly_web_hooks/webhooks_username';

    /**
     * XML path to Incoming webhook URL
     */
    const XML_FASTLY_INCOMING_WEBHOOK_URL = 'system/full_page_cache/fastly/fastly_web_hooks/incoming_webhook_url';

    /**
     * XML path to enable Publish Key and URL Purge Events
     */
    const XML_FASTLY_PUBLISH_KEY_URL_PURGE_EVENTS
        = 'system/full_page_cache/fastly/fastly_web_hooks/publish_key_url_purge_events';

    /**
     * XML path to enable Publish Purge All/Clean All Items Events
     */
    const XML_FASTLY_PUBLISH_PURGE_ALL_EVENTS
        = 'system/full_page_cache/fastly/fastly_web_hooks/publish_purge_all_items_events';

    /**
     * XML path to enable Publish Purge Events
     */
    const XML_FASTLY_PUBLISH_PURGE_EVENTS
        = 'system/full_page_cache/fastly/fastly_web_hooks/publish_purge_events';

    /**
     * XML path to enable Publish Purge All/Clean backtrace
     */
    const XML_FASTLY_PUBLISH_PURGE_ALL_TRACE
        = 'system/full_page_cache/fastly/fastly_web_hooks/publish_purge_all_trace';

    /**
     * XML path to enable Publish Purge By Key backtrace
     */
    const XML_FASTLY_PUBLISH_PURGE_BY_KEY_TRACE
        = 'system/full_page_cache/fastly/fastly_web_hooks/publish_purge_by_key_trace';

    /**
     * XML path to enable Publish Generic Purge
     */
    const XML_FASTLY_PUBLISH_PURGE_TRACE
        = 'system/full_page_cache/fastly/fastly_web_hooks/publish_purge_trace';

    /**
     * XML path to enable Publish Config change events
     */
    const XML_FASTLY_PUBLISH_CONFIG_CHANGE_EVENTS
        = 'system/full_page_cache/fastly/fastly_web_hooks/publish_config_change_events';

    /**
     * XML path to enable Publish Config change events
     */
    const XML_FASTLY_WEBHOOK_MESSAGE_PREFIX
        = 'system/full_page_cache/fastly/fastly_web_hooks/webhook_message_prefix';

    /**
     * XML path to enable Rate Limiting
     */
    const XML_FASTLY_RATE_LIMITING_ENABLE
        = 'system/full_page_cache/fastly/fastly_rate_limiting_settings/fastly_path_protection/enable_rate_limiting';

    /**
     * XML path to enable Crawler Protection
     */
    const XML_FASTLY_CRAWLER_PROTECTION_ENABLE
        = 'system/full_page_cache/fastly/fastly_rate_limiting_settings/crawler_protection/enable_crawler_protection';

    /**
     * XML path to Crawler Protection Rate Limiting limit
     */
    const XML_FASTLY_CRAWLER_RATE_LIMITING_LIMIT
        = 'system/full_page_cache/fastly/fastly_rate_limiting_settings/crawler_protection/crawler_rate_limiting_limit';

    /**
     * XML path to Crawler Protection Rate Limiting TTL
     */
    const XML_FASTLY_CRAWLER_RATE_LIMITING_TTL
        = 'system/full_page_cache/fastly/fastly_rate_limiting_settings/crawler_protection/crawler_rate_limiting_ttl';

    /**
     * XML path to Exempt Good Bots flag
     */
    const XML_FASTLY_EXEMPT_GOOD_BOTS
        = 'system/full_page_cache/fastly/fastly_rate_limiting_settings/crawler_protection/exempt_good_bots';

    /**
     * Check if Fastly is selected for Caching Application
     *
     * @return bool
     */
    public function isFastlyEnabled()
    {
        if ($this->getType() == Config::FASTLY) {
            return true;
        }

        return false;
    }

    /**
     * Return Fastly module version from core resource
     *
     * @return string
     */
    public function getFastlyVersion()
    {
        return $this->_scopeConfig->getValue(self::XML_FASTLY_MODULE_VERSION);
    }

    /**
     * Return Google Analytics CID
     *
     * @return string
     */
    public function getCID()
    {
        return $this->_scopeConfig->getValue(self::XML_FASTLY_GA_CID);
    }

    /**
     * Return Fastly API endpoint
     *
     * @return string
     */
    public function getApiEndpoint()
    {
        return self::FASTLY_API_ENDPOINT;
    }

    /**
     * Return Fastly service IP
     *
     * @return int
     */
    public function getServiceId()
    {
        return $this->_scopeConfig->getValue(self::XML_FASTLY_SERVICE_ID);
    }

    /**
     * Return Fastly API token
     *
     * @return int
     */
    public function getApiKey()
    {
        return $this->_scopeConfig->getValue(self::XML_FASTLY_API_KEY);
    }

    /**
     * Return page lifetime
     *
     * @return int
     */
    public function getTtl()
    {
        return (int)$this->_scopeConfig->getValue(self::XML_PAGECACHE_TTL);
    }

    /**
     * Return page lifetime
     *
     * @return int
     */
    public function getStaleTtl()
    {
        return (int)$this->_scopeConfig->getValue(self::XML_FASTLY_STALE_TTL);
    }

    /**
     * Return page lifetime
     *
     * @return int
     */
    public function getStaleErrorTtl()
    {
        return (int)$this->_scopeConfig->getValue(self::XML_FASTLY_STALE_ERROR_TTL);
    }

    /**
     * Return admin path timeout
     *
     * @return int
     */
    public function getAdminPathTimeout()
    {
        return (int)$this->_scopeConfig->getValue(self::XML_FASTLY_ADMIN_PATH_TIMEOUT);
    }

    public function getXMagentoTagsSize()
    {
        return (int)$this->_scopeConfig->getValue(self::XML_FASTLY_X_MAGENTO_TAGS_SIZE);
    }

    /**
     * Return Fastly ignored url parameters
     *
     * @return int
     */
    public function getIgnoredUrlParameters()
    {
        return $this->_scopeConfig->getValue(self::XML_FASTLY_IGNORED_URL_PARAMETERS);
    }

    /**
     * Return basic auth status
     *
     * @return int
     */
    public function getBasicAuthenticationStatus()
    {
        return (int)$this->_scopeConfig->getValue(self::FASTLY_BASIC_AUTH_ENABLE);
    }

    /**
     * Returns can purge catalog category.
     *
     * @return bool
     */
    public function canPurgeCatalogCategory()
    {
        return $this->_scopeConfig->isSetFlag(self::XML_FASTLY_PURGE_CATALOG_CATEGORY);
    }

    /**
     * Returns can purge catalog product.
     *
     * @return bool
     */
    public function canPurgeCatalogProduct()
    {
        return $this->_scopeConfig->isSetFlag(self::XML_FASTLY_PURGE_CATALOG_PRODUCT);
    }

    /**
     * Returns can purge CMS page.
     *
     * @return bool
     */
    public function canPurgeCmsPage()
    {
        return $this->_scopeConfig->isSetFlag(self::XML_FASTLY_PURGE_CMS_PAGE);
    }

    /**
     * Should we flush all or preserve static?
     *
     * @return bool
     */
    public function canPreserveStatic()
    {
        return $this->_scopeConfig->isSetFlag(self::XML_FASTLY_PRESERVE_STATIC);
    }

    /**
     * Returns can use soft purge
     *
     * @return bool
     */
    public function canUseSoftPurge()
    {
        return $this->_scopeConfig->isSetFlag(self::XML_FASTLY_SOFT_PURGE);
    }

    /**
     * Return is GeoIP enabled
     *
     * @return bool
     */
    public function isGeoIpEnabled()
    {
        return ($this->isEnabled() && $this->_scopeConfig->isSetFlag(self::XML_FASTLY_GEOIP_ENABLED));
    }

    /**
     * Return GeoIP action
     *
     * @return string
     */
    public function getGeoIpAction()
    {
        return $this->_scopeConfig->getValue(self::XML_FASTLY_GEOIP_ACTION);
    }

    /**
     * Return GeoIP redirect mapping
     *
     * @return array
     */
    public function getGeoIpRedirectMapping()
    {
        return $this->_scopeConfig->getValue(self::XML_FASTLY_GEOIP_COUNTRY_MAPPING);
    }

    /**
     * Determines should Image optimization be used
     *
     * @return bool
     */
    public function isImageOptimizationEnabled()
    {
        if ($this->isFastlyEnabled() !== true) {
            return false;
        }

        return $this->_scopeConfig->isSetFlag(self::XML_FASTLY_IMAGE_OPTIMIZATIONS);
    }

    /**
     * Determines should Image optimization pixel ratios be used
     *
     * @return bool
     */
    public function isImageOptimizationPixelRatioEnabled()
    {
        if ($this->isImageOptimizationEnabled() !== true) {
            return false;
        }

        return $this->_scopeConfig->isSetFlag(self::XML_FASTLY_IMAGE_OPTIMIZATIONS_PIXEL_RATIO);
    }

    /**
     * Checks if the image optimization force lossy option is enabled
     *
     * @return mixed
     */
    public function isForceLossyEnabled()
    {
        return $this->_scopeConfig->getValue(self::XML_FASTLY_FORCE_LOSSY);
    }

    /**
     * Checks if the image optimization bg color option is enabled
     *
     * @return mixed
     */
    public function isImageOptimizationBgColorEnabled()
    {
        return $this->_scopeConfig->getValue(self::XML_FASTLY_IMAGE_OPTIMIZATION_BG_COLOR);
    }

    /**
     * Return image optimization pixel ratios
     *
     * @return mixed
     */
    public function getImageOptimizationRatios()
    {
        return $this->_scopeConfig->getvalue(self::XML_FASTLY_IMAGE_OPTIMIZATIONS_RATIOS);
    }

    /**
     * Return blocked countries
     *
     * @return mixed
     */
    public function getBlockByCountry()
    {
        return $this->_scopeConfig->getValue(self::XML_FASTLY_BLOCK_BY_COUNTRY);
    }

    /**
     * Return blocked Acls
     *
     * @return mixed
     */
    public function getBlockByAcl()
    {
        return $this->_scopeConfig->getValue(self::XML_FASTLY_BLOCK_BY_ACL);
    }

    public function getWafAllowByAcl()
    {
        return $this->_scopeConfig->getValue(self::XML_FASTLY_WAF_ALLOW_BY_ACL);
    }

    /**
     * Return are Webhooks enabled
     *
     * @return bool
     */
    public function areWebHooksEnabled()
    {
        return ($this->isEnabled() && $this->_scopeConfig->isSetFlag(self::XML_FASTLY_WEBHOOKS_ENABLED));
    }

    /**
     * Get Webhooks Endpoint URL
     * @return mixed
     */
    public function getIncomingWebhookURL()
    {
        return $this->_scopeConfig->getValue(self::XML_FASTLY_INCOMING_WEBHOOK_URL);
    }

    /**
     * Get Webhooks Username
     *
     * @return mixed
     */
    public function getWebhookUsername()
    {
        return $this->_scopeConfig->getValue(self::XML_FASTLY_WEBHOOKS_USERNAME);
    }

    /**
     * Return is Publish Key and URL Purge Events enabled
     *
     * @return bool
     */
    public function canPublishKeyUrlChanges()
    {
        return ($this->isEnabled() && $this->_scopeConfig->isSetFlag(self::XML_FASTLY_PUBLISH_KEY_URL_PURGE_EVENTS));
    }

    /**
     * return is Publish Purge All/Clean All Items Events enabled
     *
     * @return bool
     */
    public function canPublishPurgeAllChanges()
    {
        return ($this->isEnabled() && $this->_scopeConfig->isSetFlag(self::XML_FASTLY_PUBLISH_PURGE_ALL_EVENTS));
    }

    /**
     * Is publishing purge changes allowed
     *
     * @return bool
     */
    public function canPublishPurgeChanges()
    {
        return ($this->isEnabled() && $this->_scopeConfig->isSetFlag(self::XML_FASTLY_PUBLISH_PURGE_EVENTS));
    }

    /**
     * Is publishing backtrace on purge all allowed
     *
     * @return bool
     */
    public function canPublishPurgeAllDebugBacktrace()
    {
        return ($this->isEnabled() && $this->_scopeConfig->isSetFlag(self::XML_FASTLY_PUBLISH_PURGE_ALL_TRACE));
    }

    /**
     * Is publishing backtrace on purge by key allowed
     *
     * @return bool
     */
    public function canPublishPurgeByKeyDebugBacktrace()
    {
        return ($this->isEnabled() && $this->_scopeConfig->isSetFlag(self::XML_FASTLY_PUBLISH_PURGE_BY_KEY_TRACE));
    }

    /**
     * Is publishing backtrace on generic purge allowed
     *
     * @return bool
     */
    public function canPublishPurgeDebugBacktrace()
    {
        return ($this->isEnabled() && $this->_scopeConfig->isSetFlag(self::XML_FASTLY_PUBLISH_PURGE_TRACE));
    }

    /**
     * return is Publish Config change events enabled
     *
     * @return bool
     */
    public function canPublishConfigChanges()
    {
        return ($this->isEnabled() && $this->_scopeConfig->isSetFlag(self::XML_FASTLY_PUBLISH_CONFIG_CHANGE_EVENTS));
    }

    /**
     * return Webhook message format
     *
     * @return mixed
     */
    public function getWebhookMessagePrefix()
    {
        return $this->_scopeConfig->getValue(self::XML_FASTLY_WEBHOOK_MESSAGE_PREFIX);
    }

    /**
     * return Webhook message format
     *
     * @return mixed
     */
    public function getLastCheckedIssuedVersion()
    {
        return $this->_scopeConfig->getValue(self::XML_FASTLY_LAST_CHECKED_ISSUED_VERSION);
    }

    /**
     * return Rate Limiting status
     *
     * @return mixed
     */
    public function isRateLimitingEnabled()
    {
        return $this->_scopeConfig->getValue(self::XML_FASTLY_RATE_LIMITING_ENABLE);
    }

    /**
     * return Rate Limiting limit
     *
     * @return mixed
     */
    public function getRateLimitingLimit()
    {
        return $this->_scopeConfig->getValue(self::XML_FASTLY_RATE_LIMITING_LIMIT);
    }

    /**
     * return Rate Limiting TTL
     *
     * @return mixed
     */
    public function getRateLimitingTtl()
    {
        return $this->_scopeConfig->getValue(self::XML_FASTLY_RATE_LIMITING_TTL);
    }

    /**
     * return Crawler Protection status
     *
     * @return mixed
     */
    public function isCrawlerProtectionEnabled()
    {
        return $this->_scopeConfig->getValue(self::XML_FASTLY_CRAWLER_PROTECTION_ENABLE);
    }

    /**
     * return Crawler Rate Limiting limit
     *
     * @return mixed
     */
    public function getCrawlerRateLimitingLimit()
    {
        return $this->_scopeConfig->getValue(self::XML_FASTLY_CRAWLER_RATE_LIMITING_LIMIT);
    }

    /**
     * return Crawler Rate Limiting TTL
     *
     * @return mixed
     */
    public function getCrawlerRateLimitingTtl()
    {
        return $this->_scopeConfig->getValue(self::XML_FASTLY_CRAWLER_RATE_LIMITING_TTL);
    }

    /**
     * Check if exempt good bots is enabled
     *
     * @return mixed
     */
    public function isExemptGoodBotsEnabled()
    {
        return $this->_scopeConfig->getValue(self::XML_FASTLY_EXEMPT_GOOD_BOTS);
    }

    /**
     * Get store ID for country.
     *
     * @param $countryCode 2-digit country code
     * @return int|null
     */
    public function getGeoIpMappingForCountry($countryCode)
    {
        if ($mapping = $this->_scopeConfig->getValue(self::XML_FASTLY_GEOIP_COUNTRY_MAPPING)) {
            return $this->extractMapping($mapping, $countryCode);
        }
        return null;
    }

    /**
     * Filter country code mapping by priority
     *
     * @param $mapping
     * @param $countryCode
     * @return int|null
     */
    private function extractMapping($mapping, $countryCode)
    {
        $final = null;
        $extractMapping = json_decode($mapping, true);
        if (!$extractMapping) {
            try {
                $extractMapping = unserialize($mapping); // @codingStandardsIgnoreLine
            } catch (\Exception $e) {
                $extractMapping = [];
            }
        }

        if (is_array($extractMapping)) {
            $countryId = 'country_id';
            $key = 'store_id';
            // check for direct match
            foreach ($extractMapping as $map) {
                if (is_array($map) &&
                    isset($map[$countryId]) &&
                    strtolower(str_replace(' ', '', $map[$countryId])) == strtolower($countryCode)) {
                    if (isset($map[$key])) {
                        return (int)$map[$key];
                    }
                } elseif (is_array($map) &&
                    isset($map[$countryId]) &&
                    $map[$countryId] == '*' &&
                    isset($map[$key]) &&
                    $final === null) {
                    // check for wildcard
                    $final = (int)$map[$key];
                }
            }
        }
        return $final;
    }

    /**
     * @return array|mixed|null
     */
    public function getRateLimitPaths()
    {
        return $this->_scopeConfig->getValue(self::XML_FASTLY_RATE_LIMITING_PATHS);
    }

    /**
     * Return generated magento2_fastly_varnish.vcl configuration file
     *
     * @param string $vclTemplatePath
     * @return string
     * @throws \Magento\Framework\Exception\FileSystemException
     */
    public function getVclFile($vclTemplatePath) // @codingStandardsIgnoreLine - Unused parameter required due to compatibility with parent class
    {
        $moduleEtcPath = $this->reader->getModuleDir(Dir::MODULE_ETC_DIR, 'Fastly_Cdn');
        $configFilePath = $moduleEtcPath . '/' . $this->_scopeConfig->getValue(self::FASTLY_CONFIGURATION_PATH);
        $directoryRead = $this->readFactory->create($moduleEtcPath);
        $configFilePath = $directoryRead->getRelativePath($configFilePath);
        $data = $directoryRead->readFile($configFilePath);
        return strtr($data, $this->getReplacements());
    }

    /**
     * Returns VCL snippet data
     *
     * @param string $path
     * @param null $specificFile
     * @return array
     * @throws \Magento\Framework\Exception\FileSystemException
     */
    public function getVclSnippets($path = '/vcl_snippets', $specificFile = null)
    {
        $snippetsData = [];

        $moduleEtcPath = $this->reader->getModuleDir(Dir::MODULE_ETC_DIR, 'Fastly_Cdn') . $path;
        $directoryRead = $this->readFactory->create($moduleEtcPath);
        if (!$specificFile) {
            $files = $directoryRead->read();

            if (is_array($files)) {
                foreach ($files as $file) {
                    if (substr($file, strpos($file, ".") + 1) !== 'vcl') {
                        continue;
                    }
                    $snippetFilePath = $moduleEtcPath . '/' . $file;
                    $snippetFilePath = $directoryRead->getRelativePath($snippetFilePath);
                    $type = explode('.', $file)[0];
                    $snippetsData[$type] = $directoryRead->readFile($snippetFilePath);
                }
            }
        } else {
            $snippetFilePath = $moduleEtcPath . '/' . $specificFile;
            $snippetFilePath = $directoryRead->getRelativePath($snippetFilePath);
            $type = explode('.', $specificFile)[0];
            $snippetsData[$type] = $directoryRead->readFile($snippetFilePath);
        }

        return $snippetsData;
    }

    public function getCustomSnippets($path, $specificFile = null)
    {
        $snippetsData = [];
        try {
            $directoryRead = $this->readFactory->create($path);
            if (!$specificFile) {
                $files = $directoryRead->read();

                if (is_array($files)) {
                    foreach ($files as $file) {
                        if (substr($file, strpos($file, ".") + 1) !== 'vcl') {
                            continue;
                        }
                        $snippetFilePath = $path . '/' . $file;
                        $snippetFilePath = $directoryRead->getRelativePath($snippetFilePath);
                        $type = explode('.', $file)[0];
                        $snippetsData[$type] = $directoryRead->readFile($snippetFilePath);
                    }
                }
            } else {
                $snippetFilePath = $path . '/' . $specificFile;
                $snippetFilePath = $directoryRead->getRelativePath($snippetFilePath);
                $type = explode('.', $specificFile)[0];
                $snippetsData[$type] = $directoryRead->readFile($snippetFilePath);
            }
            return $snippetsData;
        } catch (\Exception $e) {
            return $snippetsData;
        }
    }

    public function getFastlyEdgeModules($path = '/fastly_edge_modules', $specificFile = null)
    {
        $moduleData = [];
        try {
            $moduleEtcPath = $this->reader->getModuleDir(Dir::MODULE_ETC_DIR, 'Fastly_Cdn') . $path;
            $directoryRead = $this->readFactory->create($moduleEtcPath);
            if (!$specificFile) {
                $files = $directoryRead->read();

                if (is_array($files)) {
                    foreach ($files as $file) {
                        if (substr($file, strpos($file, ".") + 1) !== 'json') {
                            continue;
                        }
                        $fastlyModuleFilePath = $moduleEtcPath . '/' . $file;
                        $fastlyModuleFilePath = $directoryRead->getRelativePath($fastlyModuleFilePath);
                        $type = explode('.', $file)[0];
                        $moduleData[$type] = $directoryRead->readFile($fastlyModuleFilePath);
                    }
                }
            } else {
                $fastlyModuleFilePath = $moduleEtcPath . '/' . $specificFile;
                $fastlyModuleFilePath = $directoryRead->getRelativePath($fastlyModuleFilePath);
                $type = explode('.', $specificFile)[0];
                $moduleData[$type] = $directoryRead->readFile($fastlyModuleFilePath);
            }
            return $moduleData;
        } catch (\Exception $e) {
            return $moduleData;
        }
    }

    /**
     * Prepare data for VCL config
     *
     * @return array
     */
    private function getReplacements()
    {
        return [
            '### {{ design_exceptions_code }} ###' => $this->getDesignExceptions()
        ];
    }

    /**
     * Get regexs for design exceptions
     * Different browser user-agents may use different themes
     * Varnish supports regex with internal modifiers only so
     * we have to convert "/pattern/iU" into "(?Ui)pattern"
     *
     * @return string
     */
    private function getDesignExceptions()
    {
        $result = '';
        $tpl = "        %s (req.http.user-agent ~ \"%s\") {\n" . "            set req.hash += \"%s\";\n" . "        }";

        $expressions = $this->_scopeConfig->getValue(
            self::XML_VARNISH_PAGECACHE_DESIGN_THEME_REGEX,
            \Magento\Store\Model\ScopeInterface::SCOPE_STORE
        );
        if ($expressions) {
            try {
                $expressions = unserialize($expressions); // @codingStandardsIgnoreLine - used for conversion of old Magento format to json_decode
            } catch (\Exception $e) {
                $expressions = [];
            }
            $rules = array_values($expressions);
            foreach ($rules as $i => $rule) {
                if (preg_match('/^[\W]{1}(.*)[\W]{1}(\w+)?$/', $rule['regexp'], $matches)) {
                    if (!empty($matches[2])) {
                        $pattern = sprintf("(?%s)%s", $matches[2], $matches[1]);
                    } else {
                        $pattern = $matches[1];
                    }
                    $if = $i == 0 ? 'if' : ' elsif';
                    $result .= sprintf($tpl, $if, $pattern, $rule['value']);
                }
            }
        }

        if (!empty($result)) {
            $result = 'if (req.url ~ "^/(pub/)?(media|static)/.*") {' . "\n" . $result . "\n    }";
        }

        return $result;
    }

    /**
     * Validate custom snippet data
     *
     * @param $name
     * @param $type
     * @param $priority
     * @return array
     */
    public function validateCustomSnippet($name, $type, $priority)
    {
        $snippetName = str_replace(' ', '', $name);
        $types = ['init', 'recv', 'hit', 'miss', 'pass', 'fetch', 'error', 'deliver', 'log', 'hash', 'none'];

        $inArray = in_array($type, $types);
        $isNumeric = is_numeric($priority);
        $isAlphanumeric = preg_match('/^[\w]+$/', $snippetName);
        $error = null;

        if (!$inArray) {
            $error = 'Type value is not recognised.';
            return [
                'snippet_name' => null,
                'error' => $error
            ];
        }
        if (!$isNumeric) {
            $error = 'Please make sure that the priority value is a number.';
            return [
                'snippet_name' => null,
                'error' => $error
            ];
        }
        if (!$isAlphanumeric) {
            $error = 'Please make sure that the name value contains only alphanumeric characters.';
            return [
                'snippet_name' => null,
                'error' => $error
            ];
        }
        return [
            'snippet_name' => $snippetName,
            'error' => $error
        ];
    }

    /**
     * Process blocked items depending on blocking type
     *
     * @param $strippedBlockedItems
     * @param null $blockingType
     * @return string
     */
    public function processBlockedItems($strippedBlockedItems, $blockingType = null)
    {
        if (empty($blockingType)) {
            $blockingType = $this->_scopeConfig->getValue(self::XML_FASTLY_BLOCKING_TYPE);
        }
        if ($blockingType == '1') {
            $strippedBlockedItems = '!(' . $strippedBlockedItems . ')';
        }
        return $strippedBlockedItems;
    }
}


<?php

namespace Drupal\thunder\Plugin\Thunder\OptionalModule;

use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;

/**
 * Google Analytics.
 *
 * @ThunderOptionalModule(
 *   id = "google_analytics",
 *   label = @Translation("Google Analytics"),
 *   description = @Translation("Google Analytics lets you measure your advertising ROI as well as track your video, and social networking sites and applications."),
 *   type = "module",
 * )
 */
class GoogleAnalytics extends AbstractOptionalModule {

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {

    $form = parent::buildForm($form, $form_state);

    $form['google_analytics']['ga_account'] = [
      '#description' => t('This ID is unique to each site you want to track separately, and is in the form of UA-xxxxxxx-yy. To get a Web Property ID, register your site with Google Analytics, or if you already have registered your site, go to your Google Analytics Settings page to see the ID next to every site profile. Find more information in the documentation.', [
        ':analytics' => 'http://www.google.com/analytics/',
        ':webpropertyid' => Url::fromUri('https://developers.google.com/analytics/resources/concepts/gaConceptsAccounts', ['fragment' => 'webProperty'])
          ->toString(),
      ]),
      '#maxlength' => 20,
      '#placeholder' => 'UA-',
      '#size' => 15,
      '#title' => t('Web Property ID'),
      '#type' => 'textfield',
    ];

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array $formValues) {

    $this->configFactory->getEditable('google_analytics.settings')
      ->set('account', (string) $formValues['ga_account'])
      ->save(TRUE);
  }

}


<?php

namespace Drupal\thunder_media\Plugin\ImageEffect;

use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Image\ImageInterface;
use Drupal\image\ConfigurableImageEffectBase;
use Drupal\image\Entity\ImageStyle;

/**
 * Resizes an image resource.
 *
 * @ImageEffect(
 *   id = "thunder_media_auto_aspect",
 *   label = @Translation("Auto Aspect"),
 *   description = @Translation("Use different effects depending on whether the image is landscape of portrait shaped. This re-uses other preset definitions, and just chooses between them based on the rule.")
 * )
 */
class AutoAspectEffect extends ConfigurableImageEffectBase {

  /**
   * {@inheritdoc}
   */
  public function applyEffect(ImageInterface $image) {
    $ratio_adjustment = isset($this->configuration['ratio_adjustment']) ? floatval($this->configuration['ratio_adjustment']) : 1;
    $aspect = $image->getWidth() / $image->getHeight();

    // Calculate orientation: width / height * adjustment. If > 1, it's wide.
    $style_name = (($aspect * $ratio_adjustment) > 1) ? $this->configuration['landscape'] : $this->configuration['portrait'];

    if (empty($style_name)) {
      // Do nothing. just return what we've got.
      return TRUE;
    }

    /* @var ImageStyle $style */
    $style = ImageStyle::load($style_name);
    if (empty($style)) {
      // Required preset has gone missing?
      return FALSE;
    }

    // Run the preset actions ourself.
    foreach ($style->getEffects() as $sub_effect) {
      /* @var ImageEffectInterface $sub_effect */
      $sub_effect->applyEffect($image);
    }
    return TRUE;
  }

  /**
   * {@inheritdoc}
   */
  public function transformDimensions(array &$dimensions, $uri) {
    if (!isset($dimensions['width']) || !isset($dimensions['height'])) {
      // We cannot know which preset would be executed and thus cannot know the
      // resulting dimensions, unless both styles return the same dimensions:
      $landscape_dimensions = $portrait_dimensions = $dimensions;

      /* @var ImageStyle $landscape_style */
      $landscape_style = ImageStyle::load($this->configuration['landscape']);
      $landscape_style->transformDimensions($landscape_dimensions, $uri);

      /* @var ImageStyle $portrait_style */
      $portrait_style = ImageStyle::load($this->configuration['portrait']);
      $portrait_style->transformDimensions($portrait_dimensions, $uri);

      if ($landscape_dimensions == $portrait_dimensions) {
        $dimensions = $landscape_dimensions;
      }
      else {
        $dimensions['width'] = $dimensions['height'] = NULL;
      }
    }
    else {
      $ratio_adjustment = isset($this->configuration['ratio_adjustment']) ? floatval($this->configuration['ratio_adjustment']) : 1;
      $aspect = $dimensions['width'] / $dimensions['height'];
      $style_name = (($aspect * $ratio_adjustment) > 1) ? $this->configuration['landscape'] : $this->configuration['portrait'];

      /* @var ImageStyle $style */
      $style = ImageStyle::load($style_name);
      $style->transformDimensions($dimensions, $uri);
    }
  }

  /**
   * {@inheritdoc}
   */
  public function getSummary() {
    $summary = [
      '#theme' => 'image_resize_summary',
      '#data' => $this->configuration,
    ];
    $summary += parent::getSummary();

    return $summary;
  }

  /**
   * {@inheritdoc}
   */
  public function defaultConfiguration() {
    return [
      'landscape' => NULL,
      'portrait' => NULL,
      'ratio_adjustment' => 1,
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
    $image_styles = image_style_options(FALSE);
    $form['landscape'] = [
      '#type' => 'select',
      '#title' => t('Landscape image style'),
      '#options' => $image_styles,
      '#default_value' => $this->configuration['landscape'],
      '#description' => t('Select the image style for landscape images'),
      '#required' => TRUE,
    ];
    $form['portrait'] = [
      '#type' => 'select',
      '#title' => t('Portrait'),
      '#options' => $image_styles,
      '#default_value' => $this->configuration['portrait'],
      '#description' => t('Select the image style for portrait images'),
      '#required' => TRUE,
    ];
    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
    parent::submitConfigurationForm($form, $form_state);

    $this->configuration['landscape'] = $form_state->getValue('landscape');
    $this->configuration['portrait'] = $form_state->getValue('portrait');
    $this->configuration['ratio_adjustment'] = 1;
  }

}


<?php

namespace Drupal\thunder_article\Form;

use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\node\Form\NodeRevisionRevertForm;
use Drupal\node\NodeInterface;

/**
 * Provides a form for reverting a node revision.
 *
 * @internal
 */
class NodeRevisionRevertDefaultForm extends NodeRevisionRevertForm {

  /**
   * {@inheritdoc}
   */
  public function getFormId() {
    return 'node_revision_revert_default_confirm';
  }

  /**
   * {@inheritdoc}
   */
  public function getQuestion() {
    return t('Are you sure you want to revert to the default revision from %revision-date?', ['%revision-date' => $this->dateFormatter->format($this->revision->getRevisionCreationTime())]);
  }

  /**
   * {@inheritdoc}
   */
  public function getCancelUrl() {
    return new Url('entity.node.edit_form', ['node' => $this->revision->id()]);
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state, $node = NULL) {

    $this->revision = $this->nodeStorage->load($node);
    $form = ConfirmFormBase::buildForm($form, $form_state);

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    // The revision timestamp will be updated when the revision is saved. Keep
    // the original one for the confirmation message.
    $original_revision_timestamp = $this->revision->getRevisionCreationTime();

    $this->revision = $this->prepareRevertedRevision($this->revision, $form_state);
    $this->revision->revision_log = t('Copy of the revision from %date.', ['%date' => $this->dateFormatter->format($original_revision_timestamp)]);
    $this->revision->setRevisionUserId($this->currentUser()->id());
    $this->revision->setRevisionCreationTime($this->time->getRequestTime());
    $this->revision->setChangedTime($this->time->getRequestTime());
    $this->revision->save();

    $this->logger('content')
      ->notice('@type: reverted %title revision %revision.', [
        '@type' => $this->revision->bundle(),
        '%title' => $this->revision->label(),
        '%revision' => $this->revision->getRevisionId(),
      ]);
    $this->messenger()
      ->addStatus($this->t('@type %title has been reverted to the revision from %revision-date.', [
        '@type' => node_get_type_label($this->revision),
        '%title' => $this->revision->label(),
        '%revision-date' => $this->dateFormatter->format($original_revision_timestamp),
      ]));
    $form_state->setRedirectUrl($this->getCancelUrl());
  }

  /**
   * {@inheritdoc}
   */
  protected function prepareRevertedRevision(NodeInterface $revision, FormStateInterface $form_state) {
    $revision->setNewRevision();
    $revision->isDefaultRevision(TRUE);
    $revision->setRevisionTranslationAffected(TRUE);
    return $revision;
  }

}


<?php

namespace Drupal\thunder\Plugin\Field\FieldWidget;

use Drupal\content_moderation\Plugin\Field\FieldWidget\ModerationStateWidget as CoreModerationStateWidget;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;

/**
 * Plugin implementation of the 'thunder_moderation_state_default' widget.
 *
 * Thunder provides it's own moderation_state widget that only shows a list of
 * possible states to switch in. To display the current state is not part of
 * this widget. Thunder shows that in ThunderNodeForm.
 *
 * @FieldWidget(
 *   id = "thunder_moderation_state_default",
 *   label = @Translation("Moderation state (Thunder)"),
 *   field_types = {
 *     "string"
 *   }
 * )
 */
class ModerationStateWidget extends CoreModerationStateWidget {

  /**
   * {@inheritdoc}
   */
  public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
    $entity = $items->getEntity();

    $workflow = $this->moderationInformation->getWorkflowForEntity($entity);
    $default = $items->get($delta)->value ? $workflow->getTypePlugin()->getState($items->get($delta)->value) : $workflow->getTypePlugin()->getInitialState($entity);

    /** @var \Drupal\workflows\Transition[] $transitions */
    $transitions = $this->validator->getValidTransitions($entity, $this->currentUser);

    $transition_labels = [];
    $default_value = NULL;
    foreach ($transitions as $transition) {
      $transition_to_state = $transition->to();
      $transition_labels[$transition_to_state->id()] = $transition_to_state->label();
      if ($default->id() === $transition_to_state->id()) {
        $default_value = $default->id();
      }
    }

    $element = [
      '#type' => 'select',
      '#key_column' => $this->column,
      '#options' => $transition_labels,
      '#default_value' => $default_value,
      '#access' => !empty($transition_labels),
      '#wrapper_attributes' => [
        'class' => ['container-inline'],
      ],
    ];
    $element['#element_validate'][] = [get_class($this), 'validateElement'];

    return $element;
  }

  /**
   * {@inheritdoc}
   */
  public static function validateElement(array $element, FormStateInterface $form_state) {
    $form_state->setValueForElement($element, [$element['#key_column'] => $element['#value']]);
  }

}


<?php

namespace Drupal\thunder_article\Breadcrumb;

use Drupal\Core\Breadcrumb\Breadcrumb;
use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Link;
use Drupal\Core\ParamConverter\ParamNotConvertedException;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;

/**
 * Class to define the menu_link breadcrumb builder.
 */
class ThunderArticleBreadcrumbBuilder implements BreadcrumbBuilderInterface {
  use StringTranslationTrait;

  /**
   * The router request context.
   *
   * @var \Drupal\Core\Routing\RequestContext
   */
  protected $context;

  /**
   * The menu link access service.
   *
   * @var \Drupal\Core\Access\AccessManagerInterface
   */
  protected $accessManager;

  /**
   * The dynamic router service.
   *
   * @var \Symfony\Component\Routing\Matcher\RequestMatcherInterface
   */
  protected $router;

  /**
   * The dynamic router service.
   *
   * @var \Drupal\Core\PathProcessor\InboundPathProcessorInterface
   */
  protected $pathProcessor;

  /**
   * Site configFactory object.
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
  protected $configFactory;

  /**
   * The title resolver.
   *
   * @var \Drupal\Core\Controller\TitleResolverInterface
   */
  protected $titleResolver;

  /**
   * The current user object.
   *
   * @var \Drupal\Core\Session\AccountInterface
   */
  protected $currentUser;

  /**
   * The entity repository service.
   *
   * @var \Drupal\Core\Entity\EntityRepositoryInterface
   */
  protected $entityRepository;

  /**
   * The taxonomy storage.
   *
   * @var \Drupal\Taxonomy\TermStorageInterface
   */
  protected $termStorage;

  /**
   * Constructs the ThunderArticleBreadcrumbBuilder.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   The entity type manager service.
   * @param \Drupal\Core\Entity\EntityRepositoryInterface $entityRepository
   *   The entity repository service.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
   *   The config factory.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  public function __construct(EntityTypeManagerInterface $entityTypeManager, EntityRepositoryInterface $entityRepository, ConfigFactoryInterface $configFactory) {
    $this->entityRepository = $entityRepository;
    $this->termStorage = $entityTypeManager->getStorage('taxonomy_term');
    $this->configFactory = $configFactory;
  }

  /**
   * {@inheritdoc}
   */
  public function applies(RouteMatchInterface $route_match) {
    // This breadcrumb apply only for all articles.
    $parameters = $route_match->getParameters()->all();
    if (isset($parameters['node']) && is_object($parameters['node'])) {
      return $parameters['node']->getType() == 'article';
    }
    return FALSE;
  }

  /**
   * {@inheritdoc}
   */
  public function build(RouteMatchInterface $route_match) {
    $breadcrumb = new Breadcrumb();
    $breadcrumb->addCacheContexts(['route']);

    // Add all parent forums to breadcrumbs.
    /** @var Node $node */
    $node = $route_match->getParameter('node');
    $breadcrumb->addCacheableDependency($node);

    // Add all parent forums to breadcrumbs.
    /** @var \Drupal\Taxonomy\TermInterface $term */
    $term = !(empty($node->field_channel)) ? $node->field_channel->entity : '';

    $links = [];
    if ($term) {
      $breadcrumb->addCacheableDependency($term);

      $channels = $this->termStorage->loadAllParents($term->id());
      foreach (array_reverse($channels) as $term) {
        $term = $this->entityRepository->getTranslationFromContext($term);
        $breadcrumb->addCacheableDependency($term);
        $links[] = Link::createFromRoute($term->getName(), 'entity.taxonomy_term.canonical', ['taxonomy_term' => $term->id()]);
      }
    }
    if (!$links || '/' . $links[0]->getUrl()->getInternalPath() != $this->configFactory->get('system.site')->get('page.front')) {
      array_unshift($links, Link::createFromRoute($this->t('Home'), '<front>'));
    }

    return $breadcrumb->setLinks($links);
  }

  /**
   * Matches a path in the router.
   *
   * @param string $path
   *   The request path with a leading slash.
   * @param array $exclude
   *   An array of paths or system paths to skip.
   *
   * @return \Symfony\Component\HttpFoundation\Request
   *   A populated request object or NULL if the path couldn't be matched.
   */
  protected function getRequestForPath($path, array $exclude) {
    if (!empty($exclude[$path])) {
      return NULL;
    }
    // @todo Use the RequestHelper once https://www.drupal.org/node/2090293 is
    //   fixed.
    $request = Request::create($path);
    // Performance optimization: set a short accept header to reduce overhead in
    // AcceptHeaderMatcher when matching the request.
    $request->headers->set('Accept', 'text/html');
    // Find the system path by resolving aliases, language prefix, etc.
    $processed = $this->pathProcessor->processInbound($path, $request);
    if (empty($processed) || !empty($exclude[$processed])) {
      // This resolves to the front page, which we already add.
      return NULL;
    }
    $this->currentPath->setPath($processed, $request);
    // Attempt to match this path to provide a fully built request.
    try {
      $request->attributes->add($this->router->matchRequest($request));
      return $request;
    }
    catch (ParamNotConvertedException $e) {
      return NULL;
    }
    catch (ResourceNotFoundException $e) {
      return NULL;
    }
    catch (MethodNotAllowedException $e) {
      return NULL;
    }
    catch (AccessDeniedHttpException $e) {
      return NULL;
    }
  }

}


<?php

namespace Drupal\thunder_article\Twig;

/**
 * Introduce some twig filters.
 */
class FilterExtension extends \Twig_Extension {

  /**
   * Returns introduced filters.
   *
   * @return array
   *   Declared Twig filters
   */
  public function getFilters() {
    return [
      new \Twig_SimpleFilter('plain_text', [$this, 'plainText']),
      new \Twig_SimpleFilter('basic_format', [$this, 'basicFormat'], ['is_safe' => ['html']]),
    ];
  }

  /**
   * Returns the name of the extension.
   *
   * @return string
   *   The extension name
   */
  public function getName() {
    return 'thunder_article_filter_extension';
  }

  /**
   * Plains a text. Strips everything evil out.
   *
   * @param string $value
   *   The content to be processed.
   *
   * @return string
   *   The processed content.
   */
  public static function plainText($value) {
    $element = render($value);
    $element = strip_tags($element);
    $element = html_entity_decode($element, ENT_QUOTES);
    return $element;
  }

  /**
   * Cleans a text and just allow a few tags.
   *
   * @param string $value
   *   The content to be processed.
   *
   * @return string
   *   The processed content.
   */
  public static function basicFormat($value) {
    $element = render($value);
    $element = strip_tags($element, '');
    return $element;
  }

}


<?php

namespace Drupal\thunder_demo\EventSubscriber;

use Drupal\default_content\Event\DefaultContentEvents;
use Drupal\default_content\Event\ImportEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
 * Class ImportSubscriber.
 */
class ImportSubscriber implements EventSubscriberInterface {

  /**
   * Publish imported articles.
   *
   * @param \Drupal\default_content\Event\ImportEvent $event
   *   The event entity.
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   */
  public function onImport(ImportEvent $event) {
    $uuids = [
      '0bd5c257-2231-450f-b4c2-ab156af7b78d',
      '36b2e2b2-3df0-43eb-a282-d792b0999c07',
      '94ad928b-3ec8-4bcb-b617-ab1607bf69cb',
      'bbb1ee17-15f8-46bd-9df5-21c58040d741',
    ];

    foreach ($event->getImportedEntities() as $entity) {
      if (in_array($entity->uuid(), $uuids)) {
        $entity->moderation_state->value = 'published';
        $entity->save();
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents() {
    $events[DefaultContentEvents::IMPORT][] = ['onImport'];
    return $events;
  }

}


<?php

namespace Drupal\thunder_media\EventSubscriber;

use Drupal\Core\Config\ConfigCrudEvent;
use Drupal\Core\Config\ConfigEvents;
use Drupal\views\Entity\View;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
 * Class ConfigEventsSubscriber.
 *
 * @package Drupal\thunder_media\EventSubscriber
 */
class ConfigEventsSubscriber implements EventSubscriberInterface {

  /**
   * {@inheritdoc}
   *
   * @return array
   *   The event names to listen for, and the methods that should be executed.
   */
  public static function getSubscribedEvents() {
    return [
      ConfigEvents::DELETE => 'configDelete',
    ];
  }

  /**
   * React to a config object being deleted.
   *
   * @param \Drupal\Core\Config\ConfigCrudEvent $event
   *   Config crud event.
   */
  public function configDelete(ConfigCrudEvent $event) {
    $config = $event->getConfig();
    if ($config->getName() === 'views.view.thunder_media' && ($media_view = View::load('media'))) {
      $media_view->setStatus(TRUE)->save();
    }
  }

}


<?php

namespace Drupal\thunder_taxonomy;

use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Provides dynamic permissions of the taxonomy module.
 *
 * @see thunder_taxonomy_access.permissions.yml
 */
class ThunderTaxonomyPermissions implements ContainerInjectionInterface {

  use StringTranslationTrait;

  /**
   * The entity manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * Constructs a TaxonomyPermissions instance.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   The entity type manager.
   */
  public function __construct(EntityTypeManagerInterface $entityTypeManager) {
    $this->entityTypeManager = $entityTypeManager;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static($container->get('entity_type.manager'));
  }

  /**
   * Get taxonomy permissions.
   *
   * @return array
   *   Permissions array.
   */
  public function permissions() {
    $permissions = [];
    foreach ($this->entityTypeManager->getStorage('taxonomy_vocabulary')->loadMultiple() as $vocabulary) {
      $permissions += [
        'view published terms in ' . $vocabulary->id() => [
          'title' => $this->t('View published terms in %vocabulary', ['%vocabulary' => $vocabulary->label()]),
        ],
        'view unpublished terms in ' . $vocabulary->id() => [
          'title' => $this->t('View unpublished terms in %vocabulary', ['%vocabulary' => $vocabulary->label()]),
        ],
      ];
    }
    return $permissions;
  }

}


<?php

namespace Drupal\thunder\Plugin\Thunder\OptionalModule;

use Drupal\Core\Form\FormStateInterface;

/**
 * Adsense.
 *
 * @ThunderOptionalModule(
 *   id = "adsense",
 *   label = @Translation("AdSense"),
 *   description = @Translation("With Google AdSense, you can earn money from your online content."),
 *   type = "module",
 * )
 */
class Adsense extends AbstractOptionalModule {

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {

    $form = parent::buildForm($form, $form_state);

    $form['adsense']['adsense_basic_id'] = [
      '#type' => 'textfield',
      '#title' => t('Site Google AdSense Publisher ID'),
      '#required' => FALSE,
      '#default_value' => '',
      '#pattern' => 'pub-[0-9]+',
      '#description' => t('This is the Google AdSense Publisher ID for the site owner. It is used if no other ID is suitable. Get this in your Google Adsense account. It should be similar to %id.', [
        '%id' => 'pub-9999999999999',
      ]
      ),
    ];

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array $formValues) {

    $this->configFactory->getEditable('adsense.settings')
      ->set('adsense_basic_id', (string) $formValues['adsense_basic_id'])
      ->save(TRUE);
  }

}


<?php

namespace Drupal\thunder_taxonomy;

use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\taxonomy\TermAccessControlHandler;

/**
 * Defines the access control handler for the taxonomy term entity type.
 *
 * @see \Drupal\taxonomy\Entity\Term
 */
class ThunderTermAccessControlHandler extends TermAccessControlHandler {

  /**
   * {@inheritdoc}
   */
  protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {

    switch ($operation) {
      case 'view':
        // Check for status and set 'published' or 'unpublished'.
        $status = ($entity->status->value) ? 'published' : 'unpublished';
        return AccessResult::allowedIf($account->hasPermission('access content') && $account->hasPermission('view ' . $status . ' terms in ' . $entity->bundle()));

      default:
        return parent::checkAccess($entity, $operation, $account);

    }
  }

}


<?php

namespace Drupal\thunder\Plugin\Thunder\OptionalModule;

use Drupal\Core\Form\FormStateInterface;

/**
 * AMP.
 *
 * @ThunderOptionalModule(
 *   id = "thunder_liveblog",
 *   label = @Translation("Liveblog"),
 *   description = @Translation("The Liveblog module allows you to distribute blog posts to thousands of users in realtime."),
 *   type = "module",
 * )
 */
class Liveblog extends AbstractOptionalModule {

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {

    $form = parent::buildForm($form, $form_state);

    $form['thunder_liveblog']['description'] = [
      '#type' => 'item',
      '#markup' => $this->t('Register a new account at :pusher_url, create a new app and note down your keys and cluster. You can provide them right here or at a later stage on the liveblog settings form.',
        [':pusher_url' => 'http://pusher.com']),
    ];

    $form['thunder_liveblog']['pusher_app_id'] = [
      '#type' => 'textfield',
      '#title' => t('App ID'),
    ];

    $form['thunder_liveblog']['pusher_key'] = [
      '#type' => 'textfield',
      '#title' => t('Key'),
    ];

    $form['thunder_liveblog']['pusher_secret'] = [
      '#type' => 'textfield',
      '#title' => t('Secret'),
    ];

    $form['thunder_liveblog']['pusher_cluster'] = [
      '#type' => 'textfield',
      '#title' => t('Cluster'),
      '#description' => t('The cluster name to connect to. Leave empty for the default cluster: mt1 (US east coast)'),
    ];

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array $formValues) {
    $this->configFactory->getEditable('liveblog.notification_channel.liveblog_pusher')
      ->set('app_id', $formValues['pusher_app_id'])
      ->set('key', $formValues['pusher_key'])
      ->set('secret', $formValues['pusher_secret'])
      ->set('cluster', $formValues['pusher_cluster'])
      ->save(TRUE);

    if ($formValues['pusher_app_id'] && $formValues['pusher_key'] && $formValues['pusher_secret']) {
      $this->configFactory->getEditable('liveblog.settings')
        ->set('notification_channel', 'liveblog_pusher')
        ->save(TRUE);
    }
  }

}


<?php

namespace Drupal\thunder_article\Form;

use Drupal\content_moderation\ModerationInformationInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Drupal\node\Access\NodeRevisionAccessCheck;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RequestStack;

/**
 * Base for handler for node add/edit forms.
 */
class ThunderNodeForm implements ContainerInjectionInterface {

  use StringTranslationTrait;

  /**
   * The current user.
   *
   * @var \Drupal\Core\Session\AccountInterface
   */
  protected $currentUser;

  /**
   * The messenger service.
   *
   * @var \Drupal\Core\Messenger\MessengerInterface
   */
  protected $messenger;

  /**
   * The request object.
   *
   * @var \Symfony\Component\HttpFoundation\Request
   */
  protected $request;

  /**
   * The node revision access check service.
   *
   * @var \Drupal\node\Access\NodeRevisionAccessCheck
   */
  protected $nodeRevisionAccess;

  /**
   * The moderation information service.
   *
   * @var \Drupal\content_moderation\ModerationInformationInterface
   */
  protected $moderationInfo;

  /**
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * Constructs a NodeForm object.
   *
   * @param \Drupal\Core\Session\AccountInterface $current_user
   *   The current user.
   * @param \Drupal\Core\Messenger\MessengerInterface $messenger
   *   The messenger service.
   * @param \Symfony\Component\HttpFoundation\RequestStack $requestStack
   *   The request stack service.
   * @param \Drupal\node\Access\NodeRevisionAccessCheck $node_revision_access
   *   The node revision access check service.
   * @param \Drupal\content_moderation\ModerationInformationInterface $moderationInfo
   *   The moderation info service.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
   */
  public function __construct(AccountInterface $current_user, MessengerInterface $messenger, RequestStack $requestStack, NodeRevisionAccessCheck $node_revision_access, ModerationInformationInterface $moderationInfo, EntityTypeManagerInterface $entity_type_manager) {
    $this->currentUser = $current_user;
    $this->messenger = $messenger;
    $this->request = $requestStack->getCurrentRequest();
    $this->nodeRevisionAccess = $node_revision_access;
    $this->moderationInfo = $moderationInfo;
    $this->entityTypeManager = $entity_type_manager;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('current_user'),
      $container->get('messenger'),
      $container->get('request_stack'),
      $container->get('access_check.node.revision'),
      $container->has('content_moderation.moderation_information') ? $container->get('content_moderation.moderation_information') : NULL,
      $container->get('entity_type.manager')
    );
  }

  /**
   * {@inheritdoc}
   */
  public function formAlter(array &$form, FormStateInterface $form_state) {
    /** @var \Drupal\Core\Entity\ContentEntityFormInterface $form_object */
    $form_object = $form_state->getFormObject();
    /** @var \Drupal\node\NodeInterface $entity */
    $entity = $form_object->getEntity();

    $storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId());
    $latest_revision_id = $storage->getLatestTranslationAffectedRevisionId($entity->id(), $entity->language()->getId());
    if ($latest_revision_id !== NULL && $this->moderationInfo && $this->moderationInfo->hasPendingRevision($entity)) {
      $this->messenger->addWarning($this->t('This %entity_type has unpublished changes from user %user.', ['%entity_type' => $entity->get('type')->entity->label(), '%user' => $entity->getRevisionUser()->label()]));
    }

    $form['actions'] = array_merge($form['actions'], $this->actions($entity));

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  protected function actions($entity) {
    $storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId());
    $latest_revision_id = $storage->getLatestTranslationAffectedRevisionId($entity->id(), $entity->language()->getId());

    if ($latest_revision_id == NULL || !$this->moderationInfo || !$this->moderationInfo->isModeratedEntity($entity)) {
      return [];
    }

    $state = $this->moderationInfo->getWorkflowForEntity($entity)->getTypePlugin()->getState($entity->moderation_state->value);
    $element['status'] = [
      '#type' => 'item',
      '#markup' => $entity->isNew() || !$this->moderationInfo->isDefaultRevisionPublished($entity) ? $this->t('of unpublished @entity_type', ['@entity_type' => strtolower($entity->type->entity->label())]) : $this->t('of published @entity_type', ['@entity_type' => strtolower($entity->type->entity->label())]),
      '#weight' => 200,
      '#wrapper_attributes' => [
        'class' => ['status'],
      ],
      '#access' => !$state->isDefaultRevisionState(),
    ];

    $element['moderation_state_current'] = [
      '#type' => 'item',
      '#markup' => $state->label(),
      '#weight' => 210,
      '#wrapper_attributes' => [
        'class' => ['status', $state->id()],
      ],
    ];

    if ($this->moderationInfo->hasPendingRevision($entity)) {
      $route_info = Url::fromRoute('node.revision_revert_default_confirm', [
        'node' => $entity->id(),
        'node_revision' => $entity->getRevisionId(),
      ]);
      if ($this->request->query->has('destination')) {
        $query = $route_info->getOption('query');
        $query['destination'] = $this->request->query->get('destination');
        $route_info->setOption('query', $query);
      }

      $element['revert_to_default'] = [
        '#type' => 'link',
        '#title' => $this->t('Revert to default revision'),
        '#access' => $this->nodeRevisionAccess->checkAccess($entity, $this->currentUser, 'update'),
        '#weight' => 101,
        '#attributes' => [
          'class' => ['button', 'button--danger'],
        ],
      ];
      $element['revert_to_default']['#url'] = $route_info;
    }

    return $element;
  }

}


<?php

namespace Drupal\thunder_article\Plugin\Derivative;

use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\Routing\RouteProviderInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Generates content view local tasks.
 */
class DynamicLocalTasks extends DeriverBase implements ContainerDeriverInterface {

  use StringTranslationTrait;

  /**
   * The module handler service.
   *
   * @var \Drupal\Core\Extension\ModuleHandlerInterface
   */
  protected $moduleHandler;

  /**
   * The route provider service.
   *
   * @var \Drupal\Core\Routing\RouteProviderInterface
   */
  protected $routeProvider;

  /**
   * The config factory service.
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
  protected $configFactory;

  /**
   * Creates an DynamicLocalTasks object.
   *
   * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
   *   The translation manager.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   *   The module handler service.
   * @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
   *   The route provider service.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   The config factory service.
   */
  public function __construct(TranslationInterface $string_translation, ModuleHandlerInterface $module_handler, RouteProviderInterface $route_provider, ConfigFactoryInterface $config_factory) {
    $this->stringTranslation = $string_translation;
    $this->moduleHandler = $module_handler;
    $this->routeProvider = $route_provider;
    $this->configFactory = $config_factory;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, $base_plugin_id) {
    return new static(
      $container->get('string_translation'),
      $container->get('module_handler'),
      $container->get('router.route_provider'),
      $container->get('config.factory')
    );
  }

  /**
   * {@inheritdoc}
   */
  public function getDerivativeDefinitions($base_plugin_definition) {
    if ($this->moduleHandler->moduleExists('content_lock') && $this->routeProvider->getRoutesByNames(['view.locked_content.page_1'])) {
      $this->derivatives["thunder_article.content_lock"] = [
        'route_name' => "view.locked_content.page_1",
        'title' => $this->t('Locked content'),
        'parent_id' => "system.admin_content",
        'weight' => 2,
      ] + $base_plugin_definition;
    }

    if ($this->moduleHandler->moduleExists('scheduler') && $this->routeProvider->getRoutesByNames(['view.scheduler_scheduled_content.overview'])) {
      // See thunder_article_menu_local_tasks_alter() for how this is displayed
      // or not depending on configuration.
      $this->derivatives["thunder_article.scheduler"] = [
        'route_name' => "view.scheduler_scheduled_content.overview",
        'title' => $this->t('Scheduled content'),
        'parent_id' => "system.admin_content",
        'weight' => 3,
        'cache_tags' => $this->configFactory->get('thunder_article.settings')->getCacheTags(),
      ] + $base_plugin_definition;
    }

    if ($this->moduleHandler->moduleExists('access_unpublished') && $this->routeProvider->getRoutesByNames(['access_unpublished.access_token.list'])) {
      $this->derivatives["thunder_article.access_unpublished"] = [
        'route_name' => "access_unpublished.access_token.list",
        'title' => $this->t('Unpublished access'),
        'parent_id' => "system.admin_content",
        'weight' => 4,
      ] + $base_plugin_definition;
    }

    return $this->derivatives;
  }

}


<?php

namespace Drupal\thunder_taxonomy;

use Drupal\Core\Form\FormStateInterface;
use Drupal\taxonomy\TermForm;

/**
 * Base for handler for taxonomy term edit forms.
 */
class ThunderTermForm extends TermForm {

  /**
   * {@inheritdoc}
   */
  public function form(array $form, FormStateInterface $form_state) {
    $form = parent::form($form, $form_state);

    // Hide status checkbox. We have the button.
    $form['status']['#group'] = 'footer';

    // Create sidebar group.
    $form['advanced'] = [
      '#type' => 'container',
      '#attributes' => ['class' => ['entity-meta']],
      '#weight' => 99,
    ];

    // Use the same form like node edit.
    $form['#theme'] = ['node_edit_form'];
    $form['#attached']['library'][] = 'seven/node-form';

    // Move relations into sidebar.
    $form['relations']['#group'] = 'advanced';

    // Move pathauto into sidebar.
    $term = $form_state->getFormObject()->getEntity();
    $form['path_settings'] = [
      '#type' => 'details',
      '#title' => t('URL path settings'),
      '#open' => !empty($form['path']['widget'][0]['alias']['#value']),
      '#group' => 'advanced',
      '#access' => !empty($form['path']['#access']) && $term->hasField('path') && $term->get('path')->access('edit'),
      '#attributes' => [
        'class' => ['path-form'],
      ],
      '#attached' => [
        'library' => ['path/drupal.path'],
      ],
      '#weight' => 30,
    ];
    $form['path']['#group'] = 'path_settings';

    return $form;
  }

}


<?php

namespace Drupal\thunder\Installer\Form;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\thunder\OptionalModulesManager;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Provides the site configuration form.
 */
class ModuleConfigureForm extends ConfigFormBase {

  /**
   * The plugin manager.
   *
   * @var \Drupal\thunder\OptionalModulesManager
   */
  protected $optionalModulesManager;

  /**
   * Constructs a \Drupal\system\ConfigFormBase object.
   *
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   The factory for configuration objects.
   * @param \Drupal\thunder\OptionalModulesManager $optionalModulesManager
   *   The factory for configuration objects.
   */
  public function __construct(ConfigFactoryInterface $config_factory, OptionalModulesManager $optionalModulesManager) {

    parent::__construct($config_factory);

    $this->optionalModulesManager = $optionalModulesManager;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('config.factory'),
      $container->get('plugin.manager.thunder.optional_modules')
    );
  }

  /**
   * {@inheritdoc}
   */
  public function getFormId() {
    return 'thunder_module_configure_form';
  }

  /**
   * {@inheritdoc}
   */
  protected function getEditableConfigNames() {

    return [];
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {

    // We have to delete all messages, because simple_sitemap adds a bunch of
    // messages during the install process.
    // @see https://www.drupal.org/project/simple_sitemap/issues/3001388.
    $this->messenger()->deleteAll();

    $form['description'] = [
      '#type' => 'item',
      '#markup' => $this->t('Keep calm. You can install all the modules later, too.'),
    ];

    $form['install_modules'] = [
      '#type' => 'container',
    ];

    $providers = $this->optionalModulesManager->getDefinitions();

    static::sortByWeights($providers);

    foreach ($providers as $provider) {

      $instance = $this->optionalModulesManager->createInstance($provider['id']);

      $form['install_modules_' . $provider['id']] = [
        '#type' => 'checkbox',
        '#title' => $provider['label'],
        '#description' => isset($provider['description']) ? $provider['description'] : '',
        '#default_value' => isset($provider['standardlyEnabled']) ? $provider['standardlyEnabled'] : 0,
      ];

      $form = $instance->buildForm($form, $form_state);

    }
    $form['#title'] = $this->t('Install & configure modules');

    $form['actions'] = ['#type' => 'actions'];
    $form['actions']['save'] = [
      '#type' => 'submit',
      '#value' => $this->t('Save and continue'),
      '#button_type' => 'primary',
      '#submit' => ['::submitForm'],
    ];

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    $installModules = [];

    foreach ($form_state->getValues() as $key => $value) {

      if (strpos($key, 'install_modules') !== FALSE && $value) {
        preg_match('/install_modules_(?P<name>\w+)/', $key, $values);
        $installModules[] = $values['name'];
      }
    }

    $buildInfo = $form_state->getBuildInfo();

    $install_state = $buildInfo['args'];

    $install_state[0]['thunder_additional_modules'] = $installModules;
    $install_state[0]['form_state_values'] = $form_state->getValues();

    $buildInfo['args'] = $install_state;

    $form_state->setBuildInfo($buildInfo);

  }

  /**
   * Returns a sorting function to sort an array by weights.
   *
   * If an array element doesn't provide a weight, it will be set to 0.
   * If two elements have the same weight, they are sorted by label.
   *
   * @param array $array
   *   The array to be sorted.
   */
  private static function sortByWeights(array &$array) {
    uasort($array, function ($a, $b) {
      $a_weight = isset($a['weight']) ? $a['weight'] : 0;
      $b_weight = isset($b['weight']) ? $b['weight'] : 0;

      if ($a_weight == $b_weight) {
        return ($a['label'] > $b['label']) ? 1 : -1;
      }
      return ($a_weight > $b_weight) ? 1 : -1;
    });
  }

}


<?php
namespace Stiphle\Storage;

/**
 * Redis storage via Predis package
 *
 * @author Jacob Christiansen <jacob@colourbox.com>
 */
class Redis implements StorageInterface
{
    protected $lockWaitTimeout = 1000;
    protected $redisClient;

    public function __construct(\Predis\Client $redisClient)
    {
        $this->redisClient = $redisClient;
    }

    /**
     * {@inheritDoc}
     */
    public function setLockWaitTimeout($milliseconds)
    {
        $this->lockWaitTimeout = $milliseconds;
    }

    /**
     * {@inheritDoc}
     */
    public function lock($key)
    {
        $start = microtime(true);

        while (is_null($this->redisClient->set($this->getLockKey($key), 'LOCKED', 'PX', 3600, 'NX'))) {
            $passed = (microtime(true) - $start) * 1000;
            if ($passed > $this->lockWaitTimeout) {
                throw new LockWaitTimeoutException();
            }
            usleep(100);
        }
    }

    /**
     * {@inheritDoc}
     */
    public function unlock($key)
    {
        $this->redisClient->del($this->getLockKey($key));
    }

    /**
     * {@inheritDoc}
     */
    public function get($key)
    {
        return $this->redisClient->get($key);
    }

    /**
     * {@inheritDoc}
     */
    public function set($key, $value)
    {
        $this->redisClient->set($key, $value);
    }

    private function getLockKey($key)
    {
        return $key . "::LOCK";
    }
}


<?php
/**
 * @package    Stiphle
 * @subpackage Stiphle\Storage
 */
namespace Stiphle\Storage;

/**
 * This file is part of Stiphle
 *
 * Copyright (c) 2011 Dave Marshall <dave.marshall@atstsolutuions.co.uk>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

/**
 * Basic in-process storage of values
 *
 * @author      Dave Marshall <david.marshall@atstsolutions.co.uk>
 */
class Process implements StorageInterface
{
    /**
     * @var int
     */
    protected $lockWaitTimeout = 1000;

    /**
     * @var array
     */
    protected $locked = array();

    /**
     * @var array
     */
    protected $values = array();

    /**
     * Set lock wait timeout
     *
     * @param int $milliseconds
     */
    public function setLockWaitTimeout($milliseconds)
    {
        $this->lockWaitTimeout = $milliseconds;
    }

    /**
     * Lock
     *
     * If we're using storage, we might have multiple requests coming in at
     * once, so we lock the storage
     *
     * @return void
     */
    public function lock($key)
    {
        if (!isset($this->locked[$key])) {
            $this->locked[$key] = false;
        }

        $start = microtime(true);
        while($this->locked[$key]) {
            $passed = (microtime(true) - $start) * 1000;
            if ($passed > $this->lockWaitTimeout) {
                throw new LockWaitTimeoutException();
            }
        }

        $this->locked[$key] = true;

        return;
    }

    /**
     * Unlock
     *
     * @return void
     */
    public function unlock($key)
    {
        $this->locked[$key] = false;
    }

    /**
     * Get
     *
     * @param string $key
     * @return int
     */
    public function get($key)
    {
        if (isset($this->values[$key])) {
            return $this->values[$key];
        }

        return null;
    }

    /**
     * set
     *
     * @param string $key
     * @param mixed $value
     * @return void
     */
    public function set($key, $value)
    {
        $this->values[$key] = $value;
    }
}


<?php
/**
 * @package    Stiphle
 * @subpackage Stiphle\Throttle
 */
namespace Stiphle\Throttle;

use Stiphle\Throttle\ThrottleInterface;
use Stiphle\Storage\StorageInterface;
use Stiphle\Storage\Process;

/**
 * This file is part of Stiphle
 *
 * Copyright (c) 2011 Dave Marshall <dave.marshall@atstsolutuions.co.uk>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

/**
 * A 'leaky bucket' style rate limiter
 *
 * @see http://stackoverflow.com/questions/1375501/how-do-i-throttle-my-sites-api-users
 * @author      Dave Marshall <david.marshall@atstsolutions.co.uk>
 */
class LeakyBucket implements ThrottleInterface
{
    /**
     * @var StorageInterface
     */
    protected $storage;

    /**
     *
     */
    public function __construct()
    {
        $this->storage = new Process();
    }

    /**
     * Throttle
     *
     * @param string $key  - A unique key for what we're throttling
     * @param int $limit   - How many are allowed
     * @param int $milliseconds - In this many milliseconds
     * @return int
     */
    public function throttle($key, $limit, $milliseconds)
    {
        /**
         * Try and do our waiting without a lock
         */
        $key = $this->getStorageKey($key, $limit, $milliseconds);
        $wait     = 0;
        $newRatio = $this->getNewRatio($key, $limit, $milliseconds);

        if ($newRatio > $milliseconds) {
            $wait = ceil($newRatio - $milliseconds);
        }
        usleep($wait * 1000);

        /**
         * Lock, record and release
         */
        $this->storage->lock($key);
        $newRatio = $this->getNewRatio($key, $limit, $milliseconds);
        $this->setLastRatio($key, $newRatio);
        $this->setLastRequest($key, microtime(1));
        $this->storage->unlock($key);
        return $wait;
    }

    /**
     * Get Estimate (doesn't require lock)
     *
     * How long would I have to wait to make a request?
     *
     * @param string $key  - A unique key for what we're throttling
     * @param int $limit   - How many are allowed
     * @param int $milliseconds - In this many milliseconds
     * @return int - the number of milliseconds before this request should be allowed
     * to pass
     */
    public function getEstimate($key, $limit, $milliseconds)
    {
        $key = $this->getStorageKey($key, $limit, $milliseconds);
        $newRatio = $this->getNewRatio($key, $limit, $milliseconds);
        $wait     = 0;
        if ($newRatio > $milliseconds) {
            $wait = ceil($newRatio - $milliseconds);
        }
        return $wait;
    }

    /**
     * Get new ratio
     *
     * Assuming we're making a request, get the ratio of requests made to
     * requests allowed
     *
     * @param string $key  - A unique key for what we're throttling
     * @param int $limit   - How many are allowed
     * @param int $milliseconds - In this many milliseconds
     * @return float
     */
    protected function getNewRatio($key, $limit, $milliseconds)
    {
        $lastRequest = $this->getLastRequest($key) ?: 0;
        $lastRatio   = $this->getLastRatio($key) ?: 0;

        $diff = (microtime(1) - $lastRequest) * 1000;

        $newRatio = $lastRatio - $diff;
        $newRatio = $newRatio < 0 ? 0 : $newRatio;
        $newRatio+= $milliseconds/$limit;

        return $newRatio;
    }

    /**
     * Get storage key
     *
     * @param string $key  - A unique key for what we're throttling
     * @param int $limit   - How many are allowed
     * @param int $milliseconds - In this many milliseconds
     * @return string
     */
    protected function getStorageKey($key, $limit, $milliseconds)
    {
        return $key . '::' . $limit . '::' . $milliseconds;
    }

    /**
     * Set Storage
     *
     * @param StorageInterface $storage
     * @return LeakyBucket
     */
    public function setStorage(StorageInterface $storage)
    {
        $this->storage = $storage;
        return $this;
    }

    /**
     * Get Last Ratio
     *
     * @param string $key
     * @return float
     */
    protected function getLastRatio($key)
    {
        return $this->storage->get($key . '::LASTRATIO');
    }

    /**
     * Set Last Ratio
     *
     * @param string $key
     * @param float $ratio
     * @return void
     */
    protected function setLastRatio($key, $ratio)
    {
        return $this->storage->set($key . '::LASTRATIO', $ratio);
    }

    /**
     * Get Last Request
     *
     * @param string $key
     * @return float
     */
    protected function getLastRequest($key)
    {
        return $this->storage->get($key . '::LASTREQUEST');
    }

    /**
     * Set Last Request
     *
     * @param string $key
     * @param float $request
     * @return void
     */
    protected function setLastRequest($key, $request)
    {
        return $this->storage->set($key . '::LASTREQUEST', $request);
    }
}


<?php
/**
 * @package    Stiphle
 * @subpackage Stiphle\Storage
 */
namespace Stiphle\Storage;

/**
 * This file is part of Stiphle
 *
 * Copyright (c) 2011 Dave Marshall <dave.marshall@atstsolutuions.co.uk>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

/**
 * Use memcached via PHP's memcached extension
 *
 * @author      Dave Marshall <david.marshall@atstsolutions.co.uk>
 */
class Memcached implements StorageInterface
{
    /**
     * @var int
     */
    protected $lockWaitTimeout = 1000;

    /**
     * @var int  Time to sleep when attempting to get lock in microseconds
     */
    protected $sleep = 100;

    /**
     * @var int
     */
    protected $ttl = 3600;

    /**
     * Memcached instance
     */
    protected $memcached;

    /**
     * Constructor
     *
     */
    public function __construct(\Memcached $memcached)
    {
        $this->memcached = $memcached;
    }

    /**
     * Set lock wait timeout
     *
     * @param int $milliseconds
     */
    public function setLockWaitTimeout($milliseconds)
    {
        $this->lockWaitTimeout = $milliseconds;
        return;
    }

    /**
     * Set the sleep time in microseconds
     *
     * @param int
     * @return void
     */
    public function setSleep($microseconds)
    {
        $this->sleep = $microseconds;
        return;
    }

    /**
     * Set the ttl for the apc records in seconds
     *
     * @param int $seconds
     * @return void
     */
    public function setTtl($microseconds)
    {
        $this->ttl = $microseconds;
        return;
    }

    /**
     * Lock
     *
     * If we're using storage, we might have multiple requests coming in at
     * once, so we lock the storage
     *
     * @return void
     */
    public function lock($key)
    {
        $key = $key . "::LOCK";
        $start = microtime(true);

        while(!$this->memcached->add($key, true, $this->ttl)) {
            $passed = (microtime(true) - $start) * 1000;
            if ($passed > $this->lockWaitTimeout) {
                throw new LockWaitTimeoutException();
            }
            usleep($this->sleep);
        }

        return;
    }

    /**
     * Unlock
     *
     * @return void
     */
    public function unlock($key)
    {
        $key = $key . "::LOCK";
        $this->memcached->delete($key);
    }

    /**
     * Get last modified
     *
     * @param string $key
     * @return int
     */
    public function get($key)
    {
        return $this->memcached->get($key);
    }

    /**
     * set
     *
     * @param string $key
     * @param mixed $value
     * @return void
     */
    public function set($key, $value)
    {
        $this->memcached->set($key, $value, $this->ttl);
        return;
    }

}


<?php

namespace Stiphle\Throttle;

use Stiphle\Throttle\ThrottleInterface;
use Stiphle\Storage\StorageInterface;
use Stiphle\Storage\Process;

/**
 * This file is part of Stiphle
 *
 * Copyright (c) 2011 Dave Marshall <dave.marshall@atstsolutuions.co.uk>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

/**
 * A throttle based on a fixed time window
 *
 * @author      Dave Marshall <david.marshall@atstsolutions.co.uk>
 */
class TimeWindow implements ThrottleInterface
{
    /**
     * @var StorageInterface
     */
    protected $storage;

    /**
     *
     */
    public function __construct()
    {
        $this->storage = new Process();
    }

    /**
     * Throttle
     *
     * @param string $key  - A unique key for what we're throttling
     * @param int $limit   - How many are allowed
     * @param int $milliseconds - In this many milliseconds
     * @return void
     */
    public function throttle($key, $limit, $milliseconds)
    {
        /**
         * Try do our waiting without a lock, so may sneak through because of
         * this...
         */
        $wait = $this->getEstimate($key, $limit, $milliseconds);
        if ($wait > 0) {
            usleep($wait * 1000);
        }

        $key = $this->getStorageKey($key, $limit, $milliseconds);
        $this->storage->lock($key);
        $count = $this->storage->get($key);
        $count++;
        $this->storage->set($key, $count);
        $this->storage->unlock($key);
        return $wait;
    }

    /**
     * Get Estimate (doesn't require lock)
     *
     * How long would I have to wait to make a request?
     *
     * @param string $key  - A unique key for what we're throttling
     * @param int $limit   - How many are allowed
     * @param int $milliseconds - In this many milliseconds
     * @return int - the number of milliseconds before this request should be allowed
     * to pass
     */
    public function getEstimate($key, $limit, $milliseconds)
    {
        $key = $this->getStorageKey($key, $limit, $milliseconds);
        $count = $this->storage->get($key);
        if ($count < $limit) {
            return 0;
        }

        return $milliseconds - ((microtime(1) * 1000) % (float) $milliseconds);
    }

    /**
     * Get storage key
     *
     * @param string $key  - A unique key for what we're throttling
     * @param int $limit   - How many are allowed
     * @param int $milliseconds - In this many milliseconds
     * @return string
     */
    protected function getStorageKey($key, $limit, $milliseconds)
    {
        $window = $milliseconds * (floor((microtime(1) * 1000)/$milliseconds));
        $date = date('YmdHis', $window/1000);
        return $date . '::' . $key . '::' . $limit . '::' . $milliseconds . '::COUNT';
    }

    /**
     * Set Storage
     *
     * @param StorageInterface $storage
     * @return LeakyBucket
     */
    public function setStorage(StorageInterface $storage)
    {
        $this->storage = $storage;
        return $this;
    }

}


<?php declare(strict_types=1);

namespace GitWrapper;

use ArrayIterator;
use IteratorAggregate;

/**
 * Class that parses and returnes an array of branches.
 */
final class GitBranches implements IteratorAggregate
{
    /**
     * @var GitWorkingCopy
     */
    private $gitWorkingCopy;

    public function __construct(GitWorkingCopy $gitWorkingCopy)
    {
        $this->gitWorkingCopy = clone $gitWorkingCopy;
        $gitWorkingCopy->branch(['a' => true]);
    }

    /**
     * Fetches the branches via the `git branch` command.
     *
     * @param bool $onlyRemote Whether to fetch only remote branches, defaults to false which returns all branches.
     *
     * @return mixed[]
     */
    public function fetchBranches(bool $onlyRemote = false): array
    {
        $options = $onlyRemote ? ['r' => true] : ['a' => true];
        $output = $this->gitWorkingCopy->branch($options);
        $branches = (array) preg_split("/\r\n|\n|\r/", rtrim($output));
        return array_map([$this, 'trimBranch'], $branches);
    }

    public function trimBranch(string $branch): string
    {
        return ltrim($branch, ' *');
    }

    public function getIterator(): ArrayIterator
    {
        $branches = $this->all();
        return new ArrayIterator($branches);
    }

    /**
     * @return string[]
     */
    public function all(): array
    {
        return $this->fetchBranches();
    }

    /**
     * @return string[]
     */
    public function remote(): array
    {
        return $this->fetchBranches(true);
    }

    /**
     * Returns currently active branch (HEAD) of the working copy.
     */
    public function head(): string
    {
        return trim($this->gitWorkingCopy->run('rev-parse', ['--abbrev-ref', 'HEAD']));
    }
}


<?php declare(strict_types=1);

namespace GitWrapper\Event;

use GitWrapper\GitException;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

final class GitLoggerEventSubscriber implements EventSubscriberInterface, LoggerAwareInterface
{
    /**
     * Mapping of event to log level.
     *
     * @var string[]
     */
    private $logLevelMappings = [
        GitEvents::GIT_PREPARE => LogLevel::INFO,
        GitEvents::GIT_OUTPUT => LogLevel::DEBUG,
        GitEvents::GIT_SUCCESS => LogLevel::INFO,
        GitEvents::GIT_ERROR => LogLevel::ERROR,
        GitEvents::GIT_BYPASS => LogLevel::INFO,
    ];

    /**
     * @var LoggerInterface
     */
    private $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->setLogger($logger);
    }

    /**
     * {@inheritDoc}
     */
    public function setLogger(LoggerInterface $logger): void
    {
        $this->logger = $logger;
    }

    public function getLogger(): LoggerInterface
    {
        return $this->logger;
    }

    public function setLogLevelMapping(string $eventName, string $logLevel): void
    {
        $this->logLevelMappings[$eventName] = $logLevel;
    }

    /**
     * Returns the log level mapping for an event.
     */
    public function getLogLevelMapping(string $eventName): string
    {
        if (! isset($this->logLevelMappings[$eventName])) {
            throw new GitException(sprintf('Unknown event "%s"', $eventName));
        }

        return $this->logLevelMappings[$eventName];
    }

    /**
     * {@inheritDoc}
     */
    public static function getSubscribedEvents()
    {
        return [
            GitEvents::GIT_PREPARE => ['onPrepare', 0],
            GitEvents::GIT_OUTPUT => ['handleOutput', 0],
            GitEvents::GIT_SUCCESS => ['onSuccess', 0],
            GitEvents::GIT_ERROR => ['onError', 0],
            GitEvents::GIT_BYPASS => ['onBypass', 0],
        ];
    }

    /**
     * Adds a log message using the level defined in the mappings.
     *
     * @param mixed[] $context
     */
    public function log(GitEvent $gitEvent, string $message, array $context = [], ?string $eventName = null): void
    {
        // Provide backwards compatibility with Symfony 2.
        if ($eventName === null && method_exists($gitEvent, 'getName')) {
            $eventName = $gitEvent->getName();
        }

        $method = $this->getLogLevelMapping($eventName);
        $context += ['command' => $gitEvent->getProcess()->getCommandLine()];
        $this->logger->{$method}($message, $context);
    }

    public function onPrepare(GitEvent $gitEvent, ?string $eventName = null): void
    {
        $this->log($gitEvent, 'Git command preparing to run', [], $eventName);
    }

    public function handleOutput(GitOutputEvent $gitOutputEvent, ?string $eventName = null): void
    {
        $context = ['error' => $gitOutputEvent->isError() ? true : false];
        $this->log($gitOutputEvent, $gitOutputEvent->getBuffer(), $context, $eventName);
    }

    public function onSuccess(GitEvent $gitEvent, ?string $eventName = null): void
    {
        $this->log($gitEvent, 'Git command successfully run', [], $eventName);
    }

    public function onError(GitEvent $gitEvent, ?string $eventName = null): void
    {
        $this->log($gitEvent, 'Error running Git command', [], $eventName);
    }

    public function onBypass(GitEvent $gitEvent, ?string $eventName = null): void
    {
        $this->log($gitEvent, 'Git command bypassed', [], $eventName);
    }
}