elementor / elementor/elementor
Editor preview overwrites the published post-{id}.css with unpublished draft styles (external CSS print method)
- Dominant language
- PHP
- Stars
- 7.1k
- Forks
- 1.6k
- Avg merge
- 1d 5h
- Merged PRs (30d)
- 193
Description
### Description
When `elementor_css_print_method` is `external`, editing a **published** document in the editor **without publishing** overwrites that document's live CSS file with the draft's styles.
`Post_Preview` takes its *element data* from the autosave revision but derives its *file name* from the parent post, so the preview build is written to the published document's stylesheet. The front end then serves published markup styled by unpublished CSS.
I hit this on a production site: a published page rendered its correct published content while the linked stylesheet contained styles from an open, unpublished draft. It persisted for about 5.5 hours, until something triggered a normal rebuild of the published document.
**Cause**
`core/files/css/post-preview.php`:
```php
public function __construct( $post_id ) {
$this->post_id_for_data = $post_id; // autosave -> DRAFT data
$parent_id = wp_get_post_parent_id( $post_id );
parent::__construct( $parent_id ); // parent -> PUBLISHED file name
}
```
`Post_Local_Cache` overrides only `load_meta()` / `update_meta()` / `delete_meta()`, keeping metadata in memory. It does **not** override `update_file()` or `write()`, so `Core\Files\Base::write()` still runs `file_put_contents()` against the published path. `Post_Local_Cache::is_update_required()` also returns a hardcoded `true`, so this regenerates eagerly.
**Secondary effect: the overwrite is invisible to cache-busting**
The `?ver=` arg comes from the parent's `_elementor_css['time']` meta. Because `Post_Local_Cache` keeps meta in memory only, a preview write never updates it — so the **file contents change while the URL stays byte-for-byte identical**. With the long-lived `Cache-Control` typically served for files under `uploads/`, a draft build can get pinned in CDN and browser caches under an unchanged URL, which makes the symptom look intermittent and user-specific.
Observed directly (same URL, no front-end request in between):
```
before preview render: ....elementor-element-bbbb222 .elementor-heading-title{color:#756CDB;}
after preview render: ....elementor-element-bbbb222 .elementor-heading-title{color:#ABCDEF;}
```
`Elementor\Modules\LoopBuilder\Files\CSS\Loop_Preview` (Pro) extends `Post_Preview` and appears to inherit the same behaviour.
### Steps to reproduce
Tested on a clean install with **only Elementor, Elementor Pro and Hello Elementor active**, all caches flushed, on a brand-new page.
1. Under **Elementor → Settings → Advanced**, set **CSS Print Method** to **External File**.
2. Create a new page, add a Heading widget, set its **Text Color** to `#756CDB`, and **Publish**.
3. Note the page's ID (e.g. `21012`) and confirm `wp-content/uploads/elementor/css/post-21012.css` contains `color:#756CDB`.
4. Open the page in the editor and change the Heading's Text Color to `#ABCDEF`. **Do not publish or update.** Let the editor render its preview.
5. Re-read `wp-content/uploads/elementor/css/post-21012.css` on disk.
Result at step 5:
```
STEP 1 — published state
post-21012.css: 180 bytes
contains published colour #756CDB : yes
STEP 3 — editor preview renders
Post_Preview(21014) writes to: post-21012.css
STEP 4 — the PUBLISHED css file
md5 b3857db863ff -> 4c4659efd1a1 (OVERWRITTEN)
contains published colour #756CDB : NO <-- lost
contains DRAFT colour #ABCDEF : YES <-- unpublished style is now live
```
Requesting the stylesheet URL directly then returns the draft build:
```
$ curl https://example.test/wp-content/uploads/elementor/css/post-21012.css
.elementor-21012 .elementor-element.elementor-element-bbbb222 .elementor-heading-title{color:#ABCDEF;}
```
### Expected behavior
Editing a document without publishing should never modify what anonymous visitors load. Preview/autosave CSS should be written to its own file, leaving the published document's stylesheet untouched until the document is actually published.
Giving the preview file its own name is enough, and `Core\Files\Base::__construct()` already applies `elementor/files/file_name` *before* `set_file_name()` and `set_path()`, so it moves the write target and the enqueued URL together:
```php
add_filter( 'elementor/files/file_name', function ( $file_name, $file ) {
if ( $file instanceof \Elementor\Core\Files\CSS\Post_Preview ) {
return preg_replace( '/\.css$/', '-preview.css', $file_name );
}
return $file_name;
}, 10, 2 );
```
I've verified this as a workaround: with it applied, `Post_Preview` writes `post-{id}-preview.css`, the published file stays byte-identical, and the editor still shows draft styling because it enqueues the same filtered name. `Files\Manager::clear_cache()` globs `*`, so the extra files are still cleaned up by *Regenerate Files & Data*.
### Elementor System Info
```
== Server Environment ==
Operating System: Linux
Software: nginx/1.29.5
MySQL version: mariadb.org binary distribution v10.5.29
PHP Version: 8.3.33
PHP Memory Limit: 4096M
PHP Max Input Vars: 5000
PHP Max Post Size: 128M
GD Installed: Yes
ZIP Installed: Yes
Write Permissions: All right
Elementor Library: Connected
== WordPress Environment ==
Version: 7.0.3
Site URL: https://zendo.local
Home URL: https://zendo.local
WP Multisite: No
Max Upload Size: 128 MB
Memory limit: 1024M
Max Memory limit: 1024M
Permalink Structure: /blog/%postname%/
Language: en_US
Timezone: 0
Admin Email: admin@zendo.local
Debug Mode: Active
== Theme ==
Name: Hello Elementor
Version: 3.4.9
Author: Elementor Team
Child Theme: No
== User ==
Role: administrator
WP Profile lang: en-US
User Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36
== Active Plugins ==
Elementor
Version: 4.2.2
Author: Elementor.com
Elementor Pro
Version: 4.2.1
Author: Elementor.com
== Must-Use Plugins ==
Elementor Safe Mode
Version: 1.0.0
Author: Elementor.com
== Elementor - Compatibility Tag ==
Elementor Pro: Compatible
== Elementor Pro - Compatibility Tag ==
```
Relevant Elementor settings for this report, which the System Info export does not include:
```
elementor_css_print_method: 'external'
elementor_experiment-e_font_icon_svg: 'active'
elementor_disable_color_schemes: 'yes'
elementor_optimized_dom_output: false
```
Reproduced on the same versions on a production host (Apache 2.4.68 / PHP 8.3.31 / MariaDB 10.5.29) and on the local environment the export above was taken from (nginx / PHP 8.3.33 / MariaDB 10.5.29). Server, web server and OS differ between the two; the behaviour is identical.
_Refiled from #36849, which was closed for missing the full System Info export. Everything else is unchanged._
### Agreement
- [X] I confirm I have read and followed all the guidelines and instructions outlined in the Elementor Bug Report form.
- [X] I agree that my issue may be closed without further action if it doesn't meet all the requirements outlined in the Elementor Bug Report form.
Contributor guide
Research direction
Start by reproducing the external CSS workflow, then read core/files/css/post-preview.php, Post_Local_Cache, and Core\Files\Base::write(). Check how Files\Manager::clear_cache() handles generated files and compare the published and preview paths. Done means an unpublished preview no longer changes the published stylesheet, while the editor still loads its draft styles.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- php
- Domain
- backend, frontend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100