diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fb89fbb..8381196 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,7 +57,7 @@ jobs: runs-on: ubuntu-latest defaults: run: - working-directory: plugin/remote-editor-bridge + working-directory: plugin/createpress-bridge steps: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0b0c709..d8d0055 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -82,7 +82,7 @@ jobs: TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} with: tagName: ${{ inputs.tag || github.ref_name }} - releaseName: WordPress Remote Editor ${{ inputs.tag || github.ref_name }} + releaseName: CreatePress ${{ inputs.tag || github.ref_name }} releaseDraft: true prerelease: false args: ${{ matrix.args }} @@ -94,9 +94,9 @@ jobs: - name: Package the companion plugin run: | cd plugin - zip -r ../remote-editor-bridge.zip remote-editor-bridge -x 'remote-editor-bridge/vendor/*' 'remote-editor-bridge/composer.*' 'remote-editor-bridge/phpcs.xml.dist' + zip -r ../createpress-bridge.zip createpress-bridge -x 'createpress-bridge/vendor/*' 'createpress-bridge/composer.*' 'createpress-bridge/phpcs.xml.dist' - uses: softprops/action-gh-release@v2 if: startsWith(github.ref, 'refs/tags/') with: draft: true - files: remote-editor-bridge.zip + files: createpress-bridge.zip diff --git a/.gitignore b/.gitignore index 469cc42..7e6d5fd 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,4 @@ src-tauri/target/ src-tauri/gen/schemas/ *.log .DS_Store -plugin/remote-editor-bridge/vendor/ +plugin/createpress-bridge/vendor/ diff --git a/README.md b/README.md index b40b6dc..ffd4986 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# WordPress Remote Editor +# CreatePress A native desktop app for macOS, Windows and Linux that lets you write and publish posts and pages on any WordPress site, using the site's full block editor (Gutenberg), with every block plugin installed on that site working exactly as it does in wp-admin. @@ -16,7 +16,7 @@ On the WordPress site: - WordPress 6.4 or newer, served over HTTPS. - Application Passwords enabled (the default in WordPress core; some security plugins or hosts turn them off). -- Optional but recommended: the **Remote Editor Bridge** plugin from [`plugin/remote-editor-bridge`](plugin/remote-editor-bridge), which removes the second sign-in when opening the editor. +- Optional but recommended: the **CreatePress Bridge** plugin from [`plugin/createpress-bridge`](plugin/createpress-bridge), which removes the second sign-in when opening the editor. On your computer: macOS 14+, Windows 10/11 (WebView2 is installed automatically if missing), or a Debian/Ubuntu-based Linux with WebKitGTK 4.1. @@ -39,14 +39,14 @@ Live tests against a real site (for example a local one on `http://localhost:888 ```sh cd src-tauri -WPRE_TEST_SITE=https://example.com WPRE_TEST_USER=me WPRE_TEST_APP_PASSWORD="xxxx xxxx xxxx xxxx xxxx xxxx" \ +CREATEPRESS_TEST_SITE=https://example.com CREATEPRESS_TEST_USER=me CREATEPRESS_TEST_APP_PASSWORD="xxxx xxxx xxxx xxxx xxxx xxxx" \ cargo test live_tests -- --ignored --nocapture ``` Companion plugin checks: ```sh -cd plugin/remote-editor-bridge +cd plugin/createpress-bridge composer install vendor/bin/phpcs ``` @@ -68,12 +68,16 @@ Pushing a `v*` tag runs [`.github/workflows/release.yml`](.github/workflows/rele | `src/` | Platform-adaptive React shell UI | | `src-tauri/src/` | Rust core: REST client, sign-in, keychain, editor webview, menus | | `src-tauri/capabilities/` | Least-privilege permission grants for each webview | -| `plugin/remote-editor-bridge/` | Optional companion WordPress plugin | +| `plugin/createpress-bridge/` | Optional companion WordPress plugin | | `docs/` | Architecture and security notes | -## Name and trademark +## Trademark -"WordPress" is a registered trademark of the WordPress Foundation. Its trademark policy restricts using "WordPress" in product names, so the app name should be cleared (or changed to something like "Remote Editor for WordPress") before public distribution. The companion plugin already uses a compliant name. +"WordPress" is a registered trademark of the WordPress Foundation. CreatePress is an independent app and is not affiliated with or endorsed by the WordPress Foundation. + +## About + +CreatePress is developed by [Eli Linares](https://realact.net). ## License diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 8dc6e37..98a316a 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,4 +1,4 @@ -# WordPress Remote Editor: Architecture +# CreatePress: Architecture ## Goals @@ -68,7 +68,7 @@ The shell UI is React, but it is *platform-adaptive*, not a single web look: WordPress **Application Passwords** (core since 5.6) are used for all REST traffic. 1. **Discovery**: fetch the site, follow the `Link: <…>; rel="https://api.w.org/"` header to the REST index, read `authentication["application-passwords"].endpoints.authorization`. Works with pretty and plain permalinks (`?rest_route=`). -2. **Authorise**: open WordPress's own `authorize-application.php` in an isolated, incognito auth window. The user signs in with their normal login (2FA, SSO and security plugins keep working) and approves "WordPress Remote Editor". `success_url` is `wpremoteeditor://auth/callback?state=…`; the core intercepts that navigation inside its own webview (so no other app can register the scheme and steal it), checks `state`, and stores the password. +2. **Authorise**: open WordPress's own `authorize-application.php` in an isolated, incognito auth window. The user signs in with their normal login (2FA, SSO and security plugins keep working) and approves "CreatePress". `success_url` is `createpress://auth/callback?state=…`; the core intercepts that navigation inside its own webview (so no other app can register the scheme and steal it), checks `state`, and stores the password. 3. **Manual fallback**: paste a username and an Application Password created under Users → Profile. 4. **Storage**: the password goes straight into the OS keychain (macOS Keychain, Windows Credential Manager, Secret Service/libsecret on Linux) and is held in memory only in zeroizing buffers. 5. **Removal**: removing a site revokes its Application Password on the server (using `/application-passwords/introspect` to find its UUID) and deletes the keychain entry. @@ -77,7 +77,7 @@ WordPress **Application Passwords** (core since 5.6) are used for all REST traff wp-admin uses cookie authentication, which Application Passwords can't provide. Two modes: -- **With the companion plugin** (`plugin/wp-remote-editor-bridge`, recommended): the core calls `POST /wpre/v1/handoff` authenticated by the Application Password. The plugin returns a single-use, 60-second, hashed token bound to that user; the editor webview opens `wp-login.php?action=wpre_handoff&token=…`, which sets the normal auth cookie and redirects into the editor. No second login. +- **With the companion plugin** (`plugin/createpress-bridge`, recommended): the core calls `POST /createpress/v1/handoff` authenticated by the Application Password. The plugin returns a single-use, 60-second, hashed token bound to that user; the editor webview opens `wp-login.php?action=createpress_handoff&token=…`, which sets the normal auth cookie and redirects into the editor. No second login. - **Without the plugin**: the user signs in once inside the editor webview using the site's own login form. The cookie lives in that site's isolated store. ## Roles and capabilities @@ -92,7 +92,7 @@ wp-admin uses cookie authentication, which Application Passwords can't provide. src/ React shell (platform-adaptive UI) src-tauri/ Rust core: REST client, auth, keychain, editor webview capabilities/ Tauri capability files (least privilege) -plugin/wp-remote-editor-bridge/ Optional companion WordPress plugin (GPL-2.0-or-later, WPCS) +plugin/createpress-bridge/ Optional companion WordPress plugin (GPL-2.0-or-later, WPCS) .github/workflows/ CI (lint, test, phpcs) and signed release builds docs/ Architecture and security notes ``` diff --git a/docs/SECURITY.md b/docs/SECURITY.md index ffe4405..1a6958f 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -1,6 +1,6 @@ # Security Model -WordPress Remote Editor holds credentials for websites and renders remote content, so it is designed around least privilege and isolation. +CreatePress holds credentials for websites and renders remote content, so it is designed around least privilege and isolation. ## Credentials @@ -21,7 +21,7 @@ WordPress Remote Editor holds credentials for websites and renders remote conten - Authorization happens on the site's own `authorize-application.php` page, so 2FA, SSO and security plugins work unchanged. - The sign-in window is incognito (no persistent cookies) and has no IPC access. -- The callback (`wpremoteeditor://…`) is intercepted inside that window, not registered with the OS, so another app cannot hijack it. A 256-bit `state` value is checked in constant time. +- The callback (`createpress://…`) is intercepted inside that window, not registered with the OS, so another app cannot hijack it. A 256-bit `state` value is checked in constant time. ## Webviews @@ -34,8 +34,9 @@ WordPress Remote Editor holds credentials for websites and renders remote conten - The editor webview's navigation is locked to the site's origins; other links open in the system browser. - Each site has its own cookie/data store, deleted when the site is removed. - The core controls the editor only by evaluating fixed, built-in scripts. It never evaluates strings that come from a page. +- `Object.prototype` is frozen in the shell only (an initialization script on that webview). Tauri's app-wide `freezePrototype` option is off because it would also apply to the editor webview, where it breaks WordPress's own scripts and leaves the editor blank. -## Companion plugin (`remote-editor-bridge`) +## Companion plugin (`createpress-bridge`) - Handoff links can only be requested over REST with Application Password authentication by users who can edit content. - Tokens are 48 random characters, single use, valid for 60 seconds, stored only as an HMAC-SHA256 hash, rate limited to 10/minute per user, and can only redirect to `post.php`/`post-new.php`. diff --git a/index.html b/index.html index fb0ac86..ebc0619 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - WordPress Remote Editor + CreatePress
diff --git a/package-lock.json b/package-lock.json index 6d4dd81..805d46f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { - "name": "wordpress-remote-editor", - "version": "0.1.0", + "name": "createpress", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "wordpress-remote-editor", - "version": "0.1.0", + "name": "createpress", + "version": "0.2.0", "license": "GPL-2.0-or-later", "dependencies": { "@tauri-apps/api": "^2.8.0", diff --git a/package.json b/package.json index 721b90e..c273330 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "wordpress-remote-editor", + "name": "createpress", "private": true, - "version": "0.1.0", + "version": "0.2.0", "type": "module", "license": "GPL-2.0-or-later", "scripts": { diff --git a/plugin/createpress-bridge/assets/embedded.css b/plugin/createpress-bridge/assets/embedded.css new file mode 100644 index 0000000..478fa07 --- /dev/null +++ b/plugin/createpress-bridge/assets/embedded.css @@ -0,0 +1,21 @@ +/* Hide wp-admin chrome around the block editor inside the desktop app. */ +html.wp-toolbar:has(body.createpress-embedded) { + padding-top: 0; +} + +body.createpress-embedded #wpadminbar, +body.createpress-embedded #adminmenumain, +body.createpress-embedded #wpfooter, +body.createpress-embedded .edit-post-fullscreen-mode-close, +body.createpress-embedded .editor-header__back-button { + display: none !important; +} + +body.createpress-embedded #wpcontent { + margin-left: 0; +} + +body.createpress-embedded .interface-interface-skeleton { + top: 0; + left: 0; +} diff --git a/plugin/remote-editor-bridge/composer.json b/plugin/createpress-bridge/composer.json similarity index 75% rename from plugin/remote-editor-bridge/composer.json rename to plugin/createpress-bridge/composer.json index 645328b..334cfad 100644 --- a/plugin/remote-editor-bridge/composer.json +++ b/plugin/createpress-bridge/composer.json @@ -1,6 +1,6 @@ { - "name": "realact/remote-editor-bridge", - "description": "Companion plugin for the WordPress Remote Editor desktop app.", + "name": "realact/createpress-bridge", + "description": "Companion plugin for the CreatePress desktop app.", "type": "wordpress-plugin", "license": "GPL-2.0-or-later", "require": { diff --git a/plugin/remote-editor-bridge/composer.lock b/plugin/createpress-bridge/composer.lock similarity index 99% rename from plugin/remote-editor-bridge/composer.lock rename to plugin/createpress-bridge/composer.lock index 9682f87..a2263aa 100644 --- a/plugin/remote-editor-bridge/composer.lock +++ b/plugin/createpress-bridge/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1baf6cf078b38181c98f694d660844a7", + "content-hash": "5f0a5864871973ac2546ef3fe6259f19", "packages": [], "packages-dev": [ { diff --git a/plugin/createpress-bridge/createpress-bridge.php b/plugin/createpress-bridge/createpress-bridge.php new file mode 100644 index 0000000..7bc7ea4 --- /dev/null +++ b/plugin/createpress-bridge/createpress-bridge.php @@ -0,0 +1,30 @@ + 401 ) ); } if ( ! current_user_can( 'edit_posts' ) && ! current_user_can( 'edit_pages' ) ) { return new \WP_Error( - 'wpre_forbidden', - __( 'Your role cannot edit content on this site.', 'remote-editor-bridge' ), + 'createpress_forbidden', + __( 'Your role cannot edit content on this site.', 'createpress-bridge' ), array( 'status' => 403 ) ); } @@ -123,8 +123,8 @@ public static function issue( \WP_REST_Request $request ) { $redirect = self::safe_editor_url( (string) $request->get_param( 'redirect_to' ) ); if ( null === $redirect ) { return new \WP_Error( - 'wpre_bad_redirect', - __( 'The editor address is not allowed.', 'remote-editor-bridge' ), + 'createpress_bad_redirect', + __( 'The editor address is not allowed.', 'createpress-bridge' ), array( 'status' => 400 ) ); } @@ -133,8 +133,8 @@ public static function issue( \WP_REST_Request $request ) { $count = (int) get_transient( $rate_key ); if ( $count >= self::MAX_PER_MINUTE ) { return new \WP_Error( - 'wpre_rate_limited', - __( 'Too many sign-in requests. Please wait a minute.', 'remote-editor-bridge' ), + 'createpress_rate_limited', + __( 'Too many sign-in requests. Please wait a minute.', 'createpress-bridge' ), array( 'status' => 429 ) ); } @@ -153,7 +153,7 @@ public static function issue( \WP_REST_Request $request ) { $url = add_query_arg( array( - 'action' => 'wpre_handoff', + 'action' => 'createpress_handoff', 'token' => rawurlencode( $token ), ), wp_login_url() @@ -165,7 +165,7 @@ public static function issue( \WP_REST_Request $request ) { } /** - * Runs on wp-login.php?action=wpre_handoff: verifies the token, signs the + * Runs on wp-login.php?action=createpress_handoff: verifies the token, signs the * user in and redirects to the editor. */ public static function consume() { @@ -223,8 +223,8 @@ public static function consume() { */ private static function fail() { wp_die( - esc_html__( 'This sign-in link has expired or was already used. Return to WordPress Remote Editor and try again.', 'remote-editor-bridge' ), - esc_html__( 'Sign-in link expired', 'remote-editor-bridge' ), + esc_html__( 'This sign-in link has expired or was already used. Return to CreatePress and try again.', 'createpress-bridge' ), + esc_html__( 'Sign-in link expired', 'createpress-bridge' ), array( 'response' => 403 ) ); } diff --git a/plugin/remote-editor-bridge/phpcs.xml.dist b/plugin/createpress-bridge/phpcs.xml.dist similarity index 80% rename from plugin/remote-editor-bridge/phpcs.xml.dist rename to plugin/createpress-bridge/phpcs.xml.dist index 80b9c91..de0b258 100644 --- a/plugin/remote-editor-bridge/phpcs.xml.dist +++ b/plugin/createpress-bridge/phpcs.xml.dist @@ -1,5 +1,5 @@ - + WordPress Coding Standards for the companion plugin. . /vendor/* @@ -11,7 +11,7 @@ - + @@ -19,6 +19,6 @@ - remote-editor-bridge.php + createpress-bridge.php diff --git a/plugin/remote-editor-bridge/readme.txt b/plugin/createpress-bridge/readme.txt similarity index 80% rename from plugin/remote-editor-bridge/readme.txt rename to plugin/createpress-bridge/readme.txt index 1bc6c18..03c689e 100644 --- a/plugin/remote-editor-bridge/readme.txt +++ b/plugin/createpress-bridge/readme.txt @@ -1,18 +1,18 @@ -=== Remote Editor Bridge === +=== CreatePress Bridge === Contributors: realact Tags: block editor, gutenberg, desktop, application passwords, remote editing Requires at least: 6.4 Tested up to: 6.8 Requires PHP: 7.4 -Stable tag: 0.1.0 +Stable tag: 0.2.0 License: GPL-2.0-or-later License URI: https://www.gnu.org/licenses/gpl-2.0.html -Companion plugin for the WordPress Remote Editor desktop app. +Companion plugin for the CreatePress desktop app. == Description == -WordPress Remote Editor is a desktop app for macOS, Windows and Linux that edits your site with its own block editor, so every block plugin you use keeps working. +CreatePress is a desktop app for macOS, Windows and Linux that edits your site with its own block editor, so every block plugin you use keeps working. This optional plugin improves that experience: diff --git a/plugin/remote-editor-bridge/uninstall.php b/plugin/createpress-bridge/uninstall.php similarity index 75% rename from plugin/remote-editor-bridge/uninstall.php rename to plugin/createpress-bridge/uninstall.php index a1b4318..f5be021 100644 --- a/plugin/remote-editor-bridge/uninstall.php +++ b/plugin/createpress-bridge/uninstall.php @@ -3,7 +3,7 @@ * Cleans up on uninstall. Handoff tokens are short-lived transients, so there * is no persistent data to remove beyond any that remain. * - * @package RemoteEditorBridge + * @package CreatePressBridge */ defined( 'WP_UNINSTALL_PLUGIN' ) || exit; @@ -13,7 +13,7 @@ $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s", - $wpdb->esc_like( '_transient_wpre_handoff_' ) . '%', - $wpdb->esc_like( '_transient_timeout_wpre_handoff_' ) . '%' + $wpdb->esc_like( '_transient_createpress_handoff_' ) . '%', + $wpdb->esc_like( '_transient_timeout_createpress_handoff_' ) . '%' ) ); diff --git a/plugin/remote-editor-bridge/assets/embedded.css b/plugin/remote-editor-bridge/assets/embedded.css deleted file mode 100644 index 2ec47bf..0000000 --- a/plugin/remote-editor-bridge/assets/embedded.css +++ /dev/null @@ -1,21 +0,0 @@ -/* Hide wp-admin chrome around the block editor inside the desktop app. */ -html.wp-toolbar:has(body.wpre-embedded) { - padding-top: 0; -} - -body.wpre-embedded #wpadminbar, -body.wpre-embedded #adminmenumain, -body.wpre-embedded #wpfooter, -body.wpre-embedded .edit-post-fullscreen-mode-close, -body.wpre-embedded .editor-header__back-button { - display: none !important; -} - -body.wpre-embedded #wpcontent { - margin-left: 0; -} - -body.wpre-embedded .interface-interface-skeleton { - top: 0; - left: 0; -} diff --git a/plugin/remote-editor-bridge/remote-editor-bridge.php b/plugin/remote-editor-bridge/remote-editor-bridge.php deleted file mode 100644 index 2c5b9f7..0000000 --- a/plugin/remote-editor-bridge/remote-editor-bridge.php +++ /dev/null @@ -1,29 +0,0 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src-tauri/permissions/autogenerated/editor_set_hidden.toml b/src-tauri/permissions/autogenerated/editor_set_hidden.toml new file mode 100644 index 0000000..c4eb963 --- /dev/null +++ b/src-tauri/permissions/autogenerated/editor_set_hidden.toml @@ -0,0 +1,11 @@ +# Automatically generated - DO NOT EDIT! + +[[permission]] +identifier = "allow-editor-set-hidden" +description = "Enables the editor_set_hidden command without any pre-configured scope." +commands.allow = ["editor_set_hidden"] + +[[permission]] +identifier = "deny-editor-set-hidden" +description = "Denies the editor_set_hidden command without any pre-configured scope." +commands.deny = ["editor_set_hidden"] diff --git a/src-tauri/src/auth.rs b/src-tauri/src/auth.rs index 2db19a2..972dc47 100644 --- a/src-tauri/src/auth.rs +++ b/src-tauri/src/auth.rs @@ -3,7 +3,7 @@ //! WordPress's own `authorize-application.php` screen is opened in an //! isolated, incognito window with no IPC access. The user signs in with //! their normal login (2FA/SSO keep working) and approves the app. WordPress -//! then redirects to `wpremoteeditor://auth/callback?...`, which we intercept +//! then redirects to `createpress://auth/callback?...`, which we intercept //! inside our own webview, so no other app can claim the scheme. use crate::error::{AppError, AppResult}; @@ -19,10 +19,10 @@ use url::Url; use uuid::Uuid; use zeroize::Zeroizing; -pub const CALLBACK_SCHEME: &str = "wpremoteeditor"; +pub const CALLBACK_SCHEME: &str = "createpress"; /// Stable app ID so WordPress groups all passwords created by this app. pub const APP_ID: &str = "8f4d7f3e-6b0e-4c1a-9a58-2f0b8f7f9b21"; -pub const APP_NAME: &str = "WordPress Remote Editor"; +pub const APP_NAME: &str = "CreatePress"; const AUTH_WINDOW: &str = "auth"; pub struct PendingAuth { @@ -254,25 +254,15 @@ mod tests { #[test] fn builds_authorize_url_with_state() { let ep = Url::parse("https://example.com/wp-admin/authorize-application.php").unwrap(); - let u = authorize_url( - &ep, - "abc", - "WordPress Remote Editor (laptop, 2026-10-13 09:00)", - ); + let u = authorize_url(&ep, "abc", "CreatePress (laptop, 2026-10-13 09:00)"); let pairs: std::collections::HashMap<_, _> = u.query_pairs().into_owned().collect(); - assert_eq!( - pairs["app_name"], - "WordPress Remote Editor (laptop, 2026-10-13 09:00)" - ); + assert_eq!(pairs["app_name"], "CreatePress (laptop, 2026-10-13 09:00)"); assert_eq!(pairs["app_id"], APP_ID); assert_eq!( pairs["success_url"], - "wpremoteeditor://auth/callback?state=abc" - ); - assert_eq!( - pairs["reject_url"], - "wpremoteeditor://auth/rejected?state=abc" + "createpress://auth/callback?state=abc" ); + assert_eq!(pairs["reject_url"], "createpress://auth/rejected?state=abc"); } #[test] @@ -283,12 +273,9 @@ mod tests { .unwrap(); assert_eq!( password_name("eli-mbp", t), - "WordPress Remote Editor (eli-mbp, 2026-10-13 09:05)" - ); - assert_eq!( - password_name(" ", t), - "WordPress Remote Editor (2026-10-13 09:05)" + "CreatePress (eli-mbp, 2026-10-13 09:05)" ); + assert_eq!(password_name(" ", t), "CreatePress (2026-10-13 09:05)"); assert!(!password_name("bad\nname", t).contains('\n')); } diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 622d45d..4581e71 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -245,6 +245,13 @@ pub fn editor_set_bounds(app: AppHandle, bounds: Bounds) -> AppResult<()> { editor::set_bounds(&app, bounds) } +/// Hides the editor pane while a shell sheet (such as About) is shown, since +/// the pane is a separate webview drawn above the shell. +#[tauri::command] +pub fn editor_set_hidden(app: AppHandle, hidden: bool) -> AppResult<()> { + editor::set_hidden(&app, hidden) +} + #[tauri::command] pub fn editor_command(app: AppHandle, command: EditorCommand) -> AppResult<()> { editor::command(&app, command) diff --git a/src-tauri/src/editor.rs b/src-tauri/src/editor.rs index 08c4faa..a00661c 100644 --- a/src-tauri/src/editor.rs +++ b/src-tauri/src/editor.rs @@ -68,14 +68,14 @@ pub enum EditorCommand { impl EditorCommand { fn script(self) -> &'static str { match self { - Self::Save => "window.__wpre && window.__wpre.save()", - Self::Publish => "window.__wpre && window.__wpre.publish()", - Self::Undo => "window.__wpre && window.__wpre.undo()", - Self::Redo => "window.__wpre && window.__wpre.redo()", - Self::ToggleSettings => "window.__wpre && window.__wpre.toggleSettings()", - Self::DeviceDesktop => "window.__wpre && window.__wpre.device('Desktop')", - Self::DeviceTablet => "window.__wpre && window.__wpre.device('Tablet')", - Self::DeviceMobile => "window.__wpre && window.__wpre.device('Mobile')", + Self::Save => "window.__createpress && window.__createpress.save()", + Self::Publish => "window.__createpress && window.__createpress.publish()", + Self::Undo => "window.__createpress && window.__createpress.undo()", + Self::Redo => "window.__createpress && window.__createpress.redo()", + Self::ToggleSettings => "window.__createpress && window.__createpress.toggleSettings()", + Self::DeviceDesktop => "window.__createpress && window.__createpress.device('Desktop')", + Self::DeviceTablet => "window.__createpress && window.__createpress.device('Tablet')", + Self::DeviceMobile => "window.__createpress && window.__createpress.device('Mobile')", } } } @@ -186,7 +186,7 @@ async fn handoff_url(site: &SiteRecord, target: &Url) -> AppResult { let client = WpClient::new(site.api_root.clone(), &creds, site.allow_insecure_local)?; let h: Handoff = client .post( - "wpre/v1/handoff", + "createpress/v1/handoff", &serde_json::json!({ "redirect_to": target.as_str() }), ) .await?; @@ -240,7 +240,7 @@ pub async fn open( .map(|u| u.origin().ascii_serialization()) .collect(); let config = serde_json::json!({ "origins": origins }).to_string(); - let script = BRIDGE_JS.replace("__WPRE_CONFIG__", &config); + let script = BRIDGE_JS.replace("__CREATEPRESS_CONFIG__", &config); // Grant the remote page exactly one command, only on this site's origins. let mut cap = CapabilityBuilder::new(format!("editor-remote-{site_id}")) @@ -306,6 +306,17 @@ pub fn set_bounds(app: &AppHandle, bounds: Bounds) -> AppResult<()> { Ok(()) } +pub fn set_hidden(app: &AppHandle, hidden: bool) -> AppResult<()> { + if let Some(w) = app.get_webview(EDITOR_LABEL) { + if hidden { + w.hide()?; + } else { + w.show()?; + } + } + Ok(()) +} + /// On Linux, Tauri stacks a window's webviews in a vertical GTK box and /// ignores explicit positions. The shell keeps the editor full-width below its /// toolbar (focus mode, the same layout as macOS and Windows), so on Linux the diff --git a/src-tauri/src/editor_bridge.js b/src-tauri/src/editor_bridge.js index d35fe5a..ab9f582 100644 --- a/src-tauri/src/editor_bridge.js +++ b/src-tauri/src/editor_bridge.js @@ -3,7 +3,7 @@ // editor state and reports a small, fixed-shape object to the native shell. (function () { "use strict"; - var CONFIG = __WPRE_CONFIG__; + var CONFIG = __CREATEPRESS_CONFIG__; if (window.top !== window) return; if (CONFIG.origins.indexOf(window.location.origin) === -1) return; @@ -16,12 +16,12 @@ ".interface-interface-skeleton{top:0!important;left:0!important}" + "body.is-fullscreen-mode .interface-interface-skeleton{top:0!important}"; function injectStyle() { - if (document.getElementById("wpre-embedded-style")) return; + if (document.getElementById("createpress-embedded-style")) return; var style = document.createElement("style"); - style.id = "wpre-embedded-style"; + style.id = "createpress-embedded-style"; style.textContent = css; (document.head || document.documentElement).appendChild(style); - document.documentElement.classList.add("wpre-embedded"); + document.documentElement.classList.add("createpress-embedded"); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", injectStyle); @@ -107,7 +107,7 @@ return false; } - Object.defineProperty(window, "__wpre", { + Object.defineProperty(window, "__createpress", { configurable: false, writable: false, value: Object.freeze({ diff --git a/src-tauri/src/keychain.rs b/src-tauri/src/keychain.rs index ec12424..6eb2175 100644 --- a/src-tauri/src/keychain.rs +++ b/src-tauri/src/keychain.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; -const SERVICE: &str = "com.wpremoteeditor.app"; +const SERVICE: &str = "net.realact.createpress"; #[derive(Serialize, Deserialize, Zeroize, ZeroizeOnDrop)] pub struct Credentials { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 52753e1..d882c2b 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -54,13 +54,16 @@ pub fn run() { commands::open_editor, commands::close_editor, commands::editor_set_bounds, + commands::editor_set_hidden, commands::editor_command, commands::editor_report, ]) .run(tauri::generate_context!()) - .expect("error while running WordPress Remote Editor"); + .expect("error while running CreatePress"); } +const SHELL_FREEZE_PROTOTYPE: &str = "Object.freeze(Object.prototype);"; + /// The main window is a plain window hosting two webviews: the shell UI /// (resized with the window) and, when open, the editor placed over the /// shell's editor area. Building it this way keeps child-webview layout @@ -78,10 +81,29 @@ fn create_main_window(app: &mut tauri::App) -> tauri::Result<()> { let size = window .inner_size()? .to_logical::(window.scale_factor()?); + // Freeze Object.prototype in the shell only. Tauri's app-wide + // `freezePrototype` option also injects into the editor webview, where it + // breaks WordPress's own scripts (moment, @wordpress/components) and the + // block editor never renders, leaving a blank pane. window.add_child( - tauri::webview::WebviewBuilder::from_config(&config).auto_resize(), + tauri::webview::WebviewBuilder::from_config(&config) + .initialization_script(SHELL_FREEZE_PROTOTYPE) + .auto_resize(), LogicalPosition::new(0.0, 0.0), size, )?; Ok(()) } + +#[cfg(test)] +mod tests { + /// Regression: the app-wide option reaches the editor webview and leaves + /// New Post / New Page blank. The shell freezes its own prototype instead. + #[test] + fn prototype_freeze_is_shell_only() { + let conf: serde_json::Value = + serde_json::from_str(include_str!("../tauri.conf.json")).unwrap(); + assert_eq!(conf["app"]["security"]["freezePrototype"], false); + assert!(super::SHELL_FREEZE_PROTOTYPE.contains("Object.freeze(Object.prototype)")); + } +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 06a51b6..7dc4981 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -2,5 +2,5 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] fn main() { - wpre_lib::run(); + createpress_lib::run(); } diff --git a/src-tauri/src/menu.rs b/src-tauri/src/menu.rs index ef49862..c31cf1e 100644 --- a/src-tauri/src/menu.rs +++ b/src-tauri/src/menu.rs @@ -1,7 +1,7 @@ //! Native application menu. Standard Edit items matter on macOS: without them //! clipboard shortcuts don't reach the webviews. -use tauri::menu::{AboutMetadata, Menu, MenuBuilder, MenuItemBuilder, SubmenuBuilder}; +use tauri::menu::{Menu, MenuBuilder, MenuItemBuilder, SubmenuBuilder}; use tauri::{AppHandle, Emitter, Runtime}; pub fn build(app: &AppHandle) -> tauri::Result> { @@ -64,16 +64,20 @@ pub fn build(app: &AppHandle) -> tauri::Result> { .close_window() .build()?; - let help = SubmenuBuilder::new(app, "Help") - .item(&item("help-docs", "WordPress Remote Editor Help", None)?) - .build()?; + let help = SubmenuBuilder::new(app, "Help").item(&item("help-docs", "CreatePress Help", None)?); + // On macOS, About lives in the app menu instead. + #[cfg(not(target_os = "macos"))] + let help = help + .separator() + .item(&item("about", "About CreatePress", None)?); + let help = help.build()?; #[allow(unused_mut)] let mut menu = MenuBuilder::new(app); #[cfg(target_os = "macos")] { - let app_menu = SubmenuBuilder::new(app, "WordPress Remote Editor") - .about(Some(AboutMetadata::default())) + let app_menu = SubmenuBuilder::new(app, "CreatePress") + .item(&item("about", "About CreatePress", None)?) .separator() .services() .separator() @@ -85,8 +89,6 @@ pub fn build(app: &AppHandle) -> tauri::Result> { .build()?; menu = menu.item(&app_menu); } - #[cfg(not(target_os = "macos"))] - let _ = AboutMetadata::default(); menu.items(&[&file, &edit, &view, &window, &help]).build() } diff --git a/src-tauri/src/sites.rs b/src-tauri/src/sites.rs index 8c28970..221b5d2 100644 --- a/src-tauri/src/sites.rs +++ b/src-tauri/src/sites.rs @@ -151,7 +151,7 @@ mod tests { #[test] fn upsert_dedupes_and_persists() { - let dir = std::env::temp_dir().join(format!("wpre-test-{}", Uuid::new_v4())); + let dir = std::env::temp_dir().join(format!("createpress-test-{}", Uuid::new_v4())); let path = dir.join("sites.json"); let store = SiteStore::load(path.clone()); let a = store.upsert(record("https://a.com/", "eli")).unwrap(); diff --git a/src-tauri/src/wp/client.rs b/src-tauri/src/wp/client.rs index 66232c1..635b71b 100644 --- a/src-tauri/src/wp/client.rs +++ b/src-tauri/src/wp/client.rs @@ -11,7 +11,7 @@ use std::time::Duration; use url::Url; use zeroize::Zeroizing; -pub const USER_AGENT: &str = concat!("WordPressRemoteEditor/", env!("CARGO_PKG_VERSION")); +pub const USER_AGENT: &str = concat!("CreatePress/", env!("CARGO_PKG_VERSION")); const MAX_RESPONSE_BYTES: usize = 16 * 1024 * 1024; const MAX_REDIRECTS: usize = 5; diff --git a/src-tauri/src/wp/discovery.rs b/src-tauri/src/wp/discovery.rs index 657050e..02747ab 100644 --- a/src-tauri/src/wp/discovery.rs +++ b/src-tauri/src/wp/discovery.rs @@ -100,7 +100,7 @@ pub async fn discover(input: &str, allow_insecure_local: bool) -> AppResult Option<(String, String, String)> { Some(( - std::env::var("WPRE_TEST_SITE").ok()?, - std::env::var("WPRE_TEST_USER").ok()?, - std::env::var("WPRE_TEST_APP_PASSWORD").ok()?, + std::env::var("CREATEPRESS_TEST_SITE").ok()?, + std::env::var("CREATEPRESS_TEST_USER").ok()?, + std::env::var("CREATEPRESS_TEST_APP_PASSWORD").ok()?, )) } @@ -79,7 +79,7 @@ mod live_tests { let target = d.admin_url.join("post-new.php").unwrap(); let h: Handoff = client .post( - "wpre/v1/handoff", + "createpress/v1/handoff", &serde_json::json!({ "redirect_to": target.as_str() }), ) .await @@ -88,7 +88,7 @@ mod live_tests { assert!(crate::security::same_origin(&d.admin_url, &url)); assert!(url .query_pairs() - .any(|(k, v)| k == "action" && v == "wpre_handoff")); + .any(|(k, v)| k == "action" && v == "createpress_handoff")); println!(" handoff ok"); } } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index e970dcc..a44e401 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,9 +1,9 @@ { "$schema": "https://schema.tauri.app/config/2", - "productName": "WordPress Remote Editor", - "mainBinaryName": "wordpress-remote-editor", - "version": "0.1.0", - "identifier": "com.wpremoteeditor.app", + "productName": "CreatePress", + "mainBinaryName": "createpress", + "version": "0.2.0", + "identifier": "net.realact.createpress", "build": { "beforeDevCommand": "npm run dev", "devUrl": "http://localhost:1420", @@ -16,7 +16,7 @@ "windows": [ { "label": "main", - "title": "WordPress Remote Editor", + "title": "CreatePress", "width": 1280, "height": 820, "minWidth": 900, @@ -27,7 +27,7 @@ } ], "security": { - "freezePrototype": true, + "freezePrototype": false, "csp": { "default-src": "'self'", "script-src": "'self'", @@ -54,9 +54,9 @@ "category": "Productivity", "shortDescription": "A native desktop editor for WordPress sites", "longDescription": "Write and publish posts and pages on your WordPress sites with the full block editor, including blocks from any plugin, in a native desktop app.", - "copyright": "\u00a9 WordPress Remote Editor contributors", + "copyright": "\u00a9 2026 Eli Linares", "license": "GPL-2.0-or-later", - "publisher": "WordPress Remote Editor", + "publisher": "Eli Linares", "icon": [ "icons/32x32.png", "icons/128x128.png", diff --git a/src-tauri/tauri.macos.conf.json b/src-tauri/tauri.macos.conf.json index d1f6590..b82867b 100644 --- a/src-tauri/tauri.macos.conf.json +++ b/src-tauri/tauri.macos.conf.json @@ -4,7 +4,7 @@ "windows": [ { "label": "main", - "title": "WordPress Remote Editor", + "title": "CreatePress", "width": 1280, "height": 820, "minWidth": 900, diff --git a/src-tauri/tauri.windows.conf.json b/src-tauri/tauri.windows.conf.json index 34f4c09..ea62883 100644 --- a/src-tauri/tauri.windows.conf.json +++ b/src-tauri/tauri.windows.conf.json @@ -4,7 +4,7 @@ "windows": [ { "label": "main", - "title": "WordPress Remote Editor", + "title": "CreatePress", "width": 1280, "height": 820, "minWidth": 900, diff --git a/src/App.tsx b/src/App.tsx index be55b33..b80f588 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,6 +3,7 @@ import { listen } from "@tauri-apps/api/event"; import { ask, message } from "@tauri-apps/plugin-dialog"; import { api, errorMessage, type EditorReport, type PostTypeAccess, type SiteContext, type SiteSummary } from "./lib/api"; import type { Platform } from "./lib/platform"; +import { AboutSheet } from "./components/AboutSheet"; import { AddSiteSheet } from "./components/AddSiteSheet"; import { EditorView } from "./components/EditorView"; import { Icon } from "./components/Icon"; @@ -23,6 +24,7 @@ export function App({ platform }: { platform: Platform }) { const [waitingForBrowser, setWaitingForBrowser] = useState(false); const [authError, setAuthError] = useState(null); const [reloadKey, setReloadKey] = useState(0); + const [showAbout, setShowAbout] = useState(false); // Refs so native event handlers always see current state. const state = useRef({ view, report, context, postType }); @@ -132,6 +134,16 @@ export function App({ platform }: { platform: Platform }) { [refreshSites], ); + // The editor pane is a separate webview drawn above the shell, so hide it + // while the About sheet is open. + const editorOpen = view.kind === "editor"; + useEffect(() => { + if (!showAbout || !editorOpen) return; + void api.setEditorHidden(true).catch(() => {}); + return () => void api.setEditorHidden(false).catch(() => {}); + }, [showAbout, editorOpen]); + const closeAbout = useCallback(() => setShowAbout(false), []); + // Native events: auth results, editor state, menu actions. useEffect(() => { const unlisten = [ @@ -180,6 +192,9 @@ export function App({ platform }: { platform: Platform }) { case "add-site": void startAddSite(); return; + case "about": + setShowAbout(true); + return; case "toggle-sidebar": document.documentElement.classList.toggle("sidebar-hidden"); window.dispatchEvent(new Event("resize")); @@ -201,6 +216,7 @@ export function App({ platform }: { platform: Platform }) { onSelectType={selectType} onAddSite={startAddSite} onRemoveSite={removeSite} + onAbout={() => setShowAbout(true)} />
{sites.length === 0 ? ( @@ -239,6 +255,7 @@ export function App({ platform }: { platform: Platform }) { onCancel={() => setAdding(false)} /> )} + {showAbout && } ); } @@ -249,7 +266,7 @@ function Welcome({ onAdd, platform }: { onAdd: () => void; platform: Platform })
-

Welcome to WordPress Remote Editor

+

Welcome to CreatePress

Write and publish on your WordPress sites with the full block editor, including blocks from your plugins. Your sign-in is kept in the {platform === "macos" ? "macOS Keychain" : platform === "windows" ? "Windows Credential Manager" : "system keyring"}. diff --git a/src/components/AboutSheet.tsx b/src/components/AboutSheet.tsx new file mode 100644 index 0000000..f3638e7 --- /dev/null +++ b/src/components/AboutSheet.tsx @@ -0,0 +1,58 @@ +import { useEffect, useState } from "react"; +import { getVersion } from "@tauri-apps/api/app"; +import { openUrl } from "@tauri-apps/plugin-opener"; + +export const DEVELOPER = "Eli Linares"; +export const DEVELOPER_URL = "https://realact.net"; + +export function AboutSheet({ onClose }: { onClose: () => void }) { + const [version, setVersion] = useState(null); + + useEffect(() => { + getVersion().then(setVersion, () => setVersion(null)); + const onKey = (e: KeyboardEvent) => e.key === "Escape" && onClose(); + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, [onClose]); + + return ( +

+
e.stopPropagation()} + > + +

CreatePress

+ {version &&

Version {version}

} +

Write and publish on your WordPress sites with the full block editor.

+
+
Developer
+
{DEVELOPER}
+
Website
+
+ { + e.preventDefault(); + void openUrl(DEVELOPER_URL); + }} + > + realact.net + +
+
+

+ Licensed under the GPL, version 2 or later. WordPress is a trademark of the WordPress Foundation; CreatePress is not affiliated with it. +

+
+ +
+
+
+ ); +} diff --git a/src/components/AddSiteSheet.tsx b/src/components/AddSiteSheet.tsx index 07a9523..cef5843 100644 --- a/src/components/AddSiteSheet.tsx +++ b/src/components/AddSiteSheet.tsx @@ -128,7 +128,7 @@ export function AddSiteSheet({ waitingForBrowser, authError, onStartedAuthorizat {waitingForBrowser ? (

- Approve WordPress Remote Editor in the sign-in window. This sheet updates when you're done. + Approve CreatePress in the sign-in window. This sheet updates when you're done.

) : (

diff --git a/src/components/EditorView.tsx b/src/components/EditorView.tsx index 797812e..0a03614 100644 --- a/src/components/EditorView.tsx +++ b/src/components/EditorView.tsx @@ -128,7 +128,7 @@ export function EditorView({ context, postType, postId, report, onBack }: Props) {report?.kind === "login" && (

Sign in to {context.site.name} once below to open the editor. - {!context.site.bridgeAvailable && " Install the Remote Editor Bridge plugin on the site to skip this step."} + {!context.site.bridgeAvailable && " Install the CreatePress Bridge plugin on the site to skip this step."}
)} {error && ( diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx index 06488e2..9332c87 100644 --- a/src/components/Icon.tsx +++ b/src/components/Icon.tsx @@ -18,6 +18,7 @@ const paths: Record = { globe: "M10 17a7 7 0 1 0 0-14 7 7 0 0 0 0 14ZM3 10h14M10 3c2 2 2.8 4.5 2.8 7S12 15 10 17c-2-2-2.8-4.5-2.8-7S8 5 10 3Z", lock: "M5.5 9h9v7.5h-9zM7.5 9V6.5a2.5 2.5 0 0 1 5 0V9", check: "M4.5 10.5 8 14l7.5-8", + info: "M10 17a7 7 0 1 0 0-14 7 7 0 0 0 0 14ZM10 9.5v4.5M10 6.5h.01", refresh: "M15.5 8A6 6 0 0 0 4.6 6.5M4.5 3.5v3h3M4.5 12a6 6 0 0 0 10.9 1.5M15.5 16.5v-3h-3", }; diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index eaf67dc..b751b6d 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -12,6 +12,7 @@ interface Props { onSelectType: (t: PostTypeAccess) => void; onAddSite: () => void; onRemoveSite: (site: SiteSummary) => void; + onAbout: () => void; } export function Sidebar(props: Props) { @@ -75,6 +76,15 @@ export function Sidebar(props: Props) { )} +
+ +
+ {context && (
diff --git a/src/lib/api.ts b/src/lib/api.ts index adfeb2d..76a8f44 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -116,6 +116,7 @@ export const api = { invoke("open_editor", { siteId, postType, postId, bounds }), closeEditor: () => invoke("close_editor"), setEditorBounds: (bounds: Bounds) => invoke("editor_set_bounds", { bounds }), + setEditorHidden: (hidden: boolean) => invoke("editor_set_hidden", { hidden }), editorCommand: (command: EditorCommand) => invoke("editor_command", { command }), }; diff --git a/src/styles/app.css b/src/styles/app.css index 0282ed8..5067934 100644 --- a/src/styles/app.css +++ b/src/styles/app.css @@ -867,3 +867,61 @@ h2 { animation: none; } } + +/* ---- About ------------------------------------------------------------- */ +.sheet.about { + width: min(360px, calc(100vw - 32px)); + text-align: center; +} +.about-icon { + display: block; + margin: 4px auto 10px; + border-radius: 22px; +} +.sheet.about h2 { + font-size: 20px; + margin-bottom: 2px; +} +.about-version { + color: var(--text-muted); + font-size: 12px; + margin: 0 0 10px; +} +.about-tagline { + margin: 0 0 14px; + line-height: 1.45; +} +.about-credits { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 4px 12px; + margin: 0 0 14px; +} +.about-credits dt { + text-align: right; + color: var(--text-muted); +} +.about-credits dd { + margin: 0; + text-align: left; + font-weight: 500; +} +.about-credits a { + color: var(--accent); + text-decoration: none; +} +.about-credits a:hover { + text-decoration: underline; +} +.about-legal { + color: var(--text-muted); + font-size: 11px; + line-height: 1.4; + margin: 0 0 12px; +} +.sheet.about .sheet-actions { + justify-content: center; +} +.sidebar-about { + padding: 0 10px 8px; +}