Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand All @@ -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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ src-tauri/target/
src-tauri/gen/schemas/
*.log
.DS_Store
plugin/remote-editor-bridge/vendor/
plugin/createpress-bridge/vendor/
18 changes: 11 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.

Expand All @@ -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.

Expand All @@ -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
```
Expand All @@ -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

Expand Down
8 changes: 4 additions & 4 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# WordPress Remote Editor: Architecture
# CreatePress: Architecture

## Goals

Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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
```
Expand Down
7 changes: 4 additions & 3 deletions docs/SECURITY.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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

Expand All @@ -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`.
Expand Down
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="light dark" />
<title>WordPress Remote Editor</title>
<title>CreatePress</title>
</head>
<body>
<div id="root"></div>
Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
21 changes: 21 additions & 0 deletions plugin/createpress-bridge/assets/embedded.css
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 30 additions & 0 deletions plugin/createpress-bridge/createpress-bridge.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php
/**
* Plugin Name: CreatePress Bridge
* Plugin URI: https://realact.net
* Description: Companion plugin for the CreatePress desktop app. Lets the app open the block editor without a second sign-in and blends the editor into the app.
* Version: 0.2.0
* Requires at least: 6.4
* Requires PHP: 7.4
* Author: Eli Linares
* Author URI: https://realact.net
* License: GPL-2.0-or-later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: createpress-bridge
*
* @package CreatePressBridge
*/

namespace CreatePressBridge;

defined( 'ABSPATH' ) || exit;

const VERSION = '0.2.0';
const REST_NAMESPACE = 'createpress/v1';

require_once __DIR__ . '/includes/class-handoff.php';
require_once __DIR__ . '/includes/class-embedded-mode.php';

add_action( 'rest_api_init', array( Handoff::class, 'register_routes' ) );
add_action( 'login_form_createpress_handoff', array( Handoff::class, 'consume' ) );
Embedded_Mode::init();
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
* The app also injects equivalent styles itself, so this is a progressive
* enhancement that avoids a flash of wp-admin chrome while loading.
*
* @package RemoteEditorBridge
* @package CreatePressBridge
*/

namespace RemoteEditorBridge;
namespace CreatePressBridge;

defined( 'ABSPATH' ) || exit;

Expand Down Expand Up @@ -41,7 +41,7 @@ public static function is_embedded() {
* @return string
*/
public static function body_class( $classes ) {
return self::is_embedded() ? $classes . ' wpre-embedded' : $classes;
return self::is_embedded() ? $classes . ' createpress-embedded' : $classes;
}

/**
Expand All @@ -56,7 +56,7 @@ public static function enqueue() {
return;
}
wp_enqueue_style(
'remote-editor-bridge-embedded',
'createpress-bridge-embedded',
plugins_url( 'assets/embedded.css', __DIR__ ),
array(),
VERSION
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
* seconds, for that user only. Opening it in the app's editor webview sets the
* normal WordPress auth cookie and redirects to the block editor.
*
* @package RemoteEditorBridge
* @package CreatePressBridge
*/

namespace RemoteEditorBridge;
namespace CreatePressBridge;

defined( 'ABSPATH' ) || exit;

Expand All @@ -21,12 +21,12 @@ final class Handoff {

const TTL = 60;
const MAX_PER_MINUTE = 10;
const TRANSIENT = 'wpre_handoff_';
const RATE_TRANSIENT = 'wpre_handoff_rate_';
const EMBED_COOKIE = 'wpre_embedded';
const TRANSIENT = 'createpress_handoff_';
const RATE_TRANSIENT = 'createpress_handoff_rate_';
const EMBED_COOKIE = 'createpress_embedded';

/**
* Registers REST routes under wpre/v1.
* Registers REST routes under createpress/v1.
*/
public static function register_routes() {
register_rest_route(
Expand Down Expand Up @@ -69,15 +69,15 @@ public static function register_routes() {
public static function can_issue() {
if ( ! is_user_logged_in() || ! rest_get_authenticated_app_password() ) {
return new \WP_Error(
'wpre_app_password_required',
__( 'Sign-in handoff requires Application Password authentication.', 'remote-editor-bridge' ),
'createpress_app_password_required',
__( 'Sign-in handoff requires Application Password authentication.', 'createpress-bridge' ),
array( 'status' => 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 )
);
}
Expand Down Expand Up @@ -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 )
);
}
Expand All @@ -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 )
);
}
Expand All @@ -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()
Expand All @@ -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() {
Expand Down Expand Up @@ -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 )
);
}
Expand Down
Loading
Loading