├── .gitignore ├── README.md ├── nuxt ├── .env.example ├── .gitignore ├── README.md ├── assets │ └── README.md ├── components │ ├── Logo.vue │ └── README.md ├── layouts │ ├── README.md │ └── default.vue ├── middleware │ ├── README.md │ └── auth.js ├── nuxt.config.js ├── package-lock.json ├── package.json ├── pages │ ├── README.md │ ├── _.vue │ └── index.vue ├── plugins │ └── README.md ├── static │ ├── README.md │ └── favicon.ico └── store │ └── README.md ├── readme-images ├── front-end-domain-field.jpg ├── private-page-with-token.jpg └── public-page.jpg └── wp ├── .gitignore ├── .htaccess.example ├── wp-config-sample.php └── wp-content └── plugins ├── classic-editor ├── LICENSE.md ├── classic-editor.php ├── js │ └── block-editor-plugin.js └── readme.txt ├── headless-wp └── headless-wp.php └── jwt-authentication-for-wp-rest-api ├── LICENSE.txt ├── README.md ├── composer.json ├── composer.lock ├── includes ├── class-jwt-auth-i18n.php ├── class-jwt-auth-loader.php ├── class-jwt-auth.php ├── index.php └── vendor │ ├── autoload.php │ ├── composer │ ├── ClassLoader.php │ ├── LICENSE │ ├── autoload_classmap.php │ ├── autoload_namespaces.php │ ├── autoload_psr4.php │ ├── autoload_real.php │ ├── autoload_static.php │ └── installed.json │ └── firebase │ └── php-jwt │ ├── LICENSE │ ├── README.md │ ├── composer.json │ └── src │ ├── BeforeValidException.php │ ├── ExpiredException.php │ ├── JWT.php │ └── SignatureInvalidException.php ├── index.php ├── jwt-auth.php ├── languages └── jwt-auth.pot ├── public ├── class-jwt-auth-public.php └── index.php ├── readme.txt └── tests └── GeneralTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WP Headless Previews 2 | Architecture for a headless wordpress app that supports private content previewing (paired with Nuxt in this example). 3 | 4 | ## How it works 5 | By default, private content is invisible to the Wordpress REST API. Having the ability to preview content privately is important to content editors, and this system adds support for that feature. 6 | 7 | An additional permalink field is added so that Wordpress knows where to go for previews: 8 | 9 | ![Front End Domain Field](https://github.com/chris-geelhoed/wp-headless-previews/blob/master/readme-images/front-end-domain-field.jpg) 10 | 11 | Once this is in place, post permalinks will point to the app's front end: 12 | 13 | ![Altered Permalink](https://github.com/chris-geelhoed/wp-headless-previews/blob/master/readme-images/public-page.jpg) 14 | 15 | And if the page is set as private, a token will be appended to the url to allow for the page to be previewed (See the url shown at the bottom of the following image). The front end of the headless app can read this token and use it to fetch protected content. Private pages are not viewable without this. 16 | 17 | 18 | ![Altered Permalink With Token](https://github.com/chris-geelhoed/wp-headless-previews/blob/master/readme-images/private-page-with-token.jpg) 19 | 20 | ## Setup 21 | 22 | ### Wordpress 23 | The `wp` directory houses a Wordpress installation. There are 3 plugins included: 24 | 1. JWT Authentication for WP-API - This plugin provides authentication for the Wordpress rest API. It is available on the Wordpress plugin directory for free. 25 | 2. Headless WP - This is a small plugin that alters the Wordpress admin to connect with a decoupled front end like Nuxt. 26 | 3. Classic Editor - This is just a personal preference. 27 | 28 | Aside from those plugins, the Wordpress installation can be totally standard. Reference the example `.htaccess` and `wp-config.php` files - there are a few variables that must be provided for this system to work. 29 | 30 | ### Nuxt (Front Facing Part) 31 | Nuxt has been set to call Wordpress for content. See the example `.env` file - you'll need to specify the url of the Wordpress site there. To get a general sense of how this works look at the auth middleware file in nuxt - in this example I am using axios to make requests to the CMS and am using middleware to add a default header to each request (using the token originally received as a query string) and am altering the request to ask for posts with all status' - not just published posts. 32 | -------------------------------------------------------------------------------- /nuxt/.env.example: -------------------------------------------------------------------------------- 1 | API_HOST=http://local.wp-headless-previews.com 2 | -------------------------------------------------------------------------------- /nuxt/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # parcel-bundler cache (https://parceljs.org/) 63 | .cache 64 | 65 | # next.js build output 66 | .next 67 | 68 | # nuxt.js build output 69 | .nuxt 70 | 71 | # Nuxt generate 72 | dist 73 | 74 | # vuepress build output 75 | .vuepress/dist 76 | 77 | # Serverless directories 78 | .serverless 79 | 80 | # IDE / Editor 81 | .idea 82 | .editorconfig 83 | 84 | # Service worker 85 | sw.* 86 | 87 | # Mac OSX 88 | .DS_Store 89 | 90 | # Vim swap files 91 | *.swp 92 | -------------------------------------------------------------------------------- /nuxt/README.md: -------------------------------------------------------------------------------- 1 | # wp-headless-previews 2 | 3 | > WP Nuxt Previews 4 | 5 | ## Build Setup 6 | 7 | ``` bash 8 | # install dependencies 9 | $ npm run install 10 | 11 | # serve with hot reload at localhost:3000 12 | $ npm run dev 13 | 14 | # build for production and launch server 15 | $ npm run build 16 | $ npm run start 17 | 18 | # generate static project 19 | $ npm run generate 20 | ``` 21 | 22 | For detailed explanation on how things work, check out [Nuxt.js docs](https://nuxtjs.org). 23 | -------------------------------------------------------------------------------- /nuxt/assets/README.md: -------------------------------------------------------------------------------- 1 | # ASSETS 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your un-compiled assets such as LESS, SASS, or JavaScript. 6 | 7 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/assets#webpacked). 8 | -------------------------------------------------------------------------------- /nuxt/components/Logo.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 80 | -------------------------------------------------------------------------------- /nuxt/components/README.md: -------------------------------------------------------------------------------- 1 | # COMPONENTS 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | The components directory contains your Vue.js Components. 6 | 7 | _Nuxt.js doesn't supercharge these components._ 8 | -------------------------------------------------------------------------------- /nuxt/layouts/README.md: -------------------------------------------------------------------------------- 1 | # LAYOUTS 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your Application Layouts. 6 | 7 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/views#layouts). 8 | -------------------------------------------------------------------------------- /nuxt/layouts/default.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 56 | -------------------------------------------------------------------------------- /nuxt/middleware/README.md: -------------------------------------------------------------------------------- 1 | # MIDDLEWARE 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your application middleware. 6 | Middleware let you define custom functions that can be run before rendering either a page or a group of pages. 7 | 8 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/routing#middleware). 9 | -------------------------------------------------------------------------------- /nuxt/middleware/auth.js: -------------------------------------------------------------------------------- 1 | export default function ({ $axios, route }) { 2 | if (!route.query || !route.query.preview_token) { 3 | return 4 | } 5 | let token = route.query.preview_token 6 | $axios.defaults.headers.common['Authorization'] = `Bearer ${token}` 7 | if (!$axios.defaults.params) { 8 | $axios.defaults.params = {} 9 | } 10 | $axios.onRequest((config) => ({ 11 | ...config, 12 | params: { 13 | status: [ 14 | 'publish', 15 | 'future', 16 | 'draft', 17 | 'pending', 18 | 'private' 19 | ] 20 | } 21 | })) 22 | } 23 | -------------------------------------------------------------------------------- /nuxt/nuxt.config.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | 3 | export default { 4 | mode: 'universal', 5 | /* 6 | ** Headers of the page 7 | */ 8 | head: { 9 | title: process.env.npm_package_name || '', 10 | meta: [ 11 | { charset: 'utf-8' }, 12 | { name: 'viewport', content: 'width=device-width, initial-scale=1' }, 13 | { hid: 'description', name: 'description', content: process.env.npm_package_description || '' } 14 | ], 15 | link: [ 16 | { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' } 17 | ] 18 | }, 19 | /* 20 | ** Customize the progress-bar color 21 | */ 22 | loading: { color: '#fff' }, 23 | /* 24 | ** Global CSS 25 | */ 26 | css: [ 27 | ], 28 | /* 29 | ** Plugins to load before mounting the App 30 | */ 31 | plugins: [ 32 | ], 33 | router: { 34 | middleware: [ 35 | 'auth' 36 | ] 37 | }, 38 | /* 39 | ** Nuxt.js dev-modules 40 | */ 41 | buildModules: [ 42 | ], 43 | /* 44 | ** Nuxt.js modules 45 | */ 46 | modules: [ 47 | '@nuxtjs/axios', 48 | ], 49 | /* 50 | ** Build configuration 51 | */ 52 | build: { 53 | /* 54 | ** You can extend webpack config here 55 | */ 56 | extend (config, ctx) { 57 | } 58 | }, 59 | env: { 60 | API_HOST: process.env.API_HOST 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /nuxt/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wp-headless-previews", 3 | "version": "1.0.0", 4 | "description": "WP Nuxt Previews", 5 | "author": "Braid", 6 | "private": true, 7 | "scripts": { 8 | "dev": "nuxt", 9 | "build": "nuxt build", 10 | "start": "nuxt start", 11 | "generate": "nuxt generate" 12 | }, 13 | "dependencies": { 14 | "axios": "^0.19.0", 15 | "dotenv": "^8.1.0", 16 | "nuxt": "^2.0.0" 17 | }, 18 | "devDependencies": { 19 | "@nuxtjs/axios": "^5.6.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /nuxt/pages/README.md: -------------------------------------------------------------------------------- 1 | # PAGES 2 | 3 | This directory contains your Application Views and Routes. 4 | The framework reads all the `*.vue` files inside this directory and creates the router of your application. 5 | 6 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/routing). 7 | -------------------------------------------------------------------------------- /nuxt/pages/_.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 54 | 55 | 58 | -------------------------------------------------------------------------------- /nuxt/pages/index.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 40 | 41 | 73 | -------------------------------------------------------------------------------- /nuxt/plugins/README.md: -------------------------------------------------------------------------------- 1 | # PLUGINS 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains Javascript plugins that you want to run before mounting the root Vue.js application. 6 | 7 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/plugins). 8 | -------------------------------------------------------------------------------- /nuxt/static/README.md: -------------------------------------------------------------------------------- 1 | # STATIC 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your static files. 6 | Each file inside this directory is mapped to `/`. 7 | Thus you'd want to delete this README.md before deploying to production. 8 | 9 | Example: `/static/robots.txt` is mapped as `/robots.txt`. 10 | 11 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/assets#static). 12 | -------------------------------------------------------------------------------- /nuxt/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris-geelhoed/wp-headless-previews/34e575c307297a73f20fca4e70f3fc92ac96f306/nuxt/static/favicon.ico -------------------------------------------------------------------------------- /nuxt/store/README.md: -------------------------------------------------------------------------------- 1 | # STORE 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your Vuex Store files. 6 | Vuex Store option is implemented in the Nuxt.js framework. 7 | 8 | Creating a file in this directory automatically activates the option in the framework. 9 | 10 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/vuex-store). 11 | -------------------------------------------------------------------------------- /readme-images/front-end-domain-field.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris-geelhoed/wp-headless-previews/34e575c307297a73f20fca4e70f3fc92ac96f306/readme-images/front-end-domain-field.jpg -------------------------------------------------------------------------------- /readme-images/private-page-with-token.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris-geelhoed/wp-headless-previews/34e575c307297a73f20fca4e70f3fc92ac96f306/readme-images/private-page-with-token.jpg -------------------------------------------------------------------------------- /readme-images/public-page.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris-geelhoed/wp-headless-previews/34e575c307297a73f20fca4e70f3fc92ac96f306/readme-images/public-page.jpg -------------------------------------------------------------------------------- /wp/.gitignore: -------------------------------------------------------------------------------- 1 | # folders and files to be ignored by git 2 | 3 | .htaccess 4 | 5 | wp-content/themes/*/dist/ 6 | 7 | ############ 8 | ## IDEs 9 | ############ 10 | 11 | *.pydevproject 12 | .project 13 | .metadata 14 | *.swp 15 | *~.nib 16 | local.properties 17 | .classpath 18 | .settings/ 19 | .loadpath 20 | .externalToolBuilders/ 21 | *.launch 22 | .cproject 23 | .buildpath 24 | nbproject/ 25 | .vscode 26 | 27 | ############ 28 | ## OSes 29 | ############ 30 | 31 | [Tt]humbs.db 32 | [Dd]esktop.ini 33 | *.DS_store 34 | .DS_store? 35 | 36 | ############ 37 | ## Misc 38 | ############ 39 | 40 | bin/ 41 | tmp/ 42 | *.tmp 43 | *.bak 44 | *.log 45 | *.[Cc]ache 46 | *.cpr 47 | *.orig 48 | *.php.in 49 | .idea/ 50 | temp/ 51 | ._* 52 | .Trashes 53 | 54 | .svn 55 | 56 | *.codekit 57 | *.scssc 58 | *.sublime-project 59 | *.sublime-workspace 60 | 61 | 62 | ## WP Engine default starter Ignore file 63 | ## 64 | 65 | *~ 66 | .DS_Store 67 | .svn 68 | .cvs 69 | *.bak 70 | *.swp 71 | Thumbs.db 72 | 73 | # wordpress specific 74 | wp-config.php 75 | wp-content/uploads/ 76 | 77 | wp-content/blogs.dir/ 78 | wp-content/upgrade/* 79 | wp-content/backup-db/* 80 | wp-content/advanced-cache.php 81 | wp-content/wp-cache-config.php 82 | wp-content/cache/* 83 | wp-content/cache/supercache/* 84 | #wp-content/plugins/ 85 | wp-content/themes 86 | wp-content/updraft 87 | 88 | # wpengine specific 89 | .smushit-status 90 | .gitattributes 91 | _wpeprivate 92 | wp-content/object-cache.php 93 | wp-content/mu-plugins/mu-plugin.php 94 | wp-content/mu-plugins/slt-force-strong-passwords.php 95 | wp-content/mu-plugins/limit-login-attempts 96 | wp-content/mu-plugins/wpengine-common 97 | wp-content/mysql.sql 98 | wp-content/mu-plugins/0-worker.php 99 | 100 | # wp core (as of 3.4.1) 101 | /db-config.php 102 | /index.php 103 | /license.txt 104 | /readme.html 105 | /wp-activate.php 106 | /wp-app.php 107 | /wp-atom.php 108 | /wp-blog-header.php 109 | /wp-comments-post.php 110 | /wp-commentsrss2.php 111 | # /wp-config-sample.php 112 | /wp-cron.php 113 | /wp-feed.php 114 | /wp-links-opml.php 115 | /wp-load.php 116 | /wp-login.php 117 | /wp-mail.php 118 | /wp-rdf.php 119 | /wp-rss.php 120 | /wp-rss2.php 121 | /wp-pass.php 122 | /wp-register.php 123 | /wp-settings.php 124 | /wp-signup.php 125 | /wp-trackback.php 126 | /xmlrpc.php 127 | /wp-admin 128 | /wp-includes 129 | /wp-content/index.php 130 | /wp-content/themes/twentyfourteen 131 | /wp-content/themes/twentyfifteen 132 | /wp-content/themes/twentysixteen 133 | /wp-content/themes/twentyseventeen 134 | /wp-content/themes/index.php 135 | /wp-content/plugins/index.php 136 | 137 | 138 | # large/disallowed file types 139 | # a CDN should be used for these 140 | *.hqx 141 | *.bin 142 | *.exe 143 | *.dll 144 | *.deb 145 | *.dmg 146 | *.iso 147 | *.img 148 | *.msi 149 | *.msp 150 | *.msm 151 | *.mid 152 | *.midi 153 | *.kar 154 | *.mp3 155 | *.ogg 156 | *.m4a 157 | *.ra 158 | *.3gpp 159 | *.3gp 160 | *.mp4 161 | *.mpeg 162 | *.mpg 163 | *.mov 164 | *.webm 165 | *.flv 166 | *.m4v 167 | *.mng 168 | *.asx 169 | *.asf 170 | *.wmv 171 | *.avi 172 | **/*/node_modules/ 173 | *.zip -------------------------------------------------------------------------------- /wp/.htaccess.example: -------------------------------------------------------------------------------- 1 | Header set Access-Control-Allow-Origin "*" 2 | 3 | SetEnv BRAID_PREVIEW_USER_LOGIN Braid_Preview_User 4 | SetEnv BRAID_PREVIEW_USER_PASSWORD SomePasswordForThePreviewUser 5 | 6 | 7 | RewriteEngine on 8 | RewriteCond %{HTTP:Authorization} ^(.*) 9 | RewriteRule ^(.*) - [E=HTTP_AUTHORIZATION:%1] 10 | 11 | 12 | # BEGIN WordPress 13 | 14 | RewriteEngine On 15 | RewriteBase / 16 | RewriteRule ^index\.php$ - [L] 17 | RewriteCond %{REQUEST_FILENAME} !-f 18 | RewriteCond %{REQUEST_FILENAME} !-d 19 | RewriteRule . /index.php [L] 20 | 21 | 22 | # END WordPress -------------------------------------------------------------------------------- /wp/wp-config-sample.php: -------------------------------------------------------------------------------- 1 | 'classic', // Accepted values: 'classic', 'block'. 200 | * 'allow-users' => false, 201 | * 202 | * @param boolean To override the settings return an array with the above keys. 203 | */ 204 | $settings = apply_filters( 'classic_editor_plugin_settings', false ); 205 | 206 | if ( is_array( $settings ) ) { 207 | return array( 208 | 'editor' => ( isset( $settings['editor'] ) && $settings['editor'] === 'block' ) ? 'block' : 'classic', 209 | 'allow-users' => ! empty( $settings['allow-users'] ), 210 | 'hide-settings-ui' => true, 211 | ); 212 | } 213 | 214 | if ( ! empty( self::$settings ) && $refresh === 'no' ) { 215 | return self::$settings; 216 | } 217 | 218 | if ( is_multisite() ) { 219 | $defaults = array( 220 | 'editor' => get_network_option( null, 'classic-editor-replace' ) === 'block' ? 'block' : 'classic', 221 | 'allow-users' => false, 222 | ); 223 | 224 | /** 225 | * Filters the default network options. 226 | * 227 | * @param array $defaults The default options array. See `classic_editor_plugin_settings` for supported keys and values. 228 | */ 229 | $defaults = apply_filters( 'classic_editor_network_default_settings', $defaults ); 230 | 231 | if ( get_network_option( null, 'classic-editor-allow-sites' ) !== 'allow' ) { 232 | // Per-site settings are disabled. Return default network options nad hide the settings UI. 233 | $defaults['hide-settings-ui'] = true; 234 | return $defaults; 235 | } 236 | 237 | // Override with the site options. 238 | $editor_option = get_option( 'classic-editor-replace' ); 239 | $allow_users_option = get_option( 'classic-editor-allow-users' ); 240 | 241 | if ( $editor_option ) { 242 | $defaults['editor'] = $editor_option; 243 | } 244 | if ( $allow_users_option ) { 245 | $defaults['allow-users'] = ( $allow_users_option === 'allow' ); 246 | } 247 | 248 | $editor = ( isset( $defaults['editor'] ) && $defaults['editor'] === 'block' ) ? 'block' : 'classic'; 249 | $allow_users = ! empty( $defaults['allow-users'] ); 250 | } else { 251 | $allow_users = ( get_option( 'classic-editor-allow-users' ) === 'allow' ); 252 | $option = get_option( 'classic-editor-replace' ); 253 | 254 | // Normalize old options. 255 | if ( $option === 'block' || $option === 'no-replace' ) { 256 | $editor = 'block'; 257 | } else { 258 | // empty( $option ) || $option === 'classic' || $option === 'replace'. 259 | $editor = 'classic'; 260 | } 261 | } 262 | 263 | // Override the defaults with the user options. 264 | if ( ( ! isset( $GLOBALS['pagenow'] ) || $GLOBALS['pagenow'] !== 'options-writing.php' ) && $allow_users ) { 265 | $user_options = get_user_option( 'classic-editor-settings' ); 266 | 267 | if ( $user_options === 'block' || $user_options === 'classic' ) { 268 | $editor = $user_options; 269 | } 270 | } 271 | 272 | self::$settings = array( 273 | 'editor' => $editor, 274 | 'hide-settings-ui' => false, 275 | 'allow-users' => $allow_users, 276 | ); 277 | 278 | return self::$settings; 279 | } 280 | 281 | private static function is_classic( $post_id = 0 ) { 282 | if ( ! $post_id ) { 283 | $post_id = self::get_edited_post_id(); 284 | } 285 | 286 | if ( $post_id ) { 287 | $settings = self::get_settings(); 288 | 289 | if ( $settings['allow-users'] && ! isset( $_GET['classic-editor__forget'] ) ) { 290 | $which = get_post_meta( $post_id, 'classic-editor-remember', true ); 291 | 292 | if ( $which ) { 293 | // The editor choice will be "remembered" when the post is opened in either Classic or Block editor. 294 | if ( 'classic-editor' === $which ) { 295 | return true; 296 | } elseif ( 'block-editor' === $which ) { 297 | return false; 298 | } 299 | } 300 | 301 | return ( ! self::has_blocks( $post_id ) ); 302 | } 303 | } 304 | 305 | if ( isset( $_GET['classic-editor'] ) ) { 306 | return true; 307 | } 308 | 309 | return false; 310 | } 311 | 312 | /** 313 | * Get the edited post ID (early) when loading the Edit Post screen. 314 | */ 315 | private static function get_edited_post_id() { 316 | if ( 317 | ! empty( $_GET['post'] ) && 318 | ! empty( $_GET['action'] ) && 319 | $_GET['action'] === 'edit' && 320 | ! empty( $GLOBALS['pagenow'] ) && 321 | $GLOBALS['pagenow'] === 'post.php' 322 | ) { 323 | return (int) $_GET['post']; // post_ID 324 | } 325 | 326 | return 0; 327 | } 328 | 329 | public static function register_settings() { 330 | // Add an option to Settings -> Writing 331 | register_setting( 'writing', 'classic-editor-replace', array( 332 | 'sanitize_callback' => array( __CLASS__, 'validate_option_editor' ), 333 | ) ); 334 | 335 | register_setting( 'writing', 'classic-editor-allow-users', array( 336 | 'sanitize_callback' => array( __CLASS__, 'validate_option_allow_users' ), 337 | ) ); 338 | 339 | add_option_whitelist( array( 340 | 'writing' => array( 'classic-editor-replace', 'classic-editor-allow-users' ), 341 | ) ); 342 | 343 | $heading_1 = __( 'Default editor for all users', 'classic-editor' ); 344 | $heading_2 = __( 'Allow users to switch editors', 'classic-editor' ); 345 | 346 | add_settings_field( 'classic-editor-1', $heading_1, array( __CLASS__, 'settings_1' ), 'writing' ); 347 | add_settings_field( 'classic-editor-2', $heading_2, array( __CLASS__, 'settings_2' ), 'writing' ); 348 | } 349 | 350 | public static function save_user_settings( $user_id ) { 351 | if ( 352 | isset( $_POST['classic-editor-user-settings'] ) && 353 | isset( $_POST['classic-editor-replace'] ) && 354 | wp_verify_nonce( $_POST['classic-editor-user-settings'], 'allow-user-settings' ) 355 | ) { 356 | $user_id = (int) $user_id; 357 | 358 | if ( $user_id !== get_current_user_id() && ! current_user_can( 'edit_user', $user_id ) ) { 359 | return; 360 | } 361 | 362 | $editor = self::validate_option_editor( $_POST['classic-editor-replace'] ); 363 | update_user_option( $user_id, 'classic-editor-settings', $editor ); 364 | } 365 | } 366 | 367 | /** 368 | * Validate 369 | */ 370 | public static function validate_option_editor( $value ) { 371 | if ( $value === 'block' ) { 372 | return 'block'; 373 | } 374 | 375 | return 'classic'; 376 | } 377 | 378 | public static function validate_option_allow_users( $value ) { 379 | if ( $value === 'allow' ) { 380 | return 'allow'; 381 | } 382 | 383 | return 'disallow'; 384 | } 385 | 386 | public static function settings_1() { 387 | $settings = self::get_settings( 'refresh' ); 388 | 389 | ?> 390 |
391 |

392 | /> 393 | 394 |

395 |

396 | /> 397 | 398 |

399 |
400 | 407 | 414 |
415 |

416 | /> 417 | 418 |

419 |

420 | /> 421 | 422 |

423 |
424 | 444 | 445 | 446 | 447 | 451 | 452 |
448 | 449 | 450 |
453 | 454 | 462 |

463 | 464 | 465 | 466 | 467 | 477 | 478 | 479 | 480 | 485 | 486 |
468 |

469 | /> 470 | 471 |

472 |

473 | /> 474 | 475 |

476 |
481 | > 482 | 483 |

484 |
487 | 515 | 516 | ID, 'classic-editor' ); 527 | } 528 | } 529 | 530 | /** 531 | * Remember when the Block Editor was used to edit a post. 532 | */ 533 | public static function remember_block_editor( $editor_settings, $post ) { 534 | $post_type = get_post_type( $post ); 535 | 536 | if ( $post_type && self::can_edit_post_type( $post_type ) ) { 537 | self::remember( $post->ID, 'block-editor' ); 538 | } 539 | 540 | return $editor_settings; 541 | } 542 | 543 | private static function remember( $post_id, $editor ) { 544 | if ( get_post_meta( $post_id, 'classic-editor-remember', true ) !== $editor ) { 545 | update_post_meta( $post_id, 'classic-editor-remember', $editor ); 546 | } 547 | } 548 | 549 | /** 550 | * Choose which editor to use for a post. 551 | * 552 | * Passes through `$which_editor` for Block Editor (it's sets to `true` but may be changed by another plugin). 553 | * 554 | * @uses `use_block_editor_for_post` filter. 555 | * 556 | * @param boolean $use_block_editor True for Block Editor, false for Classic Editor. 557 | * @param WP_Post $post The post being edited. 558 | * @return boolean True for Block Editor, false for Classic Editor. 559 | */ 560 | public static function choose_editor( $use_block_editor, $post ) { 561 | $settings = self::get_settings(); 562 | $editors = self::get_enabled_editors_for_post( $post ); 563 | 564 | // If no editor is supported, pass through `$use_block_editor`. 565 | if ( ! $editors['block_editor'] && ! $editors['classic_editor'] ) { 566 | return $use_block_editor; 567 | } 568 | 569 | // Open the default editor when no $post and for "Add New" links, 570 | // or the alternate editor when the user is switching editors. 571 | if ( empty( $post->ID ) || $post->post_status === 'auto-draft' ) { 572 | if ( 573 | ( $settings['editor'] === 'classic' && ! isset( $_GET['classic-editor__forget'] ) ) || // Add New 574 | ( isset( $_GET['classic-editor'] ) && isset( $_GET['classic-editor__forget'] ) ) // Switch to Classic Editor when no draft post. 575 | ) { 576 | $use_block_editor = false; 577 | } 578 | } elseif ( self::is_classic( $post->ID ) ) { 579 | $use_block_editor = false; 580 | } 581 | 582 | // Enforce the editor if set by plugins. 583 | if ( $use_block_editor && ! $editors['block_editor'] ) { 584 | $use_block_editor = false; 585 | } elseif ( ! $use_block_editor && ! $editors['classic_editor'] && $editors['block_editor'] ) { 586 | $use_block_editor = true; 587 | } 588 | 589 | return $use_block_editor; 590 | } 591 | 592 | /** 593 | * Keep the `classic-editor` query arg through redirects when saving posts. 594 | */ 595 | public static function redirect_location( $location ) { 596 | if ( 597 | isset( $_REQUEST['classic-editor'] ) || 598 | ( isset( $_POST['_wp_http_referer'] ) && strpos( $_POST['_wp_http_referer'], '&classic-editor' ) !== false ) 599 | ) { 600 | $location = add_query_arg( 'classic-editor', '', $location ); 601 | } 602 | 603 | return $location; 604 | } 605 | 606 | /** 607 | * Keep the `classic-editor` query arg when looking at revisions. 608 | */ 609 | public static function get_edit_post_link( $url ) { 610 | $settings = self::get_settings(); 611 | 612 | if ( isset( $_REQUEST['classic-editor'] ) || $settings['editor'] === 'classic' ) { 613 | $url = add_query_arg( 'classic-editor', '', $url ); 614 | } 615 | 616 | return $url; 617 | } 618 | 619 | public static function add_meta_box( $post_type, $post ) { 620 | $editors = self::get_enabled_editors_for_post( $post ); 621 | 622 | if ( ! $editors['block_editor'] || ! $editors['classic_editor'] ) { 623 | // Editors cannot be switched. 624 | return; 625 | } 626 | 627 | $id = 'classic-editor-switch-editor'; 628 | $title = __( 'Editor', 'classic-editor' ); 629 | $callback = array( __CLASS__, 'do_meta_box' ); 630 | $args = array( 631 | '__back_compat_meta_box' => true, 632 | ); 633 | 634 | add_meta_box( $id, $title, $callback, null, 'side', 'default', $args ); 635 | } 636 | 637 | public static function do_meta_box( $post ) { 638 | $edit_url = get_edit_post_link( $post->ID, 'raw' ); 639 | 640 | // Switching to Block Editor. 641 | $edit_url = remove_query_arg( 'classic-editor', $edit_url ); 642 | // Forget the previous value when going to a specific editor. 643 | $edit_url = add_query_arg( 'classic-editor__forget', '', $edit_url ); 644 | 645 | ?> 646 |

647 | 648 |

649 | __( 'Switch to Classic Editor', 'classic-editor' ) ) 672 | ); 673 | } 674 | 675 | /** 676 | * Add a link to the settings on the Plugins screen. 677 | */ 678 | public static function add_settings_link( $links, $file ) { 679 | $settings = self::get_settings(); 680 | 681 | if ( $file === 'classic-editor/classic-editor.php' && ! $settings['hide-settings-ui'] && current_user_can( 'manage_options' ) ) { 682 | if ( current_filter() === 'plugin_action_links' ) { 683 | $url = admin_url( 'options-writing.php#classic-editor-options' ); 684 | } else { 685 | $url = admin_url( '/network/settings.php#classic-editor-options' ); 686 | } 687 | 688 | // Prevent warnings in PHP 7.0+ when a plugin uses this filter incorrectly. 689 | $links = (array) $links; 690 | $links[] = sprintf( '%s', $url, __( 'Settings', 'classic-editor' ) ); 691 | } 692 | 693 | return $links; 694 | } 695 | 696 | private static function can_edit_post_type( $post_type ) { 697 | $can_edit = false; 698 | 699 | if ( function_exists( 'gutenberg_can_edit_post_type' ) ) { 700 | $can_edit = gutenberg_can_edit_post_type( $post_type ); 701 | } elseif ( function_exists( 'use_block_editor_for_post_type' ) ) { 702 | $can_edit = use_block_editor_for_post_type( $post_type ); 703 | } 704 | 705 | return $can_edit; 706 | } 707 | 708 | /** 709 | * Checks which editors are enabled for the post type. 710 | * 711 | * @param string $post_type The post type. 712 | * @return array Associative array of the editors and whether they are enabled for the post type. 713 | */ 714 | private static function get_enabled_editors_for_post_type( $post_type ) { 715 | if ( isset( self::$supported_post_types[ $post_type ] ) ) { 716 | return self::$supported_post_types[ $post_type ]; 717 | } 718 | 719 | $classic_editor = post_type_supports( $post_type, 'editor' ); 720 | $block_editor = self::can_edit_post_type( $post_type ); 721 | 722 | $editors = array( 723 | 'classic_editor' => $classic_editor, 724 | 'block_editor' => $block_editor, 725 | ); 726 | 727 | /** 728 | * Filters the editors that are enabled for the post type. 729 | * 730 | * @param array $editors Associative array of the editors and whether they are enabled for the post type. 731 | * @param string $post_type The post type. 732 | */ 733 | $editors = apply_filters( 'classic_editor_enabled_editors_for_post_type', $editors, $post_type ); 734 | self::$supported_post_types[ $post_type ] = $editors; 735 | 736 | return $editors; 737 | } 738 | 739 | /** 740 | * Checks which editors are enabled for the post. 741 | * 742 | * @param WP_Post $post The post object. 743 | * @return array Associative array of the editors and whether they are enabled for the post. 744 | */ 745 | private static function get_enabled_editors_for_post( $post ) { 746 | $post_type = get_post_type( $post ); 747 | 748 | if ( ! $post_type ) { 749 | return array( 750 | 'classic_editor' => false, 751 | 'block_editor' => false, 752 | ); 753 | } 754 | 755 | $editors = self::get_enabled_editors_for_post_type( $post_type ); 756 | 757 | /** 758 | * Filters the editors that are enabled for the post. 759 | * 760 | * @param array $editors Associative array of the editors and whether they are enabled for the post. 761 | * @param WP_Post $post The post object. 762 | */ 763 | return apply_filters( 'classic_editor_enabled_editors_for_post', $editors, $post ); 764 | } 765 | 766 | /** 767 | * Adds links to the post/page screens to edit any post or page in 768 | * the Classic Editor or Block Editor. 769 | * 770 | * @param array $actions Post actions. 771 | * @param WP_Post $post Edited post. 772 | * @return array Updated post actions. 773 | */ 774 | public static function add_edit_links( $actions, $post ) { 775 | // This is in Gutenberg, don't duplicate it. 776 | if ( array_key_exists( 'classic', $actions ) ) { 777 | unset( $actions['classic'] ); 778 | } 779 | 780 | if ( ! array_key_exists( 'edit', $actions ) ) { 781 | return $actions; 782 | } 783 | 784 | $edit_url = get_edit_post_link( $post->ID, 'raw' ); 785 | 786 | if ( ! $edit_url ) { 787 | return $actions; 788 | } 789 | 790 | $editors = self::get_enabled_editors_for_post( $post ); 791 | 792 | // Do not show the links if only one editor is available. 793 | if ( ! $editors['classic_editor'] || ! $editors['block_editor'] ) { 794 | return $actions; 795 | } 796 | 797 | // Forget the previous value when going to a specific editor. 798 | $edit_url = add_query_arg( 'classic-editor__forget', '', $edit_url ); 799 | 800 | // Build the edit actions. See also: WP_Posts_List_Table::handle_row_actions(). 801 | $title = _draft_or_post_title( $post->ID ); 802 | 803 | // Link to the Block Editor. 804 | $url = remove_query_arg( 'classic-editor', $edit_url ); 805 | $text = _x( 'Edit (Block Editor)', 'Editor Name', 'classic-editor' ); 806 | /* translators: %s: post title */ 807 | $label = sprintf( __( 'Edit “%s” in the Block Editor', 'classic-editor' ), $title ); 808 | $edit_block = sprintf( '%s', esc_url( $url ), esc_attr( $label ), $text ); 809 | 810 | // Link to the Classic Editor. 811 | $url = add_query_arg( 'classic-editor', '', $edit_url ); 812 | $text = _x( 'Edit (Classic Editor)', 'Editor Name', 'classic-editor' ); 813 | /* translators: %s: post title */ 814 | $label = sprintf( __( 'Edit “%s” in the Classic Editor', 'classic-editor' ), $title ); 815 | $edit_classic = sprintf( '%s', esc_url( $url ), esc_attr( $label ), $text ); 816 | 817 | $edit_actions = array( 818 | 'classic-editor-block' => $edit_block, 819 | 'classic-editor-classic' => $edit_classic, 820 | ); 821 | 822 | // Insert the new Edit actions instead of the Edit action. 823 | $edit_offset = array_search( 'edit', array_keys( $actions ), true ); 824 | array_splice( $actions, $edit_offset, 1, $edit_actions ); 825 | 826 | return $actions; 827 | } 828 | 829 | /** 830 | * Show the editor that will be used in a "post state" in the Posts list table. 831 | */ 832 | public static function add_post_state( $post_states, $post ) { 833 | if ( get_post_status( $post ) === 'trash' ) { 834 | return $post_states; 835 | } 836 | 837 | $editors = self::get_enabled_editors_for_post( $post ); 838 | 839 | if ( ! $editors['classic_editor'] && ! $editors['block_editor'] ) { 840 | return $post_states; 841 | } elseif ( $editors['classic_editor'] && ! $editors['block_editor'] ) { 842 | // Forced to Classic Editor. 843 | $state = '' . _x( 'Classic Editor', 'Editor Name', 'classic-editor' ) . ''; 844 | } elseif ( ! $editors['classic_editor'] && $editors['block_editor'] ) { 845 | // Forced to Block Editor. 846 | $state = '' . _x( 'Block Editor', 'Editor Name', 'classic-editor' ) . ''; 847 | } else { 848 | $last_editor = get_post_meta( $post->ID, 'classic-editor-remember', true ); 849 | 850 | if ( $last_editor ) { 851 | $is_classic = ( $last_editor === 'classic-editor' ); 852 | } elseif ( ! empty( $post->post_content ) ) { 853 | $is_classic = ! self::has_blocks( $post->post_content ); 854 | } else { 855 | $settings = self::get_settings(); 856 | $is_classic = ( $settings['editor'] === 'classic' ); 857 | } 858 | 859 | $state = $is_classic ? _x( 'Classic Editor', 'Editor Name', 'classic-editor' ) : _x( 'Block Editor', 'Editor Name', 'classic-editor' ); 860 | } 861 | 862 | // Fix PHP 7+ warnings if another plugin returns unexpected type. 863 | $post_states = (array) $post_states; 864 | $post_states['classic-editor-plugin'] = $state; 865 | 866 | return $post_states; 867 | } 868 | 869 | public static function add_edit_php_inline_style() { 870 | ?> 871 | 879 | post_content; 906 | } 907 | } 908 | 909 | return false !== strpos( (string) $post, '